Skip to content

Who holds your keys

Thermite needs secrets to work: API keys for model providers, logins for subscriptions, SSH keys and kubeconfigs for the places rigs run, access to private repositories, the sign-ins of MCP servers, and tokens for people, programs and rigs. This page follows each kind of secret: where it is kept, where it travels, and who can read it.

The design follows a few rules:

  • Enter each secret once, on control. Control seals it, and never shows it again.
  • A rig gets only what it uses.
  • The code the agents run holds no key it does not need.
  • Keep a hash where a hash will do.

The words here are in the glossary.

flowchart LR
  O((Owner)) -- enters once --> C["Control<br/>(sealed store)"]
  C -- only what this rig uses,<br/>with the rig's token --> S[Supervisor]
  S -- the model route --> H["Harness<br/>(private state)"]
  S -- Git access, for one fetch --> G[(Git host)]
  H -- key in its environment --> W["Pi worker<br/>(agent-N)"]
  H -- key added by the proxy --> P[Model provider]
  H -- sign-in added by the gateway --> X[MCP server]
  C -. SSH key, kubeconfig .-> M[Machines and clusters]

Everything you add on the dashboard’s Settings › Providers page is sealed: model provider keys, subscription logins, Git access, SSH keys, kubeconfigs, RunPod keys, and MCP servers’ keys, client secrets and sign-ins. Control encrypts each one with AES-256-GCM, under a key kept in a separate file, outside its database. A copy of the database alone reveals no secret, and a sealed value that was changed fails to open. The ports, volumes and secret files reference says where each file lives.

The dashboard shows only a hint of each key: its last four characters, such as key …abcd. No page, REST route or MCP tool shows you a key again. The MCP tool providers lists what control holds without any key, token or kubeconfig, and mcp_servers lists the MCP servers with only each secret’s hint.

A rig receives a provider’s key only when you give one of its agents a model from that provider. Its supervisor fetches the rig’s model route (the provider’s address, the model and its key) from control, with the rig’s own token. It hands the route to the harness when it changes, and never logs or keeps the key. The harness keeps the route in its private state, where no agent’s or lane’s user can read it, and the isolation self-test checks that. The key is never in a command, an event, an export or a log.

Who then uses the key depends on the worker harness:

  • Pi, the built-in worker harness, runs each worker as its own user, agent-N, with the key in its environment. The lane’s commands run as another user, which can’t read that environment.
  • OpenCode v2 never gets the key. It reaches the provider through a proxy in the harness, with its trial’s token. The proxy adds the key on the way out, and the trial’s token stops working when the trial ends.
  • Codex, the Codex CLI, never gets the key either. Its configuration names one provider: the same proxy, with its trial’s token. It runs as the lane’s own user, with a home of its own and its approvals and sandbox off; the user it runs as is the boundary, as for OpenCode.
  • The planner and the model check use short-lived tokens for the same proxy, or the route itself inside the harness.
  • Advisors with a model from Edit rig get their key from control in the supervisor’s memory, each round.

Connect a model provider shows how to add one.

A subscription serves one person’s own Thermite. A Thermite that several people use takes API keys.

A Claude subscription. You get a token with claude setup-token on your own computer and paste it on Providers. Control seals it and makes no call to Anthropic with it, because Anthropic allows a subscription only in its own program, Claude Code. A rig gets the token only while one of its agents uses a Claude model. The harness keeps it with the model route in its private state, and passes it only to Claude Code. Claude Code starts with a home of its own, no setting sources and no MCP servers. An advisor on a Claude subscription runs in Claude Code with no tools at all.

A ChatGPT subscription. Control runs the sign-in with OpenAI and seals the login. It is the only one that renews it, because OpenAI replaces the refresh token at each renewal: the refresh token never leaves control. A rig gets a one-hour access token with its route, renewed while it uses it. Workers in OpenCode v2 or Codex reach OpenAI through the harness’s proxy with their trial’s token, so the hourly token itself stays in the harness, and the proxy lets it reach only OpenAI’s POST /responses and GET /models. When OpenAI answers there that the plan’s usage is full, the proxy notes it, so the agents wait even when their program keeps only the error’s message. Deleting the provider signs Thermite out at OpenAI.

Use a Claude subscription and Use a ChatGPT subscription show the steps.

These secrets let control set rigs up. They never go to a rig.

  • An SSH key for a machine is pasted, or made by control for you to add to the machine. Control uses it with the OpenSSH client in its own image, from a file that exists only for the call. You compare and trust the machine’s host key once, and control refuses a machine that later presents another one.
  • A kubeconfig for a cluster is given to kubectl the same way, in a file that exists only for the call. Control takes only a kubeconfig that holds everything inline: no login plugins and no references to other files.
  • The rig’s own tokens reach a machine inside the setup script, on standard input, never on a command line. On the machine they sit in a folder that only its user can open. On a cluster they are a Kubernetes secret in the rig’s namespace, which moving the rig onto new images leaves as it is.

A RunPod key is sealed too. Control uses it to read the account and to start and stop the test pods you ask for. It never goes to a pod. RunPod puts a key of its own in every pod’s environment; a rig in pull mode takes it out of its supervisor’s environment at start and never uses it.

A rig in pull mode holds no token for control. Its pod makes its own keys at its first start and keeps them where only the supervisor’s user can read them; control pins their public halves when the pod enrolls with its one-time code. Everything with a secret in it, such as a model route with its key or a Git access, reaches the pod sealed to the pod’s key, so RunPod’s proxy never sees it in plain text (Enrollment).

A private repository needs Git access: a deploy key that control makes (you add its public half to the repository) or an HTTPS token you paste. Both are sealed. Control hands one to a rig only for a campaign assigned to that rig whose sources name it. The supervisor asks for it at the moment it fetches, and control records each hand-over in its audit log. An SSH key goes into a file in a fresh folder that is removed after the fetch, and a token into an HTTPS header, never into the address or a command line. The host’s key is pinned the first time. The access never travels in a signed command, and the harness never sees it: it receives only a git bundle, which it checks against the signed commit.

The results repository, where rigs push their exports, uses a deploy key of its own that can write, mounted into the supervisor (settings). The supervisor also uses it to fetch a project or kit whose source names no Git access. Set up the results repository shows how. A rig without one sends its exports to control with its own token, and needs no other key.

A Hugging Face token, for gated model files, is the supervisor’s own too: a file mounted into it (THERMITE_HF_TOKEN_FILE). The supervisor sends it only to Hugging Face, and only over HTTPS, never to the file servers Hugging Face sends the download on to. It never puts it in a command, the inbox, a report or a log, so the harness and its agents never see it: they get only the downloaded file, which the harness checks against the signed SHA-256. See Model files on a rig.

An MCP server gives a campaign’s agents tools from outside Thermite, such as a knowledge base like GBrain or a code host (Give agents MCP tools). You add it once on Settings › Providers, with how it signs in: an OAuth sign-in on the server’s own page, a key in a header, a client ID and secret, or nothing. Control seals the key, the client secret and the OAuth sign-in like every other secret, and the dashboard shows only a hint, such as Authorization · key …c41b. No page, route, MCP tool, log or audit entry shows a secret again: an MCP call that takes one is logged as given (…c41b), and control takes each secret out of the error texts it shows.

  • Control opens the secret to read the server’s tools when you add it or press Read tools again, and to sign in. For OAuth it keeps the refresh token and renews the access token itself; for a client ID and secret it gets an access token from the server. A refresh token or a client secret never leaves control.
  • A rig gets a server only when a run that control assigned it names that server in its kit. Control hands it to the supervisor as the headers each call carries (the key, or an access token), on the same beat as the model route, and its audit log records each change in what a rig is handed (mcp_servers_handed, names only). The supervisor keeps the advisors’ servers in memory and calls them for the advisors. It hands the workers’ and the planner’s servers to the harness, which keeps them in memory, never on disk, and calls them for those agents.
  • No agent holds a secret. A worker or the planner calls the harness’s gateway with its trial’s token, or a token made for one planner call; an advisor in Claude Code calls a server the supervisor serves on 127.0.0.1 for that call. The gateway adds the headers and passes the server’s answer back. OpenCode’s and Claude Code’s MCP configuration name the gateway and that token, nothing else.
  • On the wire, a secret goes in a request header, never in the address: control refuses an address with a user, a password, a fragment or a secret-like query parameter, and one that is not https, except for localhost.
  • It stays with its server. Another address on another server, or another way to sign in, needs the secret again, so a sealed secret never goes to a server it was not given for. Deleting the server deletes its secrets and its sign-in.

Manage MCP servers shows the steps.

When you deploy control, you make the owner’s token on your own computer with thermite token new, and give control only its SHA-256, so control’s machine holds nothing that signs in as you. The operator, agent and reader tokens work the same way. Control keeps the tokens of rigs it enrolled, join commands and dashboard sessions as hashes too. A join command’s token works once, within an hour.

Control keeps no copy of two tokens: the supervisor’s token for its own harness, which the rig’s two containers share, and a trial’s token, which one worker holds for one trial and which stops working when the trial ends. Tokens and roles lists every token and what it may do.

Inside the harness, the broker reads the rig’s secret files when it starts. It hands their values to the harness’s controller alone, closes their folders, and refuses to start if a secret file can still be read by other users (Isolation inside a rig).

  • Control’s signing key signs every command. It is in control’s own folder, readable by control alone. Rigs get only its public half.
  • A rig’s signing key signs its reports. It is in the harness’s private state, and it survives a full reset.

Trust between control and rigs explains what each key proves.

Advisors you configure in the supervisor’s advisors file read their API keys from the files it names, at each call, so a new key takes effect without a restart. A role you give a model on Edit rig instead keeps its key or token sealed in control, which hands it to the supervisor in memory. The advisors file reference lists every field.