MDK Logo

Hosted-site reference configuration

Networks, allowlists, a monitoring-only and a delegated-writes profile, MCP off or on loopback, and the Kernel and Gateway settings for a managed site

Every control on this page is a network setting, an admission list, or a line of policy. MDK identifies the person or agent behind a request from the access token the Kernel's token issuer signed, and spec.rbac decides what that identity may do. TLS, network isolation and secret storage are still yours.

Overview

This page is a reference configuration for MDK on a hosted or managed mining site: fixed hosts, a controlled network, a change process, and an operator who needs to hand a network team one page. It gives values, not rationale; the site security blueprint owns the reasoning behind each control, and the locked-down site shape explains why discovery and transport are separate decisions. Pick one of the two profiles, apply the settings table, and read the can and cannot lists before promising anything to the site owner.

Networks and ports

Three networks, as the blueprint suggests. Every HRPC hop is UDP over HyperDHT, addressed by public key, so there is no fixed listener port to open between MDK processes; what each host needs is UDP reachability to its peers and to the bootstrap nodes.

NetworkComponentProtocol and portDirection
OperatorGateway HTTPTCP spec.gateway.port, behind the TLS proxy you runInbound from operators and the UI origin only
OperatorOperator shell (UI)Static files served by that same proxyInbound from operators
BackendKernel, Workers, app runtimesUDP, ephemeral local ports; HyperDHT bootstrap 49737 (public set) or your private listPeer to peer inside the backend network; outbound to the bootstrap nodes
BackendMCP serverTCP 127.0.0.1:<gateway port + 100>, loopback onlyLocal processes on the MCP host
BackendAgent model providerTCP to qvac or your OpenAI-compatible URLOutbound from the agent host only
DevicesAvalon (CGMiner)TCP 4028, no authenticationFrom the owning Worker only
DevicesWhatsminerEncrypted TCP 4028 (API v2) or 4433 (v3), session token from the API passwordFrom the owning Worker only
DevicesAntminer HTTPTCP 80, digest authenticationFrom the owning Worker only
DevicesContainers, meters, sensorsVendor API port, vendor authenticationFrom the owning Worker only
DevicesPool APIsHTTPS outboundFrom the pool Worker only

Docker ports published by startServices() bind 127.0.0.1 unless host is set, so a containerised stack exposes nothing to the backend network by accident.

What MDK can and cannot do here

MDK can:

  • Admit backend callers to Kernel by key, which lets them connect and nothing more (Kernel admission)
  • Check an access token and one spec.rbac rule on every Kernel request and every Gateway app route, default deny (policy model)
  • Hold an app to the app-<name> role spec.rbac binds to its service account app:<name>, seeded at install from the app's mdk-app.yaml permissions, and check the signed-in user's own rules as well when the app acts for one (app runtime)
  • Record every command.request attempt, refused ones included, with its caller, target, params and outcome (write-command log)
  • Admit callers to each Worker by key (allowedKeys, fail-closed) (Worker allowlist)
  • Admit Workers to Kernel by key (discovery.workerKeys, fail-closed)
  • Restrict the device addresses a Worker may register to a CIDR allowlist, conf.thing.allowedAddressCidrs (device addresses)
  • Serve the Gateway only to known Host names, rate limit by client IP, cap body size and send Helmet headers
  • Expose only read-only tools over MCP by default, require each agent's own access token on /mcp, and bind MCP to loopback with no option to do otherwise
  • Hold every write that changes hash, power or network membership for approval (write actions)
  • Run with no outside network at all, against a private bootstrap list every component shares (the local and air-gapped profile, from code, worker.js or spec.network.dht in mdk.yaml)

MDK cannot:

  • Federate sign-in. Users, groups and passwords live in the Kernel's identity store, and there is no SSO hook.
  • Allowlist Gateway clients by IP. The Gateway has a Host allowlist and a per-IP rate limit; source-IP filtering is your proxy's or firewall's job.
  • Offer a global read-only switch. Monitoring-only is a composition of the roles and bindings in Profile A, not one flag.
  • Review the grant an app asks for. mdk validate app <name> checks that the grant matches the request and binds somebody; whether that grant is narrow enough is your call before adding the app to spec.apps.

Before either profile

Both profiles are policy, so the policy exists before the stack boots. A spec.rbac that binds no user and no group, and whose bound subjects include nobody who may write the policy, is refused at mdk run rather than booted: a Kernel in that state cannot be handed a policy afterwards. That is a fresh stack, or one where only an app's account is bound. Run mdk rbac init and mdk user add <name> --admin first, then pick a profile.

Binding a user is enough to boot, so a policy that keeps policy: write out of every role still starts. That is a choice rather than an error: policy changes then go through mdk.yaml and a restart instead of mdk rbac apply.

Admission is the same in both: the Kernel admits the Gateway, the MCP server and each app runtime key so they can connect, and every Worker admits the Kernel client key (kernel.getClientPublicKey()) and nothing else, so every device call passes the Kernel's checks and write-command log. What separates the two profiles is spec.rbac.

Profile A: monitoring-only

The site reads telemetry and state. While this policy is in force, no route, tool or Kernel caller can send a command to a device.

  • Roles grant devices: read, workers: list and the read verbs of the app routes operators use; none carries devices: write, actions: write or actions: vote
  • Replace every wildcard administrator grant, the CLI account's included, with a dedicated policy-maintenance role holding policy: read, policy: write and the read permissions its callers need, bound only to whoever maintains mdk.yaml. Do not bind the admin role in this profile: it holds every resource and verb, devices: write and actions: write among them, so binding it breaks the guarantee above. mdk rbac init binds mdk-cli to admin by default, so rebind that account to the narrower role before relying on this profile.
  • Each app's app-<name> role in spec.rbac carries no write verb, so the app cannot reach command.request even when a user asks it to. The role is seeded from the app's mdk-app.yaml permissions at install and is yours from then on, so audit the role rather than the manifest.
  • spec.apps lists no app whose route table carries a write route; the operator shell therefore shows no write controls
  • MCP is absent, or runs with the default tool set and no allowWrite or allowTools
  • The approval flow is never reached: with nobody holding actions: write, action.push is refused

An identity whose rules cover no write verb gets ERR_KERNEL_ACTION_FORBIDDEN on command.request or action.push. A refused command.request is recorded in the write-command log; a refused action.push is denied before dispatch and leaves no entry. Monitoring-only rests on the policy, so read it back with mdk rbac show after every change. An identity with policy: write can replace this policy with one that permits device writes; this profile does not prevent its maintainers from enabling writes through a policy change.

Profile B: delegated writes

Named operators may change the fleet, and every fleet-changing write waits for approval.

  • Roles add devices: write on the devices each team owns, scoped with resourceNames such as miner/antminer/* or miner/antminer/rack-3/*, and actions: write and actions: vote on the people who push and approve. A device name is family/brand/worker/device and a pattern is anchored at both ends, so a pattern that does not start at the family segment matches nothing and denies.
  • Bindings name Group subjects rather than individuals, so joining a team is an identity change and not a policy change
  • Users sign in at the Gateway's POST /auth/login; the Gateway checks a bearer token and the route's rule before body validation, the cache or the app, and the app receives the verified identity as req._info, never the token (auth in app controllers)
  • An app acting for a signed-in user calls Kernel through req.mdk, so Kernel requires both the app's and the user's rules to allow the write; a payload's self-asserted authPerms are ignored for any caller that carries a token
  • Power mode, pool assignment, container PDU and bulk reboot go through action.push with the vote threshold the site owner signed off; single-device, operator-in-front actions may use a direct command.request
  • MCP write tools stay hidden unless spec.mcp.allowWrite or spec.mcp.allowTools names them, and each agent signs in with its own service account whose rules cover the tools it may call

MCP: off or loopback

Off means not started: MCP is never part of the default mdk run target, so a stack that does not run mdk run mcp and does not list the agent app in spec.apps has no MCP surface and no agent. That is the Profile A default.

On means loopback. The MCP server listens on 127.0.0.1 at the Gateway port plus 100 (or spec.mcp.port), and there is no option to bind elsewhere. Reaching it from another host is your reverse proxy's or tunnel's decision, and that front door must authenticate, because MDK does not. With it on:

  • Write tools are hidden unless spec.mcp.allowWrite: true or spec.mcp.allowTools lists them
  • Every /mcp call carries one agent's own access token: mdk sa add <agent> creates the credential, and the agent signs in at the Gateway's POST /auth/token. spec.mcp.token and spec.mcp.identity are refused in mdk.yaml
  • The agent's rules decide which app routes are listed as tools at all, and a route that declares a resourceName is checked again on each call
  • Requests whose Host is not a loopback name, or that carry an unlisted Origin, get 403

The operator agent, if you run it, keeps approvalTimeoutMs on and signs in as its own service account like any other agent, so a model never executes a write that no one approved.

Gateway and Kernel settings

Each row names the setting, its configuration surface, and the value for each profile. Some controls are available through mdk.yaml; others require programmatic startup or component configuration files.

mdk run does not expose pinned Worker admission or the Gateway bind address as stack settings. It admits callers from the trusted-keys directory, which lets them connect and nothing more, so both profiles are reached from spec.rbac rather than from the admission list. A host that names every caller key itself disables automatic directory admission with trustedKeysDir: false. The trust and admission reference describes automatic key exchange. Pinned Worker admission through discovery.workerKeys applies to topic discovery; local discovery and direct registration skip that check.

Settingmdk.yamlProgrammaticProfile AProfile B
Access policyspec.rbac, written by mdk rbac init, mdk user add and mdk sa add, applied on mdk rbac applygetKernel({ rbac })Read verbs, plus policy: write for policy maintenance; no wildcard grantsAdds devices: write, actions: write and actions: vote on named roles
Kernel caller allowlistNot exposed; trusted-directory callers are admittedgetKernel({ hrpc: { whitelist }, trustedKeysDir: false }); explicitly manage later admissionsEvery backend caller key, and no rolesSame
Worker admission to KernelNot exposed; CLI uses local discoverygetKernel({ discovery: { workerKeys } }), kernel.allowWorkerKey() for topic discoveryEvery topic-discovered Worker key, pinnedSame
allowAnyWorkerneverdiscovery.allowAnyWorkerfalsefalse
Worker caller allowlistset by mdk run from the trusted-keys dirboot function allowedKeys, MDK_WORKER_ALLOWED_KEYS for worker.jsThe Kernel client key aloneSame
Device address allowlistNot exposedconf.thing.allowedAddressCidrs on Worker boot functionsThe device VLAN CIDRSame
DHT bootstrap list and bind interfacespec.network.dht (host, port, bootstrap)getKernel({ hrpc: { bootstrap, dhtHost } }), bootstrap and dhtHost on Worker boot functions and startGateway(); MDK_DHT_BOOTSTRAP and MDK_DHT_HOST for worker.jsThe private profile if outbound UDP is blockedSame
Gateway bind addressNot exposed; configure the Gateway HTTP facilitystartGateway({ httpd }) overrides or the Gateway's config/facs/httpd.config.jsonThe proxy-facing interfaceSame
Gateway allowedHostsspec.gateway.config.allowedHostsstartGateway({ allowedHosts })The names operators typeSame
Gateway trustProxyspec.gateway.config.trustProxystartGateway({ trustProxy })true only behind your proxySame
Gateway rateLimitspec.gateway.config.rateLimitstartGateway({ rateLimit })Default 300 per minute per IPLower on write routes
Gateway identityNot exposed; spec.gateway.config.authVerifier is refusedstartGateway({ kernel }) in process, or startGateway({ gatewayCredential }) against a Kernel elsewhereThe Kernel's token issuerSame
MCPspec.mcp absent, or port, allowWrite, allowToolscreateMcpServer(root, port, config, pluginDirs, apps, options)Absent, or defaultsallowWrite only for agents whose rules cover the tools
Signal handlinghandled by mdk runhandleSignals: true on getKernel, createMcpServer and a standalone startGateway (ignored with an in-process kernel); worker.js installs no handlersSet where offered, so a supervisor stop runs cleanupSame
Secrets${VAR} placeholders resolved from the environment, then .env (mode 0600)Injected by your secret managerNever in mdk.yaml, git or a contractSame

When Gateway is started with startGateway({ kernel }) using a Kernel returned by getKernel(), set handleSignals: true on getKernel() so Kernel's cleanup also stops Gateway. A custom host can instead own signal handling and shut down its components through the shutdown API. Sending a supervisor stop signal alone does not guarantee that application cleanup runs.

Next steps

On this page