feat(neogaia): restore the SSH host keys from sops (task 0011) #4
Reference in New Issue
Block a user
Delete Branch "task-0011-neogaia-ssh-host-keys"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Task:
.claude/tasks/0011-neogaia-ssh-host-keys.mdSummary
neogaia's SSH host keys become secrets, so reimaging the laptop keeps its host identity and every client'sknown_hostsentry stays valid.secrets/neogaia.yaml— the first per-host secrets file, encrypted to the admin identity andneogaiaalone, so a compromised machine cannot decrypt another's material. Holds both host private keys (ed25519 and RSA).hosts/neogaia/.services.openssh.hostKeys = [ ]stops the daemon generating its own;HostKeypoints at the decrypted paths. These are ordinary secrets rather than the pre-user ones the password uses, exercising the second decryption path.Deviations from the plan
/run/secrets/paths. The first attempt setpathto/etc/ssh/ssh_host_*_key, which has sops plant a symlink inside a directory NixOS manages viasetup-etc. It bought nothing and added an activation-ordering dependency nothing pins.HostKeynow interpolatesconfig.sops.secrets.<name>.path, so daemon and secret cannot disagree.restartUnits = [ "sshd.service" ]added, not in the plan.sshdreads host keys once at startup, so without it a re-key would rewrite the files while the daemon kept serving the old ones from memory — silent identity drift, exactly what this task prevents..pubfiles have no consumer yet. An intermediate version deployed them viaenvironment.etc; dropped as unasked-for scope, sincesshdderives the public half from the private key.Verified:
nix flake checkpasses. After activation the leftover symlinks were removed andsshdrestarted with no key material anywhere in/etc/ssh— it came back active presenting fingerprints identical to the pre-change ones. The generatedsshd-keygen.servicehas noExecStartat all.Review
Risk
Overall: HIGH
.sops.yamlrule, but it rewires/etc/sshand every client that has this host inknown_hosts.nix flake checkproves the config evaluates, not that sops decrypts at activation or that the.pubfiles match the encrypted private halves.0400), sops key-group access control, and remote-login identity.services.openssh.hostKeys = [ ]removes the daemon's self-healing fallback, so any decryption or ordering failure leaves sshd with no host key at all.Both of the reviewer's pre-merge conditions were then discharged: the committed
.pubfiles were confirmed to derive from the encrypted private halves (decrypted with the host identity and diffed against the live keys), and the activation ordering was exercised by restartingsshdagainst an empty/etc/ssh.Unaddressed findings
Standards — duplicated
key_groupsin.sops.yaml(judgement call). The new per-host rule's recipient list is byte-identical to the shared rule's. Left deliberately: the two are semantically different —shared.yamlmust gain every future host,neogaia.yamlmust not — so merging them would be the wrong edit.Spec — task 0010's
authorizedKeyshandoff is left open. 0010 proposed settling it here, since SSH is not a recovery route while no key is authorized. It is not an acceptance criterion of this task, and choosing which public key to trust is the operator's call rather than one to infer. It wants its own task, and remains a real gap: a decryption failure that locks the console still has no network fallback.@@ -4,0 +12,4 @@sopsFile = ../../secrets/neogaia.yaml;mode = "0400";restartUnits = [ "sshd.service" ];};I think this boilerplate is worth abstracting into its own ssh module.
@@ -0,0 +1 @@ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p root@neogaiaroot@neogaia? Shouldn't it be alexion@neogaia?
I also don't know the "ssh_host" prefix. This key is also used to SSH into places so it doesn't make sense to call it ssh_host. This applies to every instance where this appears.
Both comments addressed in
42ff195— one applied, one I want to push back on.1. Abstracting the boilerplate — done
modules/ssh/ssh.nixnow owns the wiring, following the enable convention. The host is down to:The module derives the secret names and the
HostKeylines from a singlehostKeys.typeslist (default[ "ed25519" "rsa" ]), so the daemon and the secrets can no longer disagree about where a key lives — that coupling was the part most worth removing, more than the line count.The refactor is behaviour-preserving in the strict sense: the system derivation still evaluates to
vcd5a1b8xy4myhnsbm3f2p29idlanklj, byte-identical to the already-activated and verified system.2.
root@neogaiaand thessh_hostprefix — I think this is a mix-up, and I'd rather not renameIt isn't, and that's the crux. These are host keys — the server's own identity, presented to a client during the handshake so the client can tell it reached the real
neogaia. They are never offered to a remote server as credentials. The key that SSHes into places is a separate user key at~/.ssh/id_ed25519.On this machine the two are demonstrably different keys:
So
ssh_host_is precisely accurate, and it is also OpenSSH's own name for these files (/etc/ssh/ssh_host_<type>_key). Renaming them after the user key would point at the wrong concept, and the secret names, theHostKeydirectives, and the committed.pubfilenames would all drift away from every piece of SSH documentation.On
root@neogaia: that trailing field is a free-text comment, stamped in byssh-keygenat generation time from whoever ran it — here the daemon's own keygen, running as root. It carries no meaning to SSH and is not an ownership claim.alexion@neogaiawould actually be the misleading one, since the key belongs to the machine rather than to your account. I've left it as generated, but it's cosmetic and I'm happy to blank it or set it to plainneogaiaif you'd prefer it to read less like an account.Worth noting the task itself is framed this way throughout — "the host fingerprint presented to a client is unchanged" — so if you did mean something broader than host identity here, say so, because that would be a change in scope rather than a rename.
I've recorded the host-key/user-key distinction in
CLAUDE.mdso it doesn't come up again.— Claude
Flattened to
modules/ssh.nixinec40892— you're right, and the repo already says so. Every module folder here carries companion files next to its.nix(fish/config.fish,nvim/config.lua,tmux/extra.conf,claude-code/skills), whileexample.nixsits at the top level as a single file.ssh/was the only folder holding nothing but its own module, so the directory bought a level of nesting for nothing.The auto-loader collects recursively, so this is purely structural: the system derivation still evaluates to
vcd5a1b8xy4myhnsbm3f2p29idlanklj, identical to the activated and verified system.One note in case it was unintentional — the review carrying this comment (id 2336) is still in
PENDINGstate, so it is a draft only you can see. I only found it because the token reads your own pending reviews. If you meant it to be visible, it needs submitting.Separately, the user-SSH-key question that came out of this review is now written up as task 0019 in #5, with the per-machine identities and the workstation/server access policy you described.
— Claude
@@ -0,0 +1,61 @@{This is a single-file module, no need for a folder.