1 { config, lib, pkgs, ... }:
7 # The splicing information needed for nativeBuildInputs isn't available
8 # on the derivations likely to be used as `cfgc.package`.
9 # This middle-ground solution ensures *an* sshd can do their basic validation
10 # on the configuration.
11 validationPackage = if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
13 else pkgs.buildPackages.openssh;
15 sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } ''
20 ssh-keygen -q -f mock-hostkey -N ""
21 sshd -t -f $out -h mock-hostkey
24 cfg = config.services.openssh;
25 cfgc = config.programs.ssh;
27 nssModulesPath = config.system.nssModules.path;
31 options.openssh.authorizedKeys = {
33 type = types.listOf types.singleLineStr;
35 description = lib.mdDoc ''
36 A list of verbatim OpenSSH public keys that should be added to the
37 user's authorized keys. The keys are added to a file that the SSH
38 daemon reads in addition to the the user's authorized_keys file.
39 You can combine the `keys` and
41 Warning: If you are using `NixOps` then don't use this
42 option since it will replace the key required for deployment via ssh.
45 "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
46 "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
51 type = types.listOf types.path;
53 description = lib.mdDoc ''
54 A list of files each containing one OpenSSH public key that should be
55 added to the user's authorized keys. The contents of the files are
56 read at build time and added to a file that the SSH daemon reads in
57 addition to the the user's authorized_keys file. You can combine the
58 `keyFiles` and `keys` options.
66 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" {
68 source = pkgs.writeText "${u.name}-authorized_keys" ''
69 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys}
70 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles}
73 usersWithKeys = attrValues (flip filterAttrs config.users.users (n: u:
74 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
76 in listToAttrs (map mkAuthKeyFile usersWithKeys);
82 (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
83 (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
84 (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ])
96 description = lib.mdDoc ''
97 Whether to enable the OpenSSH secure shell daemon, which
98 allows secure remote logins.
102 startWhenNeeded = mkOption {
105 description = lib.mdDoc ''
106 If set, {command}`sshd` is socket-activated; that
107 is, instead of having it permanently running as a daemon,
108 systemd will start an instance for each incoming connection.
112 forwardX11 = mkOption {
115 description = lib.mdDoc ''
116 Whether to allow X11 connections to be forwarded.
120 allowSFTP = mkOption {
123 description = lib.mdDoc ''
124 Whether to enable the SFTP subsystem in the SSH daemon. This
125 enables the use of commands such as {command}`sftp` and
130 sftpServerExecutable = mkOption {
132 example = "internal-sftp";
133 description = lib.mdDoc ''
134 The sftp server executable. Can be a path or "internal-sftp" to use
135 the sftp server built into the sshd binary.
139 sftpFlags = mkOption {
140 type = with types; listOf str;
142 example = [ "-f AUTHPRIV" "-l INFO" ];
143 description = lib.mdDoc ''
144 Commandline flags to add to sftp-server.
148 permitRootLogin = mkOption {
149 default = "prohibit-password";
150 type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
151 description = lib.mdDoc ''
152 Whether the root user can login using ssh.
156 gatewayPorts = mkOption {
159 description = lib.mdDoc ''
160 Specifies whether remote hosts are allowed to connect to
161 ports forwarded for the client. See
162 {manpage}`sshd_config(5)`.
167 type = types.listOf types.port;
169 description = lib.mdDoc ''
170 Specifies on which ports the SSH daemon listens.
174 openFirewall = mkOption {
177 description = lib.mdDoc ''
178 Whether to automatically open the specified ports in the firewall.
182 listenAddresses = mkOption {
183 type = with types; listOf (submodule {
186 type = types.nullOr types.str;
188 description = lib.mdDoc ''
189 Host, IPv4 or IPv6 address to listen to.
193 type = types.nullOr types.int;
195 description = lib.mdDoc ''
202 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
203 description = lib.mdDoc ''
204 List of addresses and ports to listen on (ListenAddress directive
205 in config). If port is not specified for address sshd will listen
206 on all ports specified by `ports` option.
207 NOTE: this will override default listening on all local addresses and port 22.
208 NOTE: setting this option won't automatically enable given ports
209 in firewall configuration.
213 passwordAuthentication = mkOption {
216 description = lib.mdDoc ''
217 Specifies whether password authentication is allowed.
221 kbdInteractiveAuthentication = mkOption {
224 description = lib.mdDoc ''
225 Specifies whether keyboard-interactive authentication is allowed.
229 hostKeys = mkOption {
230 type = types.listOf types.attrs;
232 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
233 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
236 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; rounds = 100; openSSHFormat = true; }
237 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; rounds = 100; comment = "key comment"; }
239 description = lib.mdDoc ''
240 NixOS can automatically generate SSH host keys. This option
241 specifies the path, type and size of each key. See
242 {manpage}`ssh-keygen(1)` for supported types
248 type = types.nullOr types.lines;
250 description = lib.mdDoc ''
251 Message to display to the remote user before authentication is allowed.
255 authorizedKeysFiles = mkOption {
256 type = types.listOf types.str;
258 description = lib.mdDoc ''
259 Specify the rules for which files to read on the host.
261 This is an advanced option. If you're looking to configure user
262 keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys)
263 or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles).
265 These are paths relative to the host root file system or home
266 directories and they are subject to certain token expansion rules.
267 See AuthorizedKeysFile in man sshd_config for details.
271 authorizedKeysCommand = mkOption {
274 description = lib.mdDoc ''
275 Specifies a program to be used to look up the user's public
276 keys. The program must be owned by root, not writable by group
277 or others and specified by an absolute path.
281 authorizedKeysCommandUser = mkOption {
284 description = lib.mdDoc ''
285 Specifies the user under whose account the AuthorizedKeysCommand
286 is run. It is recommended to use a dedicated user that has no
287 other role on the host than running authorized keys commands.
291 kexAlgorithms = mkOption {
292 type = types.listOf types.str;
294 "sntrup761x25519-sha512@openssh.com"
296 "curve25519-sha256@libssh.org"
297 "diffie-hellman-group-exchange-sha256"
299 description = lib.mdDoc ''
300 Allowed key exchange algorithms
302 Uses the lower bound recommended in both
303 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
305 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
310 type = types.listOf types.str;
312 "chacha20-poly1305@openssh.com"
313 "aes256-gcm@openssh.com"
314 "aes128-gcm@openssh.com"
319 description = lib.mdDoc ''
322 Defaults to recommended settings from both
323 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
325 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
330 type = types.listOf types.str;
332 "hmac-sha2-512-etm@openssh.com"
333 "hmac-sha2-256-etm@openssh.com"
334 "umac-128-etm@openssh.com"
337 "umac-128@openssh.com"
339 description = lib.mdDoc ''
342 Defaults to recommended settings from both
343 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
345 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
349 logLevel = mkOption {
350 type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
351 default = "INFO"; # upstream default
352 description = lib.mdDoc ''
353 Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
354 QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is INFO. DEBUG and DEBUG1
355 are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
356 violates the privacy of users and is not recommended.
363 description = lib.mdDoc ''
364 Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
365 the remote IP address maps back to the very same IP address.
366 If this option is set to no (the default) then only addresses and not host names may be used in
367 ~/.ssh/authorized_keys from and sshd_config Match Host directives.
371 extraConfig = mkOption {
374 description = lib.mdDoc "Verbatim contents of {file}`sshd_config`.";
377 moduliFile = mkOption {
378 example = "/etc/my-local-ssh-moduli;";
380 description = lib.mdDoc ''
381 Path to `moduli` file to install in
382 `/etc/ssh/moduli`. If this option is unset, then
383 the `moduli` file shipped with OpenSSH will be used.
389 users.users = mkOption {
390 type = with types; attrsOf (submodule userOptions);
396 ###### implementation
398 config = mkIf cfg.enable {
404 description = "SSH privilege separation user";
406 users.groups.sshd = {};
408 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
409 services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server";
411 environment.etc = authKeysFiles //
412 { "ssh/moduli".source = cfg.moduliFile;
413 "ssh/sshd_config".source = sshconf;
419 { description = "SSH Daemon";
420 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
421 after = [ "network.target" ];
422 stopIfChanged = false;
423 path = [ cfgc.package pkgs.gawk ];
424 environment.LD_LIBRARY_PATH = nssModulesPath;
426 restartTriggers = optionals (!cfg.startWhenNeeded) [
427 config.environment.etc."ssh/sshd_config".source
432 # Make sure we don't write to stdout, since in case of
433 # socket activation, it goes to the remote side (#19589).
436 ${flip concatMapStrings cfg.hostKeys (k: ''
437 if ! [ -s "${k.path}" ]; then
438 if ! [ -h "${k.path}" ]; then
441 mkdir -m 0755 -p "$(dirname '${k.path}')"
444 ${if k ? bits then "-b ${toString k.bits}" else ""} \
445 ${if k ? rounds then "-a ${toString k.rounds}" else ""} \
446 ${if k ? comment then "-C '${k.comment}'" else ""} \
447 ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \
456 (optionalString cfg.startWhenNeeded "-") +
457 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
458 "-D " + # don't detach into a daemon process
459 "-f /etc/ssh/sshd_config";
460 KillMode = "process";
461 } // (if cfg.startWhenNeeded then {
462 StandardInput = "socket";
463 StandardError = "journal";
472 if cfg.startWhenNeeded then {
475 { description = "SSH Socket";
476 wantedBy = [ "sockets.target" ];
477 socketConfig.ListenStream = if cfg.listenAddresses != [] then
478 map (l: "${l.addr}:${toString (if l.port != null then l.port else 22)}") cfg.listenAddresses
481 socketConfig.Accept = true;
482 # Prevent brute-force attacks from shutting down socket
483 socketConfig.TriggerLimitIntervalSec = 0;
486 services."sshd@" = service;
490 services.sshd = service;
494 networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else [];
496 security.pam.services.sshd =
497 { startSession = true;
499 unixAuth = cfg.passwordAuthentication;
502 # These values are merged with the ones defined externally, see:
503 # https://github.com/NixOS/nixpkgs/pull/10155
504 # https://github.com/NixOS/nixpkgs/pull/41745
505 services.openssh.authorizedKeysFiles =
506 [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
508 services.openssh.extraConfig = mkOrder 0
512 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner}
514 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
515 ${concatMapStrings (port: ''
516 Port ${toString port}
519 ${concatMapStrings ({ port, addr, ... }: ''
520 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
521 '') cfg.listenAddresses}
523 ${optionalString cfgc.setXAuthLocation ''
524 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
527 X11Forwarding ${if cfg.forwardX11 then "yes" else "no"}
529 ${optionalString cfg.allowSFTP ''
530 Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags}
533 PermitRootLogin ${cfg.permitRootLogin}
534 GatewayPorts ${cfg.gatewayPorts}
535 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
536 KbdInteractiveAuthentication ${if cfg.kbdInteractiveAuthentication then "yes" else "no"}
538 PrintMotd no # handled by pam_motd
540 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
541 ${optionalString (cfg.authorizedKeysCommand != "none") ''
542 AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
543 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser}
546 ${flip concatMapStrings cfg.hostKeys (k: ''
550 KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}
551 Ciphers ${concatStringsSep "," cfg.ciphers}
552 MACs ${concatStringsSep "," cfg.macs}
554 LogLevel ${cfg.logLevel}
556 UseDNS ${if cfg.useDns then "yes" else "no"}
560 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
561 message = "cannot enable X11 forwarding without setting xauth location";}]
562 ++ forEach cfg.listenAddresses ({ addr, ... }: {
563 assertion = addr != null;
564 message = "addr must be specified in each listenAddresses entry";