{ config, lib, pkgs, ... }: with lib; let # The splicing information needed for nativeBuildInputs isn't available # on the derivations likely to be used as `cfgc.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 cfgc.package else pkgs.buildPackages.openssh; # reports boolean as yes / no mkValueStringSshd = with lib; v: if isInt v then toString v else if isString v then v else if true == v then "yes" else if false == v then "no" else if isList v then concatStringsSep "," v else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}"; # dont use the "=" operator settingsFormat = (pkgs.formats.keyValue { mkKeyValue = lib.generators.mkKeyValueDefault { mkValueString = mkValueStringSshd; } " ";}); configFile = settingsFormat.generate "config" cfg.settings; sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } '' cat ${configFile} - >$out < and ''; }; Macs = mkOption { type = types.listOf types.str; default = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha2-256-etm@openssh.com" "umac-128-etm@openssh.com" "hmac-sha2-512" "hmac-sha2-256" "umac-128@openssh.com" ]; description = lib.mdDoc '' Allowed MACs Defaults to recommended settings from both and ''; }; Ciphers = mkOption { type = types.listOf types.str; default = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" "aes128-gcm@openssh.com" "aes256-ctr" "aes192-ctr" "aes128-ctr" ]; description = lib.mdDoc '' Allowed ciphers Defaults to recommended settings from both and ''; }; }; }); }; extraConfig = mkOption { type = types.lines; default = ""; description = lib.mdDoc "Verbatim contents of {file}`sshd_config`."; }; moduliFile = mkOption { example = "/etc/my-local-ssh-moduli;"; type = types.path; description = lib.mdDoc '' 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 = mkOption { type = with types; attrsOf (submodule userOptions); }; }; ###### implementation config = mkIf cfg.enable { users.users.sshd = { isSystemUser = true; group = "sshd"; description = "SSH privilege separation user"; }; users.groups.sshd = {}; services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli"; services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server"; environment.etc = authKeysFiles // { "ssh/moduli".source = cfg.moduliFile; "ssh/sshd_config".source = sshconf; }; systemd = let service = { description = "SSH Daemon"; wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target"; after = [ "network.target" ]; stopIfChanged = false; path = [ cfgc.package pkgs.gawk ]; environment.LD_LIBRARY_PATH = nssModulesPath; restartTriggers = optionals (!cfg.startWhenNeeded) [ config.environment.etc."ssh/sshd_config".source ]; preStart = '' # Make sure we don't write to stdout, since in case of # socket activation, it goes to the remote side (#19589). exec >&2 ${flip concatMapStrings cfg.hostKeys (k: '' if ! [ -s "${k.path}" ]; then if ! [ -h "${k.path}" ]; then rm -f "${k.path}" fi mkdir -m 0755 -p "$(dirname '${k.path}')" ssh-keygen \ -t "${k.type}" \ ${if k ? bits then "-b ${toString k.bits}" else ""} \ ${if k ? rounds then "-a ${toString k.rounds}" else ""} \ ${if k ? comment then "-C '${k.comment}'" else ""} \ ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \ -f "${k.path}" \ -N "" fi '')} ''; serviceConfig = { ExecStart = (optionalString cfg.startWhenNeeded "-") + "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") + "-D " + # don't detach into a daemon process "-f /etc/ssh/sshd_config"; KillMode = "process"; } // (if cfg.startWhenNeeded then { StandardInput = "socket"; StandardError = "journal"; } else { Restart = "always"; Type = "simple"; }); }; in if cfg.startWhenNeeded then { sockets.sshd = { description = "SSH Socket"; wantedBy = [ "sockets.target" ]; socketConfig.ListenStream = if cfg.listenAddresses != [] then map (l: "${l.addr}:${toString (if l.port != null then l.port else 22)}") cfg.listenAddresses else cfg.ports; socketConfig.Accept = true; # Prevent brute-force attacks from shutting down socket socketConfig.TriggerLimitIntervalSec = 0; }; services."sshd@" = service; } else { services.sshd = service; }; networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else []; security.pam.services.sshd = { startSession = true; showMotd = true; unixAuth = cfg.settings.PasswordAuthentication; }; # 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 = [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ]; services.openssh.extraConfig = mkOrder 0 '' UsePAM yes Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner} AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"} ${concatMapStrings (port: '' Port ${toString port} '') cfg.ports} ${concatMapStrings ({ port, addr, ... }: '' ListenAddress ${addr}${if port != null then ":" + toString port else ""} '') cfg.listenAddresses} ${optionalString cfgc.setXAuthLocation '' XAuthLocation ${pkgs.xorg.xauth}/bin/xauth ''} ${optionalString cfg.allowSFTP '' Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags} ''} PrintMotd no # handled by pam_motd AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} ${optionalString (cfg.authorizedKeysCommand != "none") '' AuthorizedKeysCommand ${cfg.authorizedKeysCommand} AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser} ''} ${flip concatMapStrings cfg.hostKeys (k: '' HostKey ${k.path} '')} ''; assertions = [{ assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true; message = "cannot enable X11 forwarding without setting xauth location";}] ++ forEach cfg.listenAddresses ({ addr, ... }: { assertion = addr != null; message = "addr must be specified in each listenAddresses entry"; }); }; }