{ config, lib, pkgs, ... }: let # The splicing information needed for nativeBuildInputs isn't available # on the derivations likely to be used as `cfg.package`. # This middle-ground solution ensures *an* sshd can do their basic validation # on the configuration. validationPackage = if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then cfg.package else pkgs.buildPackages.openssh; # dont use the "=" operator settingsFormat = let # reports boolean as yes / no mkValueString = with lib; v: if lib.isInt v then toString v else if lib.isString v then v else if true == v then "yes" else if false == v then "no" else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty { }) v}"; base = pkgs.formats.keyValue { mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " "; }; # OpenSSH is very inconsistent with options that can take multiple values. # For some of them, they can simply appear multiple times and are appended, for others the # values must be separated by whitespace or even commas. # Consult either sshd_config(5) or, as last resort, the OpehSSH source for parsing # the options at servconf.c:process_server_config_line_depth() to determine the right "mode" # for each. But fortunately this fact is documented for most of them in the manpage. commaSeparated = [ "Ciphers" "KexAlgorithms" "Macs" ]; spaceSeparated = [ "AuthorizedKeysFile" "AllowGroups" "AllowUsers" "DenyGroups" "DenyUsers" ]; in { inherit (base) type; generate = name: value: let transformedValue = lib.mapAttrs ( key: val: if lib.isList val then if lib.elem key commaSeparated then lib.concatStringsSep "," val else if lib.elem key spaceSeparated then lib.concatStringsSep " " val else throw "list value for unknown key ${key}: ${(lib.generators.toPretty { }) val}" else val ) value; in base.generate name transformedValue; }; configFile = settingsFormat.generate "sshd.conf-settings" ( lib.filterAttrs (n: v: v != null) cfg.settings ); sshconf = pkgs.runCommand "sshd.conf-final" { } '' cat ${configFile} - >$out < and ''; }; Macs = lib.mkOption { type = lib.types.nullOr (lib.types.listOf lib.types.str); default = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha2-256-etm@openssh.com" "umac-128-etm@openssh.com" ]; description = '' Allowed MACs Defaults to recommended settings from both and ''; }; StrictModes = lib.mkOption { type = lib.types.nullOr (lib.types.bool); default = true; description = '' Whether sshd should check file modes and ownership of directories ''; }; Ciphers = lib.mkOption { type = lib.types.nullOr (lib.types.listOf lib.types.str); default = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" "aes128-gcm@openssh.com" "aes256-ctr" "aes192-ctr" "aes128-ctr" ]; description = '' Allowed ciphers Defaults to recommended settings from both and ''; }; AllowUsers = lib.mkOption { type = with lib.types; nullOr (listOf str); default = null; description = '' If specified, login is allowed only for the listed users. See {manpage}`sshd_config(5)` for details. ''; }; DenyUsers = lib.mkOption { type = with lib.types; nullOr (listOf str); default = null; description = '' If specified, login is denied for all listed users. Takes precedence over [](#opt-services.openssh.settings.AllowUsers). See {manpage}`sshd_config(5)` for details. ''; }; AllowGroups = lib.mkOption { type = with lib.types; nullOr (listOf str); default = null; description = '' If specified, login is allowed only for users part of the listed groups. See {manpage}`sshd_config(5)` for details. ''; }; DenyGroups = lib.mkOption { type = with lib.types; nullOr (listOf str); default = null; description = '' If specified, login is denied for all users part of the listed groups. Takes precedence over [](#opt-services.openssh.settings.AllowGroups). See {manpage}`sshd_config(5)` for details. ''; }; # Disabled by default, since pam_motd handles this. PrintMotd = lib.mkEnableOption "printing /etc/motd when a user logs in interactively" // { type = lib.types.nullOr lib.types.bool; }; }; } ); }; extraConfig = lib.mkOption { type = lib.types.lines; default = ""; description = "Verbatim contents of {file}`sshd_config`."; }; moduliFile = lib.mkOption { example = "/etc/my-local-ssh-moduli;"; type = lib.types.path; description = '' Path to `moduli` file to install in `/etc/ssh/moduli`. If this option is unset, then the `moduli` file shipped with OpenSSH will be used. ''; }; }; users.users = lib.mkOption { type = with lib.types; attrsOf (submodule userOptions); }; }; ###### implementation config = lib.mkIf cfg.enable { users.users.sshd = { isSystemUser = true; group = "sshd"; description = "SSH privilege separation user"; }; users.groups.sshd = { }; services.openssh.moduliFile = lib.mkDefault "${cfg.package}/etc/ssh/moduli"; services.openssh.sftpServerExecutable = lib.mkDefault "${cfg.package}/libexec/sftp-server"; environment.etc = authKeysFiles // authPrincipalsFiles // { "ssh/moduli".source = cfg.moduliFile; "ssh/sshd_config".source = sshconf; }; systemd.tmpfiles.settings."ssh-root-provision" = { "/root"."d-" = { user = "root"; group = ":root"; mode = ":700"; }; "/root/.ssh"."d-" = { user = "root"; group = ":root"; mode = ":700"; }; "/root/.ssh/authorized_keys"."f^" = { user = "root"; group = ":root"; mode = ":600"; argument = "ssh.authorized_keys.root"; }; }; systemd = { sockets.sshd = lib.mkIf cfg.startWhenNeeded { description = "SSH Socket"; wantedBy = [ "sockets.target" ]; socketConfig.ListenStream = if cfg.listenAddresses != [ ] then lib.concatMap ( { addr, port }: if port != null then [ "${addr}:${toString port}" ] else map (p: "${addr}:${toString p}") cfg.ports ) cfg.listenAddresses else cfg.ports; socketConfig.Accept = true; # Prevent brute-force attacks from shutting down socket socketConfig.TriggerLimitIntervalSec = 0; }; services."sshd@" = { description = "SSH per-connection Daemon"; after = [ "network.target" "sshd-keygen.service" ]; wants = [ "sshd-keygen.service" ]; stopIfChanged = false; path = [ cfg.package ]; environment.LD_LIBRARY_PATH = nssModulesPath; serviceConfig = { ExecStart = lib.concatStringsSep " " [ "-${lib.getExe' cfg.package "sshd"}" "-i" "-D" "-f /etc/ssh/sshd_config" ]; KillMode = "process"; StandardInput = "socket"; StandardError = "journal"; }; }; services.sshd = lib.mkIf (!cfg.startWhenNeeded) { description = "SSH Daemon"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "sshd-keygen.service" ]; wants = [ "sshd-keygen.service" ]; stopIfChanged = false; path = [ cfg.package ]; environment.LD_LIBRARY_PATH = nssModulesPath; restartTriggers = [ config.environment.etc."ssh/sshd_config".source ]; serviceConfig = { Restart = "always"; ExecStart = lib.concatStringsSep " " [ (lib.getExe' cfg.package "sshd") "-D" "-f" "/etc/ssh/sshd_config" ]; KillMode = "process"; }; }; services.sshd-keygen = { description = "SSH Host Keys Generation"; unitConfig = { ConditionFileNotEmpty = map (k: "|!${k.path}") cfg.hostKeys; }; serviceConfig = { Type = "oneshot"; }; path = [ cfg.package ]; script = lib.flip lib.concatMapStrings cfg.hostKeys (k: '' if ! [ -s "${k.path}" ]; then if ! [ -h "${k.path}" ]; then rm -f "${k.path}" fi mkdir -p "$(dirname '${k.path}')" chmod 0755 "$(dirname '${k.path}')" ssh-keygen \ -t "${k.type}" \ ${lib.optionalString (k ? bits) "-b ${toString k.bits}"} \ ${lib.optionalString (k ? rounds) "-a ${toString k.rounds}"} \ ${lib.optionalString (k ? comment) "-C '${k.comment}'"} \ ${lib.optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \ -f "${k.path}" \ -N "" fi ''); }; }; networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall cfg.ports; security.pam.services.sshd = lib.mkIf cfg.settings.UsePAM { startSession = true; showMotd = true; unixAuth = if cfg.settings.PasswordAuthentication == true then true else false; }; # These values are merged with the ones defined externally, see: # https://github.com/NixOS/nixpkgs/pull/10155 # https://github.com/NixOS/nixpkgs/pull/41745 services.openssh.authorizedKeysFiles = lib.optional cfg.authorizedKeysInHomedir "%h/.ssh/authorized_keys" ++ [ "/etc/ssh/authorized_keys.d/%u" ]; services.openssh.settings.AuthorizedPrincipalsFile = lib.mkIf ( authPrincipalsFiles != { } ) "/etc/ssh/authorized_principals.d/%u"; services.openssh.extraConfig = lib.mkOrder 0 '' Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner} AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"} ${lib.concatMapStrings (port: '' Port ${toString port} '') cfg.ports} ${lib.concatMapStrings ( { port, addr, ... }: '' ListenAddress ${addr}${lib.optionalString (port != null) (":" + toString port)} '' ) cfg.listenAddresses} ${lib.optionalString cfgc.setXAuthLocation '' XAuthLocation ${pkgs.xorg.xauth}/bin/xauth ''} ${lib.optionalString cfg.allowSFTP '' Subsystem sftp ${cfg.sftpServerExecutable} ${lib.concatStringsSep " " cfg.sftpFlags} ''} AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} ${lib.optionalString (cfg.authorizedKeysCommand != "none") '' AuthorizedKeysCommand ${cfg.authorizedKeysCommand} AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser} ''} ${lib.flip lib.concatMapStrings cfg.hostKeys (k: '' HostKey ${k.path} '')} ''; system.checks = [ (pkgs.runCommand "check-sshd-config" { nativeBuildInputs = [ validationPackage ]; } '' ${lib.concatMapStringsSep "\n" ( lport: "sshd -G -T -C lport=${toString lport} -f ${sshconf} > /dev/null" ) cfg.ports} ${lib.concatMapStringsSep "\n" ( la: lib.concatMapStringsSep "\n" ( port: "sshd -G -T -C ${lib.escapeShellArg "laddr=${la.addr},lport=${toString port}"} -f ${sshconf} > /dev/null" ) (if la.port != null then [ la.port ] else cfg.ports) ) cfg.listenAddresses} touch $out '' ) ]; assertions = [ { assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true; message = "cannot enable X11 forwarding without setting xauth location"; } { assertion = (builtins.match "(.*\n)?(\t )*[Kk][Ee][Rr][Bb][Ee][Rr][Oo][Ss][Aa][Uu][Tt][Hh][Ee][Nn][Tt][Ii][Cc][Aa][Tt][Ii][Oo][Nn][ |\t|=|\"]+yes.*" "${configFile}\n${cfg.extraConfig}") != null -> cfgc.package.withKerberos; message = "cannot enable Kerberos authentication without using a package with Kerberos support"; } { assertion = (builtins.match "(.*\n)?(\t )*[Gg][Ss][Ss][Aa][Pp][Ii][Aa][Uu][Tt][Hh][Ee][Nn][Tt][Ii][Cc][Aa][Tt][Ii][Oo][Nn][ |\t|=|\"]+yes.*" "${configFile}\n${cfg.extraConfig}") != null -> cfgc.package.withKerberos; message = "cannot enable GSSAPI authentication without using a package with Kerberos support"; } ( let duplicates = # Filter out the groups with more than 1 element lib.filter (l: lib.length l > 1) ( # Grab the groups, we don't care about the group identifiers lib.attrValues ( # Group the settings that are the same in lower case lib.groupBy lib.strings.toLower (lib.attrNames cfg.settings) ) ); formattedDuplicates = lib.concatMapStringsSep ", " ( dupl: "(${lib.concatStringsSep ", " dupl})" ) duplicates; in { assertion = lib.length duplicates == 0; message = ''Duplicate sshd config key; does your capitalization match the option's? Duplicate keys: ${formattedDuplicates}''; } ) ] ++ lib.forEach cfg.listenAddresses ( { addr, ... }: { assertion = addr != null; message = "addr must be specified in each listenAddresses entry"; } ); }; }