{ config, lib, registry, ... }: # Configures inbound and outbound SSH access. let cfg = config.modules.network.ssh; # NOLINT(let_order) defaultAuthorizedKeys = lib.flatten ( clientKeysFor workstationNames ++ lib.optional isServer (clientKeysFor serverNames) ); clientKeysFor = names: lib.filter (key: key != null) (map (name: targets.${name}.clientKey) names); targets = lib.mapAttrs mkTarget defaultTargets; mkTarget = name: target: { hostName = target.hostName or name; user = target.user or user; port = target.port or 22; aliases = target.aliases or [ name ]; clientKey = target.clientKey or null; hostKeys = target.hostKeys or [ ]; }; user = config.user.name; defaultTargets = { gitea = { hostName = "git.alexion.dev"; port = 2022; user = "gitea"; aliases = [ "gitea" "git.alexion.dev" ]; }; neogaia = { hostName = registry.address.neogaia.self; clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxQ4kWsBo2OGYIPOkFe0vNEcB3yoJwAu0y9wrdQzALE alexion@neogaia"; hostKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJS+wp7K123+4BT6G4f954R6WyrbWveY7VlpoBUf6I5p neogaia" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCo2wWUKxyAS4J5TqbWf8glDhJvS5XmdRqFhMeJwG3pOB+4AccZ1T8LU7ZN+RjtRi3j2qXBJvIHuzhtQNtmT59TxocvfobYiqOgJpvVO5K6yD8ZoUJs6ziDkIduI9w9mdRIESoi+dBbVu8n24r61cKDVh+jWX+yjzkOcWcOzqDyQhhkjqblZ1WMAdujEMuEPvif1i2LCxStUaZqRGcx09m/ME2fYcaJrpuxxxvX2+CPJNicoo6Rx9i7ZjAoNuvH+jui4KT62DzlQtQtCl2CFUOM0gCPSa+MbNQ9elfHPvGzEcwOIMo2cuy9KURUkQu+sAgaG8S1PEniDDTecskHtuRdmPZawnQGpIhzo919Q6wUgjT8scK4mmSXRWmGmkMt0GNA2tfj5tDks6r5Q8XsYqtWs4rsOEvfmxVSdM771w+fqDBAil99Jsh0ksPK9+Bwgg8cMDzLLFDn8JA5y2G1HocMMom+u5DYKwPXEKnCILkasB8y24+O3PhSu1EuWw277w6EUEXvU03rCf0Ak/ULjxp9a00EGlloEwSmFI7Aub9XHDr87IdbGInEn+PMqyBYADiN+3h6nE2JO+nMa6i/CHdebmT+T7YJvuTKHD9sjFmQsYaghlq03DZrhHcm4hgUvE1dqGojHrhk/WgA3EWTWtK/+BP0Vy2jXaaz+qAx+EGnhQ== neogaia" ]; }; zeus = { hostName = registry.address.zeus.self; clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBcHHuubptXCdvED/p2dR3MpjGN/AimqLasLY5984G alexion@zeus"; hostKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ4GBPt966LR4LpmXgiMMkepIqNIlXTUBh4zdNeek/lN root@zeus" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3xOxGAJCG9PE0FVufgJX9VRO64IUUlRGHSOfYfAI4Mit46xKsr7z/OvqPPE9z8rOIcVPbLkc/mkMuHbDgolleunAFCIeuIIYpkL4o4/TZECh9BtN2WdPhjcD7S0rV+rR7p24Qy5bTCSwZmsQq8QxHTVPjjBngtPGRSpKVrZAuHCcdCJ13hBWAvnaav5KysKC89nMj/7FIh02v8kipGfq5LGCZ20Qp2lmv2DS24NhBPXzcw8pLN47I5GYVkZh/0RWOByteo3eXtKCF+JCwfUPCTP+zHGD+1tbO8//B2fkt3Pe9lUrn5eM2ZnX1emXSWfQng3cMqkftzWqvBwv0sSDU+9YV6H2Ury+GrrfukpBtQVe9PQcBWqbRPhxuagncR+af/tx0ZohZS0t74m3WgLtZ4r2mpTZS4oBmRsfvWzMFGSgwtva+3Y+NsLjPPM1Cz5Dn05JJlidzcABfsUg2H23vkmO2otaQI01dUni+szlRn4w6sVyxNcvY2fQGjmt9k7jmVI5D2A3yl+OLJguAAlw3ia335X1tNUG8FoI/VyZNhOckIihmbTtihNIg37Ro4yiuPRhnUWFvg2hcBCa9hzNP7XDUPnoCRQyJ4adQHQA/x4Kh+HJ6pZqtGcdqU80fzmWtrLkWG6v5/NRKs6soYDup7oQIKbJo9TtXvqtzmVmovQ== root@zeus" ]; }; pikachu = { hostName = registry.address.pikachu.self; clientKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNqJIC6VRXyvrNf3n9su9KdPCikC3CjK/QrCK2reHdB alexion@pikachu"; hostKeys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKljRf4pJO+pqEqjpPz08gOYq3g1PpxvE66xVw7uMEnA root@pikachu" "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCy/riwm7dflA3mT+3a0/2CIoS2LbAsK/vn35kOoNeuzn0yhiF+imexP6tkB3S2t+H5ybRzkbbuNZcynFfeCqthFc8kvbdCnt8Diqoeg96fZ6ecvh5QE5yH9op8534EySetZ/exakFLnF+6EiWMuWUW3DFwsc2kcgDJObqSE8gTx/d7JK953MiTFmSJBFyg1RtQ3ZnMT+iCrvY2dyCLQai7VeF8koVKF2c0leAq2Hc75rb/L9md8MoJa64iPiz7hwTCin3xoFyaY/5hNVvyqFd5PivgR69gLdJkuVsUYO2mJzhur8cYmJD+pGjJ0U45hyE9TMrCFjeJHHuvSt3+2kph62wv95jLNk0WmMlwgyunISxENCSVVtNYdBMXhUh8VhEAW17QpVUg9EnPvxOdTKEjrvfOZYASWUa51JKbgBgexVgFbxdjDZR88DZa31AVBts/cx/59gXTUahFXMYLdZgssx+5uibZQWnvCyfUV9WLbfmK1lgL6hzReg1VkQ87iGr6skjtQYemJxRaFNA1+Q5f3kmG3KncuK/594a3qXYP4gC6A2blf8om1YZ4aXXh6f+GFKLjoEw1vvM2rJ+rjzfymwDX+pxVQ9L13OEtVZc9Ez76pOkbm1hqdbL0gY45+0cpxodhWV0wMQJBDXL1MHP8qcs+/vw0GxVK5l1SnWBGlw== root@pikachu" ]; }; }; workstationNames = [ "neogaia" "zeus" ]; isServer = lib.elem currentHost serverNames; currentHost = config.networking.hostName; serverNames = [ "pikachu" ]; knownHosts = lib.listToAttrs ( lib.concatLists ( lib.mapAttrsToList ( targetName: target: lib.imap0 ( i: key: lib.nameValuePair "${targetName}-${toString i}" { hostNames = knownHostNamesFor target; publicKey = keyWithoutComment key; } ) target.hostKeys ) targets ) ); knownHostNamesFor = target: let withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}"; names = lib.unique (target.aliases ++ [ target.hostName ]); in map withPort names; keyWithoutComment = key: lib.concatStringsSep " " (lib.take 2 (lib.splitString " " key)); sshSettingsFor = name: let target = targets.${name}; in { header = "Host ${lib.concatStringsSep " " target.aliases}"; HostName = target.hostName; User = target.user; } // lib.optionalAttrs (target.port != 22) { Port = target.port; }; outboundTargetNames = let groupTargets = if isServer then [ "gitea" ] ++ serverNames else if isWorkstation then [ "gitea" ] ++ serverNames ++ workstationNames else [ "gitea" ]; in lib.filter (name: name != currentHost) groupTargets; isWorkstation = lib.elem currentHost workstationNames; hostKeySecret = type: "ssh-host-${type}-key"; hostKeyTypes = [ "ed25519" "rsa" ]; userKeySecret = "ssh-user-ed25519-key"; in { options.modules.network.ssh = { enable = lib.mkEnableOption "the OpenSSH daemon"; hostKeys.restore = lib.mkOption { type = lib.types.bool; default = true; description = '' Restores host keys from secrets. Preserves the fingerprint across reimages. Guests disable this option and generate their own host keys. ''; }; hostKeys.sopsFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Sets encrypted file that contains host private keys. Names each secret `ssh-host--key`. Requires a value when `restore` is enabled. ''; }; userKey.sopsFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Sets encrypted file that contains the SSH client private key. Names the secret `ssh-user-ed25519-key`. Guests leave this unset because they do not authenticate to remote servers. ''; }; }; config = lib.mkIf cfg.enable ( lib.mkMerge [ { services.openssh.enable = true; users.users.${user}.openssh.authorizedKeys.keys = defaultAuthorizedKeys; programs.ssh.knownHosts = knownHosts; home-manager.users.${user}.programs.ssh = { enable = true; enableDefaultConfig = false; settings = lib.mapAttrs (name: _: sshSettingsFor name) ( lib.genAttrs outboundTargetNames (name: name) ); }; } (lib.mkIf cfg.hostKeys.restore { assertions = [ { assertion = cfg.hostKeys.sopsFile != null; message = "Set `modules.network.ssh.hostKeys.sopsFile` when host-key restoration is enabled."; } ]; sops.secrets = lib.genAttrs (map hostKeySecret hostKeyTypes) (_: { inherit (cfg.hostKeys) sopsFile; mode = "0400"; restartUnits = [ "sshd.service" ]; }); services.openssh.hostKeys = [ ]; services.openssh.extraConfig = lib.concatStringsSep "" ( map (type: "HostKey ${config.sops.secrets.${hostKeySecret type}.path}\n") hostKeyTypes ); }) (lib.mkIf (cfg.userKey.sopsFile != null) { sops.secrets.${userKeySecret} = { inherit (cfg.userKey) sopsFile; mode = "0400"; owner = user; }; home-manager.users.${user}.programs.ssh.settings."*".IdentityFile = config.sops.secrets.${userKeySecret}.path; }) ] ); }