docs: plan sops secrets with two-tier age identities

Supersede ADR 0001's SSH-derived key mechanism with an admin identity held
outside the repo plus a per-host identity on each encrypted root. Decoupling
the two is what lets the SSH host keys become secrets themselves rather than
the root of trust they were.

Add the spec, the three implementing tasks, and the glossary terms the
breakdown speaks in.
This commit is contained in:
2026-07-19 14:57:41 -04:00
parent 7cd9370366
commit 5e254857b9
7 changed files with 290 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
---
spec: sops-secrets
---
## What to build
The tracer bullet for encrypted secrets: a working two-tier age identity model, with the primary user's login password arriving as a decrypted secret rather than a value typed into a running machine.
Establish the two identities the model rests on.
An admin identity is generated and stored as a secure note in the operator's password manager; only its public recipient ever appears in the repo, and no copy of the private half is committed in any form.
A host identity is generated on `neogaia` itself, onto its encrypted root, never transmitted, and deliberately not derived from the machine's SSH host key.
Commit a sops configuration naming both recipients and a shared secrets file encrypted to admin plus `neogaia`, holding the primary user's password hash.
The hash lives in the shared file rather than a per-host one because the same password is used on every machine, so per-host copies would only make rotation a multi-file edit.
Wire the tooling into the flake as unconditional plumbing in the shared base config — not behind an enable flag, on the same grounds as the overlays and the flakes settings.
The base config carries only the machinery: the flake input, the identity file location, and the default secrets file.
The password secret itself is declared beside the user declaration it feeds, so a reader finds the secret where they find its use.
The password secret must be marked as needed for user creation, which decrypts it in an earlier activation stage than ordinary secrets.
That ordering is why the host identity has to sit on the root filesystem rather than anywhere mounted later.
The transition is safe on `neogaia`: if activation fails the rebuild fails and the running generation persists with its existing hand-set password intact.
## Acceptance criteria
- [ ] An admin age identity exists in the operator's password manager; its private half is committed nowhere, in no form
- [ ] A host age identity exists on `neogaia`'s encrypted root and was generated on the machine
- [ ] The sops configuration in the repo names the admin recipient and the `neogaia` recipient
- [ ] A shared secrets file, encrypted to admin plus `neogaia`, holds the primary user's password hash
- [ ] The secrets flake input is added, following the base nixpkgs
- [ ] The shared base config carries the machinery unconditionally — identity file location and default secrets file — with no enable flag
- [ ] The password secret is declared beside the user declaration, consumed through `hashedPasswordFile`, and marked as needed for user creation
- [ ] `nix flake check` builds the `neogaia` toplevel; a mistyped secret name or missing secrets file fails it
- [ ] Manual confirmation: `neogaia` activates, and console login succeeds against the decrypted password hash

View File

@@ -0,0 +1,27 @@
---
spec: sops-secrets
blocked-by: 0010-sops-skeleton-and-password
---
## What to build
`neogaia`'s SSH host keys become secrets, so reimaging the laptop no longer invalidates its host identity or breaks `known_hosts` for every client that has ever connected to it.
Introduce a per-host secrets file for `neogaia`, encrypted to the admin identity plus `neogaia` alone — the first file in the repo that is not readable by the whole fleet, and the thing that keeps a compromised machine from decrypting another's material.
The host's SSH **private** keys go in it.
The host **public** keys are committed in plaintext.
Publishing them is their function, and encrypting them would impose a re-key cycle every time one changes.
Stop the SSH daemon generating its own host keys and point it at the decrypted paths instead.
These secrets decrypt in the ordinary activation stage rather than the early pre-user one, so this slice exercises the second of the two decryption paths.
## Acceptance criteria
- [ ] A secrets file for `neogaia` exists, encrypted to the admin identity and `neogaia` only — not to any other recipient
- [ ] `neogaia`'s SSH host private keys are stored in it
- [ ] The corresponding host public keys are committed in plaintext
- [ ] The SSH daemon no longer generates its own host keys and reads the decrypted paths
- [ ] The host key secrets are declared beside the SSH daemon configuration that consumes them
- [ ] `nix flake check` builds the `neogaia` toplevel
- [ ] Manual confirmation: after activation the secrets materialize with the declared ownership and mode, the daemon adopts the restored keys, and the host fingerprint presented to a client is unchanged

View File

@@ -0,0 +1,37 @@
---
spec: sops-secrets
blocked-by: [0010-sops-skeleton-and-password, 0011-neogaia-ssh-host-keys]
---
## What to build
A thorough revision of the install document, not an appendix to it.
The existing procedure is built around a login password set by hand through `nixos-enter` after the install and never committed.
That step no longer exists, so the parts of the document that describe it are wrong rather than merely incomplete: the framing that names two hand-entered secrets, the step that sets the bootstrap password, the reboot step's instruction to log in with it, and the closing follow-up section — which additionally describes the superseded key-derivation mechanism as the reason sops wiring cannot happen during an install.
The LUKS passphrase remains the one secret genuinely entered by hand, and the revised document should say so plainly.
The install ordering inverts.
A host's identity is now provisioned and registered *before* its first boot, because the login password arrives only from a decrypted secret and there is no fallback credential — a first boot without a registered identity has no way in.
The document should carry that as the reason, since it is the whole point of the reordering.
Cover four procedures:
- Provisioning a host that is already installed and running, done live on the machine: generate the identity, add its recipient, re-key the affected files with the admin identity, rebuild. No reimage, no live ISO.
- Provisioning a host that does not yet exist, done on the live ISO before the install: generate the identity, add its recipient, re-key, write the identity onto the target root, then install. The install builds from a local clone, so no push is required mid-procedure; the recipient change is committed afterward.
- The editing workflow: which secrets the workstation can change on its own, and which require unlocking the admin identity for the session. That friction is intended, not an oversight.
- Recovery from a live ISO for a machine whose identity was provisioned wrongly, so a failed first boot is a known procedure rather than an improvised one.
Everything goes in the existing install document; no new document is introduced.
## Acceptance criteria
- [ ] No step remains that sets a login password by hand, and nothing instructs the operator to log in with one
- [ ] The document's framing names the LUKS passphrase as the only hand-entered secret
- [ ] The install ordering places identity provisioning before first boot, and states why there is no fallback credential
- [ ] The closing section no longer describes deriving identities from SSH host keys or defers sops wiring to a post-boot follow-up
- [ ] Live provisioning for an already-running host is documented
- [ ] Pre-install provisioning on the live ISO for a not-yet-existing host is documented, including that the recipient change is committed after the install
- [ ] The editing workflow is documented, distinguishing what the workstation can re-key alone from what needs the admin identity
- [ ] The live-ISO recovery path for a wrongly-provisioned machine is documented
- [ ] The document reads end to end as one coherent procedure for a reader who has never seen the previous version