diff --git a/modules/programs/ssh/default.nix b/modules/programs/ssh/default.nix index f93890fbc..f4b1f2024 100644 --- a/modules/programs/ssh/default.nix +++ b/modules/programs/ssh/default.nix @@ -3,89 +3,205 @@ with lib; let - cfg = config.programs.ssh; + cfg = config.programs.ssh; knownHosts = map (h: getAttr h cfg.knownHosts) (attrNames cfg.knownHosts); - host = - { name, ... }: - { - options = { - hostNames = mkOption { - type = types.listOf types.str; - default = []; - description = '' - A list of host names and/or IP numbers used for accessing - the host's ssh service. - ''; - }; - publicKey = mkOption { - default = null; - type = types.nullOr types.str; - example = "ecdsa-sha2-nistp521 AAAAE2VjZHN...UEPg=="; - description = '' - The public key data for the host. You can fetch a public key - from a running SSH server with the ssh-keyscan - command. The public key should not include any host names, only - the key type and the key itself. - ''; - }; - publicKeyFile = mkOption { - default = null; - type = types.nullOr types.path; - description = '' - The path to the public key file for the host. The public - key file is read at build time and saved in the Nix store. - You can fetch a public key file from a running SSH server - with the ssh-keyscan command. The content - of the file should follow the same format as described for - the publicKey option. - ''; - }; + host = { name, ... }: { + options = { + hostNames = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + A list of host names and/or IP numbers used for accessing + the host's ssh service. + ''; }; - config = { - hostNames = mkDefault [ name ]; + publicKey = mkOption { + default = null; + type = types.nullOr types.str; + example = "ecdsa-sha2-nistp521 AAAAE2VjZHN...UEPg=="; + description = '' + The public key data for the host. You can fetch a public key + from a running SSH server with the ssh-keyscan + command. The public key should not include any host names, only + the key type and the key itself. + ''; + }; + publicKeyFile = mkOption { + default = null; + type = types.nullOr types.path; + description = '' + The path to the public key file for the host. The public + key file is read at build time and saved in the Nix store. + You can fetch a public key file from a running SSH server + with the ssh-keyscan command. The content + of the file should follow the same format as described for + the publicKey option. + ''; }; }; -in + config = { hostNames = mkDefault [ name ]; }; + }; -{ +in { options = { - programs.ssh.knownHosts = mkOption { - default = {}; - type = types.attrsOf (types.submodule host); + users.users = + mkOption { type = with types; attrsOf (submodule userOptions); }; + + services.openssh.authorizedKeysFiles = mkOption { + type = types.listOf types.str; + default = [ ]; description = '' - The set of system-wide known SSH hosts. - ''; - example = literalExpression '' - [ - { - hostNames = [ "myhost" "myhost.mydomain.com" "10.10.1.4" ]; - publicKeyFile = ./pubkeys/myhost_ssh_host_dsa_key.pub; - } - { - hostNames = [ "myhost2" ]; - publicKeyFile = ./pubkeys/myhost2_ssh_host_dsa_key.pub; - } - ] + Specify the rules for which files to read on the host. + + This is an advanced option. If you're looking to configure user + keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys) + or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles). + + These are paths relative to the host root file system or home + directories and they are subject to certain token expansion rules. + See AuthorizedKeysFile in man sshd_config for details. ''; }; + + programs.ssh = { + knownHosts = mkOption { + default = { }; + type = types.attrsOf (types.submodule host); + description = '' + The set of system-wide known SSH hosts. + ''; + example = literalExpression '' + [ + { + hostNames = [ "myhost" "myhost.mydomain.com" "10.10.1.4" ]; + publicKeyFile = ./pubkeys/myhost_ssh_host_dsa_key.pub; + } + { + hostNames = [ "myhost2" ]; + publicKeyFile = ./pubkeys/myhost2_ssh_host_dsa_key.pub; + } + ] + ''; + }; + + pubkeyAcceptedKeyTypes = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "ssh-ed25519" "ssh-rsa" ]; + description = lib.mdDoc '' + Specifies the key types that will be used for public key authentication. + ''; + }; + + hostKeyAlgorithms = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "ssh-ed25519" "ssh-rsa" ]; + description = '' + Specifies the host key algorithms that the client wants to use in order of preference. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration text written to `/etc/ssh/ssh_config.d/10-extra-nix.conf`. + See {manpage}`ssh_config(5)` for help. + ''; + }; + + kexAlgorithms = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = [ + "curve25519-sha256@libssh.org" + "diffie-hellman-group-exchange-sha256" + ]; + description = '' + Specifies the available KEX (Key Exchange) algorithms. + ''; + }; + + ciphers = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" ]; + description = '' + Specifies the ciphers allowed and their order of preference. + ''; + }; + + macs = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha1" ]; + description = '' + Specifies the MAC (message authentication code) algorithms in order of preference. The MAC algorithm is used + for data integrity protection. + ''; + }; + }; }; config = { assertions = flip mapAttrsToList cfg.knownHosts (name: data: { - assertion = (data.publicKey == null && data.publicKeyFile != null) || - (data.publicKey != null && data.publicKeyFile == null); - message = "knownHost ${name} must contain either a publicKey or publicKeyFile"; + assertion = (data.publicKey == null && data.publicKeyFile != null) + || (data.publicKey != null && data.publicKeyFile == null); + message = + "knownHost ${name} must contain either a publicKey or publicKeyFile"; }); - environment.etc."ssh/ssh_known_hosts".text = (flip (concatMapStringsSep "\n") knownHosts - (h: assert h.hostNames != []; - concatStringsSep "," h.hostNames + " " - + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile) - )) + "\n"; + environment.etc = authKeysFiles // { + "ssh/ssh_known_hosts" = mkIf (builtins.length knownHosts > 0) { + text = (flip (concatMapStringsSep "\n") knownHosts (h: + assert h.hostNames != [ ]; + concatStringsSep "," h.hostNames + " " + (if h.publicKey != null then + h.publicKey + else + readFile h.publicKeyFile))) + "\n"; + }; + "ssh/sshd_config.d/101-authorized-keys.conf" = { + text = '' + AuthorizedKeysFile ${ + toString config.services.openssh.authorizedKeysFiles + } + ''; + # Allows us to automatically migrate from using a file to a symlink + knownSha256Hashes = [ oldAuthorizedKeysHash ]; + }; + "ssh/sshd_config.d/10-extra-nix.conf" = { + text = '' + ${optionalString (cfg.pubkeyAcceptedKeyTypes != [ ]) + "PubkeyAcceptedKeyTypes ${ + concatStringsSep "," cfg.pubkeyAcceptedKeyTypes + }"} + + ${config.programs.ssh.extraConfig} + + ${optionalString (cfg.hostKeyAlgorithms != [ ]) + "HostKeyAlgorithms ${concatStringsSep "," cfg.hostKeyAlgorithms}"} + ${optionalString (cfg.kexAlgorithms != null) + "KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}"} + ${optionalString (cfg.ciphers != null) + "Ciphers ${concatStringsSep "," cfg.ciphers}"} + ${optionalString (cfg.macs != null) + "MACs ${concatStringsSep "," cfg.macs}"} + ''; + }; + }; + + # Clean up .before-nix-darwin file left over from using knownSha256Hashes + system.activationScripts.etc.text = '' + auth_keys_orig=/etc/ssh/sshd_config.d/101-authorized-keys.conf.before-nix-darwin + if [ -e "$auth_keys_orig" ] && [ "$(shasum -a 256 $auth_keys_orig | cut -d ' ' -f 1)" = "${oldAuthorizedKeysHash}" ]; then + rm "$auth_keys_orig" + fi + ''; }; }