MDK Logo

Site security blueprint

Steps, options, and suggestions for securing an enterprise MDK site assembled from UI, Gateway, Kernel, and Workers

MDK ships no user identity at any tier. The Gateway serves every plugin route to any caller, Kernel inspects no human identity, and WorkerRuntime applies no caller allowlist. Identity, allowlists, TLS, and network policy are part of the site you build, not defaults the SDK turns on. Treat mvp-site, full-site, and per-family snippets as boot demos, not a production template.

Overview

This page is a blueprint for securing an enterprise site you assemble from MDK: UI, Gateway, Kernel, and Workers on separate hosts or containers. Walk the steps in order. Each step names what to do, the options MDK actually gives you, and a suggested default. Use it with the security boundaries concept (what each layer trusts) and the control plane (how a request travels).

MDK leaves these controls to you on purpose. The SDK coordinates devices; your site owns who may talk to it.

Constraints the SDK does not lift

Build around these. They are current 0.y.z behavior, not optional extras.

LayerWhat MDK doesWhat you still own
UIToken attachment if you pass an AuthProvider. No route guards ship with the toolkitSession, HTTPS, apiBaseUrl, hiding write controls
GatewayPlugin host over HTTP. Manifest "auth" / "permissions" have no reader. Bundled @tetherto/mdk-plugin-auth is not wiredIdentity checks in every controller
KernelEncrypted HRPC. Optional caller-key allowlist (auth.whitelist, default empty). authPerms only on approval-gated writes, taken from the callerAllowlist, mapping verified roles to authPerms / voter
WorkersNoise transport and key-addressed HRPC. No caller allowlist. No actor in handler contextNetwork isolation, secrets, payload checks, audit upstream
MCP / agentPOST /mcp on 127.0.0.1 with no user auth. Agent plugin binds to local without an identity layerBind policy, human approval for writes, real userId
Devices / poolsVendor protocol APIs and authenticationDevice-network isolation

Consumers enter through the Gateway. The browser never holds a Kernel key. Direct Kernel or Worker reachability is a backend-network concern, not a product feature.

Blueprint

Choose a network layout

Pick a deployment topology before you write auth code. Isolation is the first control; identity sits on top of it.

Options

OptionWhen to pick itTrade-off
Single processLocal demos, tests, smallest footprintOne heap. A compromise of the process is the whole site
Local (one OS process per service, shared directory)One machine, production-like restarts, or a step toward microservicesStronger isolation than a single heap. Kernel and Workers still share the host. No DHT
Microservices over DHTEnterprise sites: separate hosts or containers, resource limits per service, Workers away from KernelPeers that know the discovery topic can find Kernel and Workers. Key distribution and a Kernel allowlist are mandatory

Suggested default: microservices (one process or container per service, discovery over DHT), with three networks you define:

  • Operator: UI origin and Gateway HTTP, behind TLS and a reverse proxy you control
  • Backend: Kernel and Workers on separate hosts, no inbound path from browsers or agents
  • Devices: miner, container, meter, and pool APIs, reachable only from the Workers that own them

Pass the Kernel public key to a remote Gateway as kernelKey. Treat the DHT discovery topic as deployment configuration, not a credential: generate a site-specific topic, distribute it out of band, and do not reuse example or well-known values. Pair this shape with a non-empty Kernel allowlist in the next step. Without that allowlist, DHT makes Kernel reachable to any peer that has the topic and the listener key.

Do not pass @tetherto/mdk-client or Kernel keys into the browser. HTTP to the Gateway is the only operator path.

Admit only the Gateway to Kernel

Pre v1.0, auth.whitelist defaults to [] and admits any HRPC caller that knows Kernel's public key. A production Kernel always has a non-empty allowlist.

Options

OptionWhen to pick itTrade-off
Empty allowlistLocal development onlyAnyone with the Kernel public key can read telemetry and dispatch commands, skipping the Gateway
Allowlist the Gateway DHT public keyEvery real siteYou must keep that Gateway key stable across restarts
Allowlist Gateway plus extra backend clientsA second mdk-client service you trust (batch jobs, a second Gateway)Each extra key is a full Kernel principal. Review it like a production credential

Suggested default: one persistent Gateway key pair, that hex public key only, on Kernel:

const kernel = await getKernel({
  hrpc: { whitelist: ['<gateway-dht-public-key-hex>'] }
})

The auth-whitelist example shows the exchange. Generate the Gateway key once, store it with the same care as a TLS key, and put it on the allowlist. Do not generate a fresh key at each boot: the Gateway does not yet pass a persistent caller seed, so re-check the published Gateway key after a bounce.

On separate hosts, pass Kernel's public key to the Gateway as kernelKey. Do not rely on the well-known key file (<tmpdir>/mdk/.kernel-key) across machines. Where that file still exists on the Kernel host, keep it mode 0600 on a directory you own; it is not deleted on shutdown. Do not expose Kernel's HRPC listener off the backend network.

Isolate Workers and devices

WorkerRuntime does not enforce a caller allowlist. Any backend peer that can reach a Worker and address its public key may send command.request. Kernel's allowlist does not protect a Worker that is dialed directly.

Options

OptionWhen to pick itTrade-off
Workers only on the Kernel network, keys unpublishedEvery siteOperational discipline: no Worker port or DHT topic on the operator network
One Worker process per trust zoneDifferent device families or vendors must not share credentialsMore processes to supervise
Device APIs on a third network (CGMiner 4028, miner HTTP, container APIs)Real hardwareRequired when the vendor protocol has weak or no auth (Avalon / CGMiner have none; Antminer uses digest auth; Whatsminer uses a device password)

Suggested default: persist each Worker's storeDir so its key is stable, treat that key as a machine identity, and register the Worker with Kernel only after the device network is isolated. On DHT, keep Worker public keys and the discovery topic off the operator network and out of git. Inject device passwords and pool API keys from a secret manager or protected environment. Never put secrets in mdk-contract.json, git, or a committed seed file.

Validate command payloads again in the handler (types, ranges, allowed targets). Kernel checks the command name against the contract; it does not know your hardware's unsafe parameter combinations. Redact credentials and vendor responses in handler errors and logs. For CGMiner-style devices, network isolation is the control: changing the Worker password field does not add a wire handshake the protocol lacks.

Put identity in Gateway controllers

The Gateway is the only supported user-auth seam. Every bundled plugin is served to any caller. Paths under /auth/metrics/* are historical names, not a login gate. Manifest "auth": true changes nothing.

Do not mount @tetherto/mdk-plugin-auth expecting it to work. The Gateway does not register it, and its controllers still expect ctx.authLib, a services argument, and req._info.user, which the current host does not provide.

Options

OptionWhen to pick itTrade-off
Your SSO (OIDC / SAML / existing IdP), validated in each controllerProduction sites with an identity providerYou write the plugin. This is the supported path
Gateway-issued JWT after an OAuth redirect you implement (/oauth/...?authToken=, plus POST /auth/token and /auth/userinfo)Browser UI using gatewayRedirectAuth({ oauthBaseUrl })You still write those routes. The bundled auth plugin is not a substitute
Bearer tokens only (Authorization: Bearer), no browser redirectService accounts, scripts, or a UI that already has a tokenPair with bearerTokenAuth() in the UI
No identity (noAuth(), or nothing in the controller)Laptop demos bound to loopbackAnyone who can reach the port has the fleet

Suggested default: one identity helper shared by every controller. Map verified roles onto Kernel authPerms (miner:w, container:w, and any others you define). Never accept authPerms or voter from the HTTP client. Reject missing tokens with 401 and missing perms with 403, setting err.statusCode so the Gateway does not collapse them to 400.

const { validateToken } = require('../lib/my-identity-layer')

module.exports = async function protectedRoute (req) {
  const token = req.headers.authorization?.replace('Bearer ', '')
  if (!token) throw Object.assign(new Error('ERR_UNAUTHORIZED'), { statusCode: 401 })

  const { permissions, email } = validateToken(token)
  if (!permissions.includes('miner:w')) {
    throw Object.assign(new Error('ERR_FORBIDDEN'), { statusCode: 403 })
  }

  // Pass email and permissions into Kernel yourself. Do not take them from req.body.
}

The Gateway plugins guide owns this pattern. Also:

  • Bind HTTP to the proxy network, not a public interface without that proxy
  • Rate-limit writes. ?overwriteCache=true bypasses the request cache with no auth: ignore it for anonymous callers, or strip it in your adapter
  • The HTTP worker sets trustProxy: true. Only do that behind a proxy whose hop count you trust, or client IPs can be spoofed
  • Log actor, route, target, command, outcome, and a correlation id for every write. Redact secrets. Worker handlers cannot see actor identity, so this audit stays in the Gateway

Authorize writes

Kernel treats two write paths differently. Choose per action, then enforce the choice in the Gateway. Direct command.request has no Kernel-side permission check. Approval-gated writes read authPerms, but that array is caller-supplied once the HRPC connection is accepted.

Options

OptionWhen to pick itTrade-off
Direct command (command.request) after a Gateway perm checkLow-impact, operator-in-front actions (LED, a single-device reboot you already authorized)Fast. Kernel will not second-guess the caller. A missing controller check is full control
Approval-gated write (action.push, then votes)Fleet-changing actions: power mode, pool assignment, container PDU, bulk rebootSlower. Still requires the Gateway to set authPerms and voter from the verified token. The write-actions guide covers the HTTP side
Agent-proposed write with a human approval pauseOperator agent (approvalTimeoutMs on @tetherto/mdk-plugin-agent)The model cannot execute a write until someone approves. Without identity, every request is the same local operator

Suggested default: approval-gated writes for anything that changes hash, power, or network membership; direct commands only behind a controller that already checked a token. Do not expose a raw sendCommand route to the UI without that check.

Wire the UI session

<MdkProvider auth={...}> is how the browser attaches a credential. The toolkit does not guard routes or deny writes on its own.

Options

OptionWhen to pick itTrade-off
noAuth()Fixture-backed demos, open APIs on loopbackNo token. A finished-looking dashboard still sends unauthenticated calls
bearerTokenAuth()You already issue JWTs from the Gateway identity pluginSession ends on 401. You own login UI
gatewayRedirectAuth({ oauthBaseUrl })OAuth redirect, ?authToken= capture, refresh against POST /auth/tokenNeeds your token routes. Default MdkProvider auth without oauthBaseUrl cannot sign in
Custom AuthProviderA session model the three presets do not coverYou implement getToken, refresh, and sign-out

Suggested default for a production dashboard:

  • <MdkProvider apiBaseUrl={httpsOrigin} auth={bearerTokenAuth()}> or gatewayRedirectAuth({ oauthBaseUrl }) after those routes exist
  • HTTPS, a real apiBaseUrl, no Vite dev proxy
  • One fetch client so pages cannot skip Authorization
  • Route guards on any view that can mutate the fleet (the shell template uses <RequireAuth>; copy that idea, not noAuth())
  • Hide write controls unless the token carries the matching permission. Hiding is not authorization; the Gateway still denies
  • Scrub ?authToken= from the URL after capture. Tokens in query strings leak through access logs and Referer
  • Static bundle behind the same proxy that terminates TLS. Do not run vite dev as the production UI

The full-site example UI has no sign-in, no session, and a hardcoded profile. Do not copy that into a reachable host.

Decide about MCP and the operator agent

MCP is optional. When you enable it, it has the same authority as the process that hosts it. Standalone @tetherto/mdk-mcp and the Gateway's auto-generated server both bind 127.0.0.1 and answer POST /mcp with no user auth. Auto-generated MCP defaults to Gateway port plus 100.

Options

OptionWhen to pick itTrade-off
No MCP, no agentDashboards and scripts onlySmallest surface
MCP on loopback, operator agent on the GatewayIn-process chat for people who already passed Gateway identityKeep approvalTimeoutMs on. Pass a real userId into createSession / resumeSession. A session id in a URL must not read another operator's chat
MCP or agent published past localhostAn agent runtime on another hostMDK does not authenticate this path. Put mutual TLS or your SSO proxy in front, and authorize inside the reused HTTP handlers (autoGenerateMcp is those handlers with no extra check)

Suggested default: leave MCP on 127.0.0.1. Do not publish it with compose ports:, an SSH tunnel, or a reverse proxy unless that front door is authenticated. Treat the model provider (qvac or an OpenAI-compatible URL) as sensitive infrastructure: prompts can include site topology and, if you are careless, secrets.

Handle secrets, stores, and day-two operations

Options for secrets

OptionWhen to pick itTrade-off
Secret manager (Vault, cloud SM, Kubernetes secrets) into the host processProductionExtra moving part. This is the suggested default
Protected environment on a locked-down hostSmall single-host sitesRotation and audit are your scripts
Values in git, mdk-contract.json, example site.deploy.json, or the UI bundleNeverCredentials leak with the repo or the browser

Options for process lifecycle

OptionWhen to pick itTrade-off
SIGINT / supervisor stop so WAL and cleanup runAlwaysA SIGKILL can leave .mdk-data / storeDir unusable. The next boot may look healthy with zero Workers
Health check HTTP only (GET /auth/site)InsufficientThe Gateway can keep serving HTTP after the Kernel channel closes (CHANNEL_CLOSED) and never reconnect

Suggested default:

  • Mode 0600 (or equivalent) on Kernel db, Worker storeDir, and key files. Encrypt at rest if the host requires it
  • Back up those directories as operational data, with the same access control as the live files
  • Rotate Kernel, Gateway, and Worker keys when staff leave or a host is rebuilt, and update the allowlist in the same change
  • Practice recovery from a wiped data directory before an incident
  • Watch Gateway-to-Kernel errors, not only the HTTP port
  • Run Node.js >=24. Pin lockfiles. Prefer non-root containers with a read-only root filesystem
  • Stay on the latest main or latest tag you have reviewed. 0.y.z is initial development; older tags may not receive security fixes. Report product issues through the security policy, not public GitHub issues

Decision cheat sheet

DecisionSuggested production choiceDemo-only choice
TopologyMicroservices over DHT, three networks (operator / backend / devices)Single process on loopback
Kernel admissionNon-empty auth.whitelist with a persistent Gateway keyEmpty allowlist
Worker accessBackend network only, keys unpublishedSame host, unpublished anyway
User identityYour SSO or JWTs, checked in every Gateway controllerNo check, noAuth()
WritesApproval-gated for fleet changes; Gateway sets authPermsDirect sendCommand with no token
UIHTTPS, bearerTokenAuth or working gatewayRedirectAuth, route guardsVite proxy, noAuth(), HashRouter
MCP / agentLoopback, human approval, real userIdDisabled, or loopback with local operator
SecretsSecret manager into the processExample admin passwords

Next steps

  • Read the security boundaries: what each layer trusts, and what it does not
  • Follow the control plane: read path, direct command, and approval-gated write
  • Add identity in Gateway plugin controllers: the only supported user-auth seam
  • Restrict Kernel callers with the HRPC allowlist: transport admission for the Gateway
  • Compare deployment topologies: this blueprint assumes microservices; local and single-process are smaller footprints
  • Report product vulnerabilities through the security policy: private advisory, not a public issue

On this page