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.
| Network | Component | Protocol and port | Direction |
|---|---|---|---|
| Operator | Gateway HTTP | TCP spec.gateway.port, behind the TLS proxy you run | Inbound from operators and the UI origin only |
| Operator | Operator shell (UI) | Static files served by that same proxy | Inbound from operators |
| Backend | Kernel, Workers, app runtimes | UDP, ephemeral local ports; HyperDHT bootstrap 49737 (public set) or your private list | Peer to peer inside the backend network; outbound to the bootstrap nodes |
| Backend | MCP server | TCP 127.0.0.1:<gateway port + 100>, loopback only | Local processes on the MCP host |
| Backend | Agent model provider | TCP to qvac or your OpenAI-compatible URL | Outbound from the agent host only |
| Devices | Avalon (CGMiner) | TCP 4028, no authentication | From the owning Worker only |
| Devices | Whatsminer | Encrypted TCP 4028 (API v2) or 4433 (v3), session token from the API password | From the owning Worker only |
| Devices | Antminer HTTP | TCP 80, digest authentication | From the owning Worker only |
| Devices | Containers, meters, sensors | Vendor API port, vendor authentication | From the owning Worker only |
| Devices | Pool APIs | HTTPS outbound | From 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.rbacrule on every Kernel request and every Gateway app route, default deny (policy model) - Hold an app to the
app-<name>rolespec.rbacbinds to its service accountapp:<name>, seeded at install from the app'smdk-app.yamlpermissions, and check the signed-in user's own rules as well when the app acts for one (app runtime) - Record every
command.requestattempt, 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
Hostnames, 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.jsorspec.network.dhtinmdk.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
Hostallowlist 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 tospec.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: listand the read verbs of the app routes operators use; none carriesdevices: write,actions: writeoractions: vote - Replace every wildcard administrator grant, the CLI account's included, with a dedicated policy-maintenance role holding
policy: read,policy: writeand the read permissions its callers need, bound only to whoever maintainsmdk.yaml. Do not bind theadminrole in this profile: it holds every resource and verb,devices: writeandactions: writeamong them, so binding it breaks the guarantee above.mdk rbac initbindsmdk-clitoadminby default, so rebind that account to the narrower role before relying on this profile. - Each app's
app-<name>role inspec.rbaccarries no write verb, so the app cannot reachcommand.requesteven when a user asks it to. The role is seeded from the app'smdk-app.yamlpermissionsat install and is yours from then on, so audit the role rather than the manifest. spec.appslists 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
allowWriteorallowTools - The approval flow is never reached: with nobody holding
actions: write,action.pushis 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: writeon the devices each team owns, scoped withresourceNamessuch asminer/antminer/*orminer/antminer/rack-3/*, andactions: writeandactions: voteon the people who push and approve. A device name isfamily/brand/worker/deviceand a pattern is anchored at both ends, so a pattern that does not start at the family segment matches nothing and denies. - Bindings name
Groupsubjects 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 asreq._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-assertedauthPermsare ignored for any caller that carries a token - Power mode, pool assignment, container PDU and bulk reboot go through
action.pushwith the vote threshold the site owner signed off; single-device, operator-in-front actions may use a directcommand.request - MCP write tools stay hidden unless
spec.mcp.allowWriteorspec.mcp.allowToolsnames 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: trueorspec.mcp.allowToolslists them - Every
/mcpcall carries one agent's own access token:mdk sa add <agent>creates the credential, and the agent signs in at the Gateway'sPOST /auth/token.spec.mcp.tokenandspec.mcp.identityare refused inmdk.yaml - The agent's rules decide which app routes are listed as tools at all, and a route that declares a
resourceNameis checked again on each call - Requests whose
Hostis not a loopback name, or that carry an unlistedOrigin, get403
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.
| Setting | mdk.yaml | Programmatic | Profile A | Profile B |
|---|---|---|---|---|
| Access policy | spec.rbac, written by mdk rbac init, mdk user add and mdk sa add, applied on mdk rbac apply | getKernel({ rbac }) | Read verbs, plus policy: write for policy maintenance; no wildcard grants | Adds devices: write, actions: write and actions: vote on named roles |
| Kernel caller allowlist | Not exposed; trusted-directory callers are admitted | getKernel({ hrpc: { whitelist }, trustedKeysDir: false }); explicitly manage later admissions | Every backend caller key, and no roles | Same |
| Worker admission to Kernel | Not exposed; CLI uses local discovery | getKernel({ discovery: { workerKeys } }), kernel.allowWorkerKey() for topic discovery | Every topic-discovered Worker key, pinned | Same |
allowAnyWorker | never | discovery.allowAnyWorker | false | false |
| Worker caller allowlist | set by mdk run from the trusted-keys dir | boot function allowedKeys, MDK_WORKER_ALLOWED_KEYS for worker.js | The Kernel client key alone | Same |
| Device address allowlist | Not exposed | conf.thing.allowedAddressCidrs on Worker boot functions | The device VLAN CIDR | Same |
| DHT bootstrap list and bind interface | spec.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.js | The private profile if outbound UDP is blocked | Same |
| Gateway bind address | Not exposed; configure the Gateway HTTP facility | startGateway({ httpd }) overrides or the Gateway's config/facs/httpd.config.json | The proxy-facing interface | Same |
Gateway allowedHosts | spec.gateway.config.allowedHosts | startGateway({ allowedHosts }) | The names operators type | Same |
Gateway trustProxy | spec.gateway.config.trustProxy | startGateway({ trustProxy }) | true only behind your proxy | Same |
Gateway rateLimit | spec.gateway.config.rateLimit | startGateway({ rateLimit }) | Default 300 per minute per IP | Lower on write routes |
| Gateway identity | Not exposed; spec.gateway.config.authVerifier is refused | startGateway({ kernel }) in process, or startGateway({ gatewayCredential }) against a Kernel elsewhere | The Kernel's token issuer | Same |
| MCP | spec.mcp absent, or port, allowWrite, allowTools | createMcpServer(root, port, config, pluginDirs, apps, options) | Absent, or defaults | allowWrite only for agents whose rules cover the tools |
| Signal handling | handled by mdk run | handleSignals: true on getKernel, createMcpServer and a standalone startGateway (ignored with an in-process kernel); worker.js installs no handlers | Set where offered, so a supervisor stop runs cleanup | Same |
| Secrets | ${VAR} placeholders resolved from the environment, then .env (mode 0600) | Injected by your secret manager | Never in mdk.yaml, git or a contract | Same |
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
- Follow the site security blueprint: the rationale and trade-offs behind every row
- Understand the locked-down site shape: why pinned keys remove the topic but not the DHT
- Compare HRPC with a known-endpoint RPC: setup, failure modes, latency and diagnosability at a managed site
- Read the signed-in user in app controllers: what
req._infoandreq.mdkcarry, and how a route's rule is chosen - Stage fleet-changing writes with the write-actions guide
- Check the environment requirements for the Node.js and npm baseline