feat(ssh): adopt the client key as a secret and declare who may reach each machine (task 0019) #9
Reference in New Issue
Block a user
Delete Branch "task-0019-user-ssh-keys-and-access-policy"
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/0019-user-ssh-keys-and-access-policy.mdMakes the operator's SSH client key a secret and declares which machines may reach which.
The key that authenticates pushes to the remote existed only as a file created by hand on one laptop, so a reimage would have destroyed it and locked the operator out. It now lives in
secrets/neogaia.yaml, encrypted to the admin identity andneogaiaalone, decrypting at activation as0400owned by the primary user. The client is pointed at the decrypted path, so there is no second copy to drift.Access is three options on the ssh module: the client keys of the machines the operator works from, the client keys of the machines that serve, and the set this machine admits. A host declares what it draws from in its own file:
A server draws on both, so servers reach each other while a compromised one reaches no machine the operator works from.
Deviations
fleet/default.nixmapping each machine to a role and a key, which the module looked up by hostname, and then as two lists behind a role enum. Both were more machinery than the problem has: authorizing a key needs the key text and nothing else, so the per-machine names, the hostname lookup and the role only reconstructed a grouping the two lists already are. A host now states what it admits rather than a category something else maps to keys. This also removed a single-file directory from the repository root.authorizedKeysdefaults to the workstation keys. AnmkOptionof a list type is not mandatory the way a scalar one is — undeclared, it yields[ ]rather than failing, and a machine admitting no key is unreachable over SSH. The default makes the safe case the silent one. Recorded inCLAUDE.md, since the empty value is dangerous here specifically.alexion@neogaiarather than the adopted key'scontact@alexion.dev, so the list says which machine each key belongs to. An authorized-keys comment is free text and independent of the private key.matchBlocksis deprecated in this pin, so the client usessettings.enableDefaultConfig = falsedrops home-manager's own default directives, leaving the generated~/.ssh/configat two lines and everything else at OpenSSH's own defaults.neogaiaexists, so the server half has nothing to act on. Verified by temporarily adding a synthetic server key and declaring both lists on the host, then reverting: drawing on the workstation keys alone excluded the server key, drawing on both admitted it, and omitting the declaration fell back to the workstation keys rather than to none.Manual confirmation ran against the deployed machine after
nixos-rebuild switch, and the reviewed tree builds bit-for-bit to the running system. The hand-placed key was moved aside for the test so both directions were exercised against the secret alone:ssh -vto the remote reportedServer accepts key: /run/secrets/ssh-user-ed25519-key, and an inbound connection toneogaiaauthenticated and returned a shell.Unrelated tidy-up
The last commit flattens
lib/default.nixandsystem/default.nixtolib.nixandsystem.nix. Each directory held one file, so it added a level to descend without grouping anything, and removing this task's ownfleet/left these as the last of that shape. The shared base config's path to the shared secrets file was relative to the directory it sat in, so it moved with the file. Verified to build the same system toplevel as before the move.Review
Overall: HIGH
git revert, but a bad authorized-keys set on a live machine needs console rollback rather than a push.nix flake checkonly builds the toplevel; nothing in-repo asserts the admitted set or the secret's mode.The review ran against the earlier role-based design. Its two unaddressed findings are resolved or moot:
~/.ssh/id_ed25519is still on the machine. Still outstanding: deleting it was refused by the agent's permission layer. It is redundant rather than needed, since the same key is insecrets/neogaia.yamland the client is pointed at the decrypted path, and it is recoverable from that file if ever wanted back. Removing the two files is a manual one-liner.— Claude
feat(ssh): adopt the client key as a secret and derive access from roles (task 0019)to feat(ssh): adopt the client key as a secret and declare who may reach each machine (task 0019)9a860d7715to78081143cf