refactor(ssh): extract the host key wiring into a module
The sops secret declarations, the empty `hostKeys`, and the `HostKey` lines were boilerplate sitting in the host, and every future host restoring its identity would have repeated them. `modules.ssh` takes the encrypted file and the key types, deriving the secret names and the `HostKey` lines from one list, so the daemon and the secrets cannot disagree about where a key lives. The resulting system derivation is unchanged. Also record two gotchas: reading PR review comments needs the Gitea API rather than tea, and host keys are not user authentication keys.
This commit is contained in:
61
modules/ssh/ssh.nix
Normal file
61
modules/ssh/ssh.nix
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
# The OpenSSH daemon, serving host keys restored from secrets.
|
||||
let
|
||||
cfg = config.modules.ssh;
|
||||
|
||||
secretName = type: "ssh-host-${type}-key";
|
||||
in
|
||||
{
|
||||
options.modules.ssh = {
|
||||
enable = lib.mkEnableOption "the OpenSSH daemon, with host keys restored from secrets";
|
||||
|
||||
hostKeys.sopsFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = ''
|
||||
Encrypted file holding this host's SSH host private keys, one entry per
|
||||
key type, named `ssh-host-<type>-key`.
|
||||
|
||||
These are the keys the daemon presents to identify itself to connecting
|
||||
clients, not keys used to authenticate anyone to a remote server.
|
||||
Restoring them from secrets rather than generating them keeps the host's
|
||||
fingerprint across a reimage, so every client's `known_hosts` entry
|
||||
stays valid.
|
||||
'';
|
||||
};
|
||||
|
||||
hostKeys.types = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [
|
||||
"ed25519"
|
||||
"rsa"
|
||||
];
|
||||
description = ''
|
||||
Key types to restore, naming both the entries read from the encrypted
|
||||
file and the algorithms the daemon offers. Dropping a type a client has
|
||||
already pinned makes the host unrecognisable to it.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
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) (_: {
|
||||
inherit (cfg.hostKeys) sopsFile;
|
||||
mode = "0400";
|
||||
restartUnits = [ "sshd.service" ];
|
||||
});
|
||||
|
||||
# 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"
|
||||
) cfg.hostKeys.types;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user