feat(ssh): adopt the client key as a secret and declare who may reach each machine (task 0019) #9
@@ -38,14 +38,41 @@ Note that the fallback only becomes real once a second machine exists to connect
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `neogaia` has its own client SSH identity, distinct from its host keys, adopted from the key already on the machine
|
||||
- [ ] Its private half is stored in `neogaia`'s own secrets file, encrypted to the admin identity and `neogaia` alone
|
||||
- [ ] Its public half is committed in plaintext
|
||||
- [ ] The private half decrypts at activation, readable only by the primary user and not by other accounts
|
||||
- [ ] The SSH client uses the decrypted key with no hand-placed copy in the user's home directory
|
||||
- [ ] Each machine declares a role, and the keys it authorizes follow from that role rather than from a per-host list
|
||||
- [ ] Workstation keys are authorized on every machine
|
||||
- [ ] Server keys are authorized on servers only, and on no workstation
|
||||
- [ ] Registering a new machine is a role declaration in one place, requiring no edit to any other host
|
||||
- [ ] `nix flake check` builds the `neogaia` toplevel
|
||||
- [ ] Manual confirmation: the key materializes with the declared ownership and mode, an authenticated push to the remote still succeeds, and `neogaia` accepts an SSH connection offering the adopted key
|
||||
- [x] `neogaia` has its own client SSH identity, distinct from its host keys, adopted from the key already on the machine
|
||||
- [x] Its private half is stored in `neogaia`'s own secrets file, encrypted to the admin identity and `neogaia` alone
|
||||
- [x] Its public half is committed in plaintext
|
||||
- [x] The private half decrypts at activation, readable only by the primary user and not by other accounts
|
||||
- [x] The SSH client uses the decrypted key with no hand-placed copy in the user's home directory
|
||||
- [x] Each machine declares a role, and the keys it authorizes follow from that role rather than from a per-host list
|
||||
- [x] Workstation keys are authorized on every machine
|
||||
- [x] Server keys are authorized on servers only, and on no workstation
|
||||
- [x] Registering a new machine is a role declaration in one place, requiring no edit to any other host
|
||||
- [x] `nix flake check` builds the `neogaia` toplevel
|
||||
- [x] Manual confirmation: the key materializes with the declared ownership and mode, an authenticated push to the remote still succeeds, and `neogaia` accepts an SSH connection offering the adopted key
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The fleet is a plain data file, `fleet/default.nix`, mapping each machine to its role and client public key.
|
||||
The ssh module reads it, looks this machine up by hostname, and derives the authorized set, so a machine's own configuration never names another's key.
|
||||
|
||||
The role policy is one attribute set: a workstation authorizes workstation keys alone, a server authorizes both.
|
||||
Only `neogaia` exists, so the server half cannot be exercised by the real fleet.
|
||||
It was verified by temporarily adding a synthetic server and a second workstation, evaluating the authorized set with `neogaia` as a workstation and again as a server, and reverting.
|
||||
A workstation excluded the server's key; a server admitted all three.
|
||||
|
||||
Two assertions guard the derivation.
|
||||
One rejects a machine absent from the fleet or carrying a role no policy defines.
|
||||
The other rejects any fleet entry whose role is undefined, because such an entry matches no policy and would lose its access everywhere without failing anything.
|
||||
Both were confirmed to fire.
|
||||
|
||||
Home-manager's `matchBlocks` is deprecated in favour of `settings`, so the client uses the latter.
|
||||
`enableDefaultConfig = false` drops home-manager's own default directives, leaving the generated `~/.ssh/config` at two lines and every other directive at the value OpenSSH itself ships.
|
||||
|
||||
Manual confirmation was performed after a `nixos-rebuild switch`.
|
||||
The secret materialized as `-r--------` owned by the primary user, and the public half derived from it matches the committed fleet entry.
|
||||
The hand-placed `~/.ssh/id_ed25519` was moved aside for the test, so both directions were exercised against the decrypted secret alone: `ssh -v` to the remote reported `Server accepts key: /run/secrets/ssh-user-ed25519-key`, and an inbound connection to `neogaia` authenticated and returned a shell.
|
||||
|
||||
One follow-up is outstanding.
|
||||
Deleting the now-redundant `~/.ssh/id_ed25519` and its public half was refused by the agent's permission layer, so both files remain on the machine.
|
||||
They are superseded rather than needed: the same key is in `secrets/neogaia.yaml`, and the client is pointed at the decrypted path.
|
||||
Removing them is a one-line manual step, and the key is recoverable from the secrets file if it is ever wanted back.
|
||||
|
||||
8
fleet/default.nix
Normal file
8
fleet/default.nix
Normal file
@@ -0,0 +1,8 @@
|
||||
# Every machine, its role, and the public half of the client key it
|
||||
# authenticates with.
|
||||
{
|
||||
neogaia = {
|
||||
role = "workstation";
|
||||
sshPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE contact@alexion.dev";
|
||||
};
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
# publishing them is their purpose.
|
||||
modules.ssh.enable = true;
|
||||
modules.ssh.hostKeys.sopsFile = ../../secrets/neogaia.yaml;
|
||||
modules.ssh.userKey.sopsFile = ../../secrets/neogaia.yaml;
|
||||
|
||||
# fish as the login shell.
|
||||
modules.fish.enable = true;
|
||||
|
||||
@@ -3,11 +3,40 @@
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
# The OpenSSH daemon, serving host keys restored from secrets.
|
||||
# SSH on this machine, in both directions.
|
||||
let
|
||||
cfg = config.modules.ssh;
|
||||
user = config.user.name;
|
||||
|
||||
secretName = type: "ssh-host-${type}-key";
|
||||
fleet = import ../fleet;
|
||||
|
||||
hostKeySecret = type: "ssh-host-${type}-key";
|
||||
userKeySecret = "ssh-user-ed25519-key";
|
||||
|
||||
# The roles whose keys a machine of the given role authorizes.
|
||||
# A workstation admits workstations alone, so a server that is compromised
|
||||
# reaches no machine of the operator's own.
|
||||
authorizedRoles = {
|
||||
workstation = [ "workstation" ];
|
||||
server = [
|
||||
"workstation"
|
||||
"server"
|
||||
];
|
||||
};
|
||||
|
||||
machine = fleet.${config.networking.hostName} or null;
|
||||
|
||||
# Guarded so that an unregistered machine fails the assertion below with a
|
||||
# readable message, rather than on a missing attribute here.
|
||||
registered = machine != null && authorizedRoles ? ${machine.role};
|
||||
|
||||
authorizedKeys = lib.optionals registered (
|
||||
lib.mapAttrsToList (_name: m: m.sshPublicKey) (
|
||||
lib.filterAttrs (_name: m: lib.elem m.role authorizedRoles.${machine.role}) fleet
|
||||
)
|
||||
);
|
||||
|
||||
undefinedRoles = lib.attrNames (lib.filterAttrs (_name: m: !(authorizedRoles ? ${m.role})) fleet);
|
||||
in
|
||||
{
|
||||
options.modules.ssh = {
|
||||
@@ -39,23 +68,78 @@ in
|
||||
already pinned makes the host unrecognisable to it.
|
||||
'';
|
||||
};
|
||||
|
||||
userKey.sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
Encrypted file holding this machine's SSH client private key, under the
|
||||
entry `ssh-user-ed25519-key`.
|
||||
|
||||
This is the key the primary user offers to authenticate to a remote
|
||||
server, not a key the daemon presents to identify this machine.
|
||||
It belongs to this machine alone, so withdrawing its access does not
|
||||
re-key any other.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = registered;
|
||||
message = ''
|
||||
modules.ssh: ${config.networking.hostName} is not in the fleet under a
|
||||
defined role, so the keys it authorizes cannot be derived.
|
||||
'';
|
||||
}
|
||||
{
|
||||
# An undefined role matches no policy, which would drop that machine's
|
||||
# access everywhere without failing anything.
|
||||
assertion = undefinedRoles == [ ];
|
||||
message = ''
|
||||
modules.ssh: fleet entries carry a role no policy defines: ${lib.concatStringsSep ", " undefinedRoles}.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
services.openssh.enable = true;
|
||||
|
||||
# The daemon reads its host keys once at startup, so a re-key has to restart
|
||||
# it to take effect.
|
||||
sops.secrets = lib.genAttrs (map secretName cfg.hostKeys.types) (_: {
|
||||
sops.secrets =
|
||||
# The daemon reads its host keys once at startup, so a re-key has to
|
||||
# restart it to take effect.
|
||||
lib.genAttrs (map hostKeySecret cfg.hostKeys.types) (_: {
|
||||
inherit (cfg.hostKeys) sopsFile;
|
||||
mode = "0400";
|
||||
restartUnits = [ "sshd.service" ];
|
||||
});
|
||||
})
|
||||
// {
|
||||
# The primary user is the only account that authenticates with this key,
|
||||
# and the mode admits no other.
|
||||
# The client rereads it per connection, so no unit restarts on a re-key.
|
||||
${userKeySecret} = {
|
||||
inherit (cfg.userKey) sopsFile;
|
||||
mode = "0400";
|
||||
owner = user;
|
||||
};
|
||||
};
|
||||
|
||||
# An empty list is what stops the daemon generating keys of its own.
|
||||
services.openssh.hostKeys = [ ];
|
||||
services.openssh.extraConfig = lib.concatMapStrings (
|
||||
type: "HostKey ${config.sops.secrets.${secretName type}.path}\n"
|
||||
type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n"
|
||||
) cfg.hostKeys.types;
|
||||
|
||||
# The primary user is the only account reachable over SSH.
|
||||
users.users.${user}.openssh.authorizedKeys.keys = authorizedKeys;
|
||||
|
||||
# The client reads the decrypted key where it is written, so no copy of it
|
||||
# lives in the user's home to drift from the secret.
|
||||
# Declaring no defaults of home-manager's own leaves every other directive
|
||||
# at the one OpenSSH itself ships.
|
||||
home-manager.users.${user}.programs.ssh = {
|
||||
enable = true;
|
||||
enableDefaultConfig = false;
|
||||
settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
ssh-host-ed25519-key: ENC[AES256_GCM,data:K6F9JrmmnK0EKqe9wsjGKOM01ygHvKRUj/a65IdWQsnR8MHNKR90xxviwZa8BdJOjhfOnZYua66h/folPYFT8jiR4HRXRTcZ25LNrGK+oOoxnvN2ES/9X0NDRhTbfxRUfBYb6gf38cyJUoCxoVps+NQ19eCO+kbj/4vfrYyiiZRdL6Bf7xMrftzkMZ4Fxr2xK0NmARvh39Q8tYWJX+57H0bNsRdTQkZkRsLl8l8kQyD8U1g5OsMkNCYZchHZyp6YIzaSoZyAZRBGNxKJmSwzC7ewP/JA7S5nqErtrsZIlS4s27yn1ERT7VfJwhx4xGZgnCF5HQDz/eW6/EhdF8MOTA2HkZqz8u0QpamtgGi0X/K/Ip+ntYIyAa4VtmKcgghPrftZ4xc0TeeOVPU1KgbW7HBnqeDiNQd5k/878FDmsoKWt3CrdL1RszeUCJH1DY+kN44cMlbKwo8P3vp7oNTV1Sn5NUuWv6JxCYX4HTYlW92Wt//mqjtX2gWrMB0R63aPlJNmcIVjDN2mNGXn/Uyn,iv:q6chCkDdccg6pyH7fr8yMvNDcGjl5ZhO4CN25M7ydkk=,tag:JSuTMuG+0fSEjvG8uLG56A==,type:str]
|
||||
ssh-host-rsa-key: ENC[AES256_GCM,data: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,iv:xbUR7YIZKQd6qlzsEZT4aur5xZuRBgQvmSSTdpc05ag=,tag:UIuoHsgMYWmhdJR/GDB0iA==,type:str]
|
||||
ssh-user-ed25519-key: ENC[AES256_GCM,data:p8HGPQrlLLj57Zlkbe/g4HzMRnEucrrl/Aeh/iG/3nSM8I73cCEC8lZXcQgU9BtBP3rrY5F0BvT8MKVlkgCVqJ63EVPj/xjZqmmyKQvf9VH7tx8IoqAA3K/eSt9F7HhUPR3f2NQtce4xeOf7qYHFvP1LNEtv0UgmgROhUfYPaHb7JZ3p4kZ8V8LhVttmIJi9k45P182TtZJkgt36Ys6gyBp7A6UOT7Ha/HdhB6fkqt3H5HWqSq786NXUhqXgzPSDFvH4Iy+aBczAehjYRVNPJ8LTnDovmr8fMzuTJ1t8WCEnljkDWen645ToCObfJTmCIphGfD/VkST0ZphvmV8MY4/z+Ch+CUxI2O2XgJYQVsD7qBtKKxlAS4P4U28JgLt01Y4tPK6Qs+VTXEsBqKxYS99AUVjNAfxE2d7c0IpQSNHqm8mw0d41OKiphIAxgJ14RtUQDxsytAlEl8giQKeJ3yFf8xVYKJ0NTvKyf2X0+d7k3ZiEP0FGbjBwA484kCUzfYuTX+bRoKLd7EcaJkdHSuqQprzUfYpAGgS/,iv:f8YPwuXpP94FMhCIgkLpR1PVufl3PPQVnNaEn56WXPE=,tag:8YnJjtOCEleEZdKQu0bBfA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
@@ -20,7 +21,7 @@ sops:
|
||||
cbQ5nwrYjI7FapucJQQFebB7FrLKCDgZRyw+/nhCmEJSWFz7iZKLQA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14a04vphzjq74epfrz9a09wjw8lzchtru84awzuq2n45d8f42ychqjs89qe
|
||||
lastmodified: "2026-07-20T03:45:30Z"
|
||||
mac: ENC[AES256_GCM,data:ThhDV+a054Z32fFkgkJ40I/Wr9EY9gMa/Wc8uToMaqGGdMT87xJH1QEJxdKHuzgWAXFLH9+4Rl/t2uRFC6AVyOGyBou/NDy0G3WzIfrJn1raTYwIvNm4I/0vycRvLyOi4ERPpDERqgoDczeE9i7U7Qves/KdLFPqOlGHDBI/QWE=,iv:oBNu3jeM7S41+mhKYzpYUzyynrJ8hQ7uA8Msd0UrIcA=,tag:HWV32TmxfKZlLcDWo4iTiA==,type:str]
|
||||
lastmodified: "2026-07-20T16:29:45Z"
|
||||
mac: ENC[AES256_GCM,data:XLrro73Z4hA4ntA2uFlBj+eNJJXlYw3YUFFLI/it9KvvEbxlvAVka++HQpby2rRZm+VjSXa7BDtzSnLA4Jpue2csUQUMVpKCGvWBNxTBnZpkKpi7fJd2AOFcMA+mXk19OS9YdnfZy9VLpr4+XiM8e+zxNEijZmHXn0gsGUotd8I=,iv:6FWSG86Ix764vD/BMlnEWDdQX74yIg0wkmz5Zz343VQ=,tag:tIeODybScPRVuLBpj7gzVw==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
|
||||
Reference in New Issue
Block a user