Skip to content

Why the daemon holds no secrets

Nerthus.Core (until cutover). This page describes the frozen system that runs today and is deleted at cutover. Replaced by: Instance secrets.

Somebody auditing this system should meet the argument before the mechanism, because the mechanism looks like an inconvenience until the argument is on the page.

The claim

nerthusd serves no secrets, holds no credential inventory, and has no route that returns either. That is not a gap waiting to be filled. Two proposals have been made and both were refused in writing, in the code they would have landed in.

Why

The server is a host, the host has an operator, and the operator is IT. Every «why can't the daemon just…» question about this surface has the same answer. A credential the daemon can read is a credential its operator can read, and a backup of that host is a backup of the credential — silently, with nothing on any screen changing. The estate's whole moderation design rests on IT being in no keyring; the credential tree rests on the same footing.

A daemon-side copy of the inventory would be a second list. sejf/inventory.yaml lives in nerthus.core.infra.common. A copy inside the daemon would diverge the first time one was written without the other — which is precisely the failure the tree exists to prevent, reproduced one layer down. The keyring work already deleted one such second list; adding another to serve a screen would undo it.

So how does anything get a secret?

Distribution is git plus local decryption. A host holds one private key, generated on the device by bootstrap.sh, which never leaves it. Everything that host needs is committed as ciphertext encrypted to that key, and the device decrypts its own material. Revocation is one deletion: a key no longer in the roster receives no future ciphertext.

The browser reads the tree from GitLab, not from the daemon. #/sejf fetches cleartext sejf/inventory.yaml over the GitLab API with the reader's own credential, and fetches the ciphertext behind a value the same way, decrypting it in the tab with the reader's own age identity. No host ever clones the secrets tree, and the token lives in one tab's memory rather than in localStorage — a GitLab token reaches every repository its holder can reach, which is a different blast radius from the daemon's own API token.

The expiry watch runs without any key at all. rotate_by is a cleartext field, so a scheduled job says which credentials lapse next month while being unable to decrypt a single one. That property is why the inventory is cleartext and it is the half of this design that pays for itself first.

What the boundary costs

It is real and it is the design's:

  • A reader loads their key again on a second device. There is no server-side session that remembers it.
  • The daemon cannot tell you whether a Worker secret is set. sejf:verify does, from CI, against the live APIs — and its answer is names and presence, never values.
  • A rotation is several acts. The holders list makes the count visible; nothing collapses it into one button, because the thing that would collapse it is a service holding every credential.

Where the reasoning lives

The rulings are in 17-sejf06-distribution.md for the distribution boundary, 08-views.md for the client surfaces — and the code repeats them at the two places somebody would otherwise add a route: Nerthus.Core/daemon/services/Vault.ps1 and Nerthus.Core.Dashboard/src/api/sejf.ts. The secret tree is the operator's side of the same thing.