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

MDK ships no user identity at any tier. Every control on this page admits or scopes a backend process by its key, or a network by its reach. The person or agent behind a request is identified only by the authVerifier you give the Gateway.

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 and scope each one to read, write or admin (Kernel roles)
  • 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 a bearer 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 (programmatic hosts pass the list; mdk run uses the public DHT)

MDK cannot:

  • Identify a person or an agent. Only the Gateway authVerifier you write does that, and nothing below the Gateway sees it.
  • 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 controls in Profile A, not one flag.
  • Enforce an app's declared permissions. An app runtime key is a full Kernel principal; review an app before adding it to spec.apps (app runtime).

Profile A: monitoring-only

The site reads telemetry and state. No exposed route, tool or Kernel caller can send a command to a device.

  • Kernel admits the Gateway, the MCP server and each app runtime key with roles: ['read']; no entry carries write or admin
  • Every Worker admits the Kernel client key (kernel.getClientPublicKey()) and the Gateway key, the two callers that dial Workers directly; the Worker allowlist carries no roles, so Kernel's read scope does not bind the Gateway's direct calls
  • 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 no write caller, action.push is refused at the transport

A read caller that sends command.request or action.push through Kernel gets ERR_KERNEL_ACTION_FORBIDDEN. The Gateway's direct Worker path carries no roles, so monitoring-only also rests on spec.apps exposing no write route and on the Gateway host staying yours.

Profile B: delegated writes

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

  • Kernel admits the Gateway with roles: ['write'], the MCP server with roles: ['read'] unless agents may act, and each app runtime key with the narrowest role its routes need
  • Workers admit the Kernel client key and the Gateway key, as in Profile A
  • The Gateway runs an authVerifier backed by your identity provider, marks every route that reads or changes the fleet "auth": true, and maps verified roles onto authPerms (miner:w, container:w) and voter; it never accepts either from the HTTP client (auth in app controllers)
  • 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 behind a controller that checked the token
  • MCP write tools stay hidden unless spec.mcp.allowWrite or spec.mcp.allowTools names them, and the MCP server then carries an identity whose perms match the routes it may call, behind an authenticated front door

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
  • spec.mcp.token (use a ${MDK_MCP_TOKEN} placeholder resolved from the environment or .env) requires Authorization: Bearer on every /mcp call
  • spec.mcp.identity supplies the perms that "auth": true routes need; without it those routes are not exposed as tools
  • 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 receives a real userId from the Gateway identity, 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 Kernel caller roles, pinned Worker admission, or the Gateway bind address as stack settings. It admits callers from the trusted-keys directory with full Kernel access, so it does not establish the restricted roles in these profiles. A programmatic host that assigns caller roles must disable automatic directory admission with trustedKeysDir: false and explicitly manage caller admission. 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
Kernel caller allowlist and rolesNot exposed; trusted-directory callers receive full accessgetKernel({ hrpc: { whitelist }, trustedKeysDir: false }); explicitly manage later admissionsEvery entry roles: ['read']Gateway ['write'], others as narrow as their routes allow
Kernel allowAllneverhrpc.allowAllfalsefalse
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.jsKernel client key plus the Gateway keySame
Device address allowlistNot exposedconf.thing.allowedAddressCidrs on Worker boot functionsThe device VLAN CIDRSame
DHT bootstrap listNot exposed; mdk run uses the public DHTgetKernel({ hrpc: { bootstrap } }), bootstrap on Worker boot functions and startGateway(); no worker.js overridePrivate list 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 authVerifierspec.gateway.config.authVerifier names a built-in; a function needs codestartGateway({ authVerifier })Your verifier; 'loopback' reads the socket address, so never behind a proxyYour verifier, required
MCPspec.mcp absent, or port, allowWrite, allowTools, token, identitycreateMcpServer(root, port, config, pluginDirs, apps, options)Absent, or defaultstoken set, allowWrite only with an authenticated front door
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