Trust model
Submitting a change to a host takes membership in two sets at once: people who can SSH to that box, and people who hold the fleet’s shared secret. Everything below follows from that sentence.
The three layers
- SSH is the only way onto a box.
golemdbinds127.0.0.1:7474by default, and every deployment golem ships keeps it there.--listenwill take any address you hand it; a routable one is a choice, not a thing that happens to you. golemctlrides the SSH connection you already have. Give it anssh://[user@]host[:port]target — or an inventory host with ansshfield — and it opens a local forward to the agent’s loopback port and speaks plain HTTP through it. Your~/.ssh/configapplies: an existingControlMastermakes the forward cost milliseconds.- A shared secret authorizes, even then.
golemdreads a token file and requiresAuthorization: Bearer <token>on every request. A malformed, non-Bearer, absent, or wrong header is one and the same401, which never says which part of the attempt was wrong.
The SSH session is the encryption and the host authentication. Golem has no TLS of its own and needs none: inside the tunnel there is no on-path attacker to defend against.
The bearer comparison folds over every byte whatever the first mismatch is, so its time depends on the token’s length but not on its bytes — a caller cannot learn the secret one character at a time from response timing. The length guard that runs first does leak the length, and that is accepted (ADR 0042): the token is 32 random bytes minted by the harness, so its length is a property of how golem mints tokens rather than of any particular secret.
Configuring it
On the agent:
golemd --host web \ --listen 127.0.0.1:7474 \ --auth-token-file /etc/golem/token[auth] token_file = "/etc/golem/token" in golemd.toml says the same
thing; the flag wins. The file is root-owned, mode 0600.
On the operator’s machine, golemctl takes the secret from the first of:
a host’s inventory token_file, GOLEM_AUTH_TOKEN, or the file named by
GOLEM_AUTH_TOKEN_FILE. A per-host token_file overrides the environment,
so one fan-out can span hosts holding different secrets.
Secrets in a manifest
A manifest is compiled once and applied to many hosts, so it gets stored,
cached, and handed to CI. Secretspec.get "DB_PASSWORD" keeps a password out
of it: emetc resolves the value through secretspec at compile time and
encrypts it to the fleet’s key, and the manifest carries ciphertext.
, env = [ env "DB_PASSWORD" (Secretspec.get "DB_PASSWORD") ]Interpolating a secret into a string does not seal the string. The value
becomes literal chunks with sealed holes, so a config file with one password
in it stays reviewable in the manifest and in golemctl plan while only the
password is opaque.
Encryption is deterministic, which is what keeps content addressing meaning
what it says: the same secret always produces the same bytes, so an unchanged
password is a Noop rather than a rebuild-triggered churn, and a rotated one
produces a new content id that re-enacts exactly the units depending on it.
A secret may only be a value. A path, a unit name, a scroll name, or a mode
is an identifier, and emetc rejects a secret reaching one rather than
silently unsealing it. It also refuses a secret in a lineInFile: that glyph
owns one line and not the file it appends to, so it can promise nothing about
who may read the result — write it with a file glyph at mode = "0600",
which owns the whole file and enforces its mode.
The fleet key rides the same channel as the bearer token: root-owned, mode
0600, named by [secrets] key_file in golemd.toml. emetc reads it from
--secret-key or GOLEM_SECRET_KEY_FILE. Rotating it means recompiling every
manifest that carries a secret — a manifest sealed to the old key is
undecodable by the new one, and golemd reports a key mismatch rather than
enacting a stale credential.
Compiling a program that uses a secret needs both provider access and the key,
so emetc is no longer hermetic for those programs. One that uses no secret
needs neither.
What golem does not do
Confidentiality, host authenticity, and reachability are the infrastructure’s, composed from mechanisms that already exist: SSH, unix domain sockets with filesystem permissions, loopback binds, network segmentation, a mesh VPN. Golem’s own abstractions may provision those — they compile to the four glyphs like anything else — but the agent itself enforces exactly one check.
Signing is not the roadmap. An HMAC or ed25519 scheme defends against an on-path attacker replaying or forging a request, and inside an SSH tunnel there is no on-path. Manifest signing would buy key distribution, a wire-format change, and a verification path to defend a boundary the transport already holds.
Content addressing is not authentication. A content id proves what a scroll is, not who sent it. A changed scroll has a changed id; that makes drift detectable, not callers accountable.
What is missing, plainly
No per-user identity. The gate is deliberately one check, so it
establishes that someone authorized did this — never who. Per-user
identity, an audit trail of who submitted, and SSO group mapping arrive
together, with an authentik-issued token replacing the shared secret. That
swap changes the middleware and nothing else: the agent still reads one
header and either answers 401 or proceeds.
The local forward port is racy, and the race is not closed. golemctl
asks the kernel for a free loopback port, closes it, and hands the number to
ssh — which binds it only after it authenticates, 100 ms to seconds on a
cold connection, while publishing that number in its world-readable /proc
command line the whole time. Three things narrow the window:
ExitOnForwardFailure=yes turns a lost bind into an exit rather than a
silent no-forward; golemctl requires the port to answer on two probes a
quarter-second apart while ssh is still alive; and it re-checks the child
once more immediately before the first request. What survives is a local
process that reads the port from /proc, binds it while ssh is still
authenticating, and then answers every probe: golemctl cannot tell it from
a working forward, and will send it the bearer token.
That window is accepted (ADR 0042), because reaching it takes a hostile account on the operator’s own machine — which has already lost the game. Closing it needs a rendezvous only the owner can open: forwarding a unix socket in an operator-only directory rather than a TCP port. That is the recorded path, not yet the code.
Rotating the secret
Rotation is rewriting one file and restarting the agents, and its footgun is
that a half-finished rotation locks you out of every host you have not
reached yet — golemctl holds the new secret, those agents still require the
old one, and they answer 401.
So rotate the whole set in one pass, and treat “some hosts return 401” as
an unfinished rotation rather than a broken host:
- Write the new secret to every agent’s token file.
- Restart every agent — the token is read once, at startup.
- Publish the new secret to the operators (or update each inventory’s
token_file).
An agent whose token file is unreadable or empty refuses to start rather than starting ungated, so a mis-provisioned rotation fails loudly instead of quietly opening a box.
See Status for the full implemented-vs-roadmap breakdown.