at 24.11-pre 28 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 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 12 then cfgc.package 13 else pkgs.buildPackages.openssh; 14 15 # dont use the "=" operator 16 settingsFormat = 17 let 18 # reports boolean as yes / no 19 mkValueString = with lib; v: 20 if isInt v then toString v 21 else if isString v then v 22 else if true == v then "yes" 23 else if false == v then "no" 24 else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}"; 25 26 base = pkgs.formats.keyValue { 27 mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " "; 28 }; 29 # OpenSSH is very inconsistent with options that can take multiple values. 30 # For some of them, they can simply appear multiple times and are appended, for others the 31 # values must be separated by whitespace or even commas. 32 # Consult either sshd_config(5) or, as last resort, the OpehSSH source for parsing 33 # the options at servconf.c:process_server_config_line_depth() to determine the right "mode" 34 # for each. But fortunaly this fact is documented for most of them in the manpage. 35 commaSeparated = [ "Ciphers" "KexAlgorithms" "Macs" ]; 36 spaceSeparated = [ "AuthorizedKeysFile" "AllowGroups" "AllowUsers" "DenyGroups" "DenyUsers" ]; 37 in { 38 inherit (base) type; 39 generate = name: value: 40 let transformedValue = mapAttrs (key: val: 41 if isList val then 42 if elem key commaSeparated then concatStringsSep "," val 43 else if elem key spaceSeparated then concatStringsSep " " val 44 else throw "list value for unknown key ${key}: ${(lib.generators.toPretty {}) val}" 45 else 46 val 47 ) value; 48 in 49 base.generate name transformedValue; 50 }; 51 52 configFile = settingsFormat.generate "sshd.conf-settings" (filterAttrs (n: v: v != null) cfg.settings); 53 sshconf = pkgs.runCommand "sshd.conf-final" { } '' 54 cat ${configFile} - >$out <<EOL 55 ${cfg.extraConfig} 56 EOL 57 ''; 58 59 cfg = config.services.openssh; 60 cfgc = config.programs.ssh; 61 62 63 nssModulesPath = config.system.nssModules.path; 64 65 userOptions = { 66 67 options.openssh.authorizedKeys = { 68 keys = mkOption { 69 type = types.listOf types.singleLineStr; 70 default = []; 71 description = '' 72 A list of verbatim OpenSSH public keys that should be added to the 73 user's authorized keys. The keys are added to a file that the SSH 74 daemon reads in addition to the the user's authorized_keys file. 75 You can combine the `keys` and 76 `keyFiles` options. 77 Warning: If you are using `NixOps` then don't use this 78 option since it will replace the key required for deployment via ssh. 79 ''; 80 example = [ 81 "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host" 82 "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar" 83 ]; 84 }; 85 86 keyFiles = mkOption { 87 type = types.listOf types.path; 88 default = []; 89 description = '' 90 A list of files each containing one OpenSSH public key that should be 91 added to the user's authorized keys. The contents of the files are 92 read at build time and added to a file that the SSH daemon reads in 93 addition to the the user's authorized_keys file. You can combine the 94 `keyFiles` and `keys` options. 95 ''; 96 }; 97 }; 98 99 options.openssh.authorizedPrincipals = mkOption { 100 type = with types; listOf types.singleLineStr; 101 default = []; 102 description = '' 103 A list of verbatim principal names that should be added to the user's 104 authorized principals. 105 ''; 106 example = [ 107 "example@host" 108 "foo@bar" 109 ]; 110 }; 111 112 }; 113 114 authKeysFiles = let 115 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" { 116 mode = "0444"; 117 source = pkgs.writeText "${u.name}-authorized_keys" '' 118 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys} 119 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles} 120 ''; 121 }; 122 usersWithKeys = attrValues (flip filterAttrs config.users.users (n: u: 123 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0 124 )); 125 in listToAttrs (map mkAuthKeyFile usersWithKeys); 126 127 authPrincipalsFiles = let 128 mkAuthPrincipalsFile = u: nameValuePair "ssh/authorized_principals.d/${u.name}" { 129 mode = "0444"; 130 text = concatStringsSep "\n" u.openssh.authorizedPrincipals; 131 }; 132 usersWithPrincipals = attrValues (flip filterAttrs config.users.users (n: u: 133 length u.openssh.authorizedPrincipals != 0 134 )); 135 in listToAttrs (map mkAuthPrincipalsFile usersWithPrincipals); 136 137in 138 139{ 140 imports = [ 141 (mkAliasOptionModuleMD [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ]) 142 (mkAliasOptionModuleMD [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ]) 143 (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ]) 144 145 (mkRenamedOptionModule [ "services" "openssh" "kbdInteractiveAuthentication" ] [ "services" "openssh" "settings" "KbdInteractiveAuthentication" ]) 146 (mkRenamedOptionModule [ "services" "openssh" "passwordAuthentication" ] [ "services" "openssh" "settings" "PasswordAuthentication" ]) 147 (mkRenamedOptionModule [ "services" "openssh" "useDns" ] [ "services" "openssh" "settings" "UseDns" ]) 148 (mkRenamedOptionModule [ "services" "openssh" "permitRootLogin" ] [ "services" "openssh" "settings" "PermitRootLogin" ]) 149 (mkRenamedOptionModule [ "services" "openssh" "logLevel" ] [ "services" "openssh" "settings" "LogLevel" ]) 150 (mkRenamedOptionModule [ "services" "openssh" "macs" ] [ "services" "openssh" "settings" "Macs" ]) 151 (mkRenamedOptionModule [ "services" "openssh" "ciphers" ] [ "services" "openssh" "settings" "Ciphers" ]) 152 (mkRenamedOptionModule [ "services" "openssh" "kexAlgorithms" ] [ "services" "openssh" "settings" "KexAlgorithms" ]) 153 (mkRenamedOptionModule [ "services" "openssh" "gatewayPorts" ] [ "services" "openssh" "settings" "GatewayPorts" ]) 154 (mkRenamedOptionModule [ "services" "openssh" "forwardX11" ] [ "services" "openssh" "settings" "X11Forwarding" ]) 155 ]; 156 157 ###### interface 158 159 options = { 160 161 services.openssh = { 162 163 enable = mkOption { 164 type = types.bool; 165 default = false; 166 description = '' 167 Whether to enable the OpenSSH secure shell daemon, which 168 allows secure remote logins. 169 ''; 170 }; 171 172 startWhenNeeded = mkOption { 173 type = types.bool; 174 default = false; 175 description = '' 176 If set, {command}`sshd` is socket-activated; that 177 is, instead of having it permanently running as a daemon, 178 systemd will start an instance for each incoming connection. 179 ''; 180 }; 181 182 allowSFTP = mkOption { 183 type = types.bool; 184 default = true; 185 description = '' 186 Whether to enable the SFTP subsystem in the SSH daemon. This 187 enables the use of commands such as {command}`sftp` and 188 {command}`sshfs`. 189 ''; 190 }; 191 192 sftpServerExecutable = mkOption { 193 type = types.str; 194 example = "internal-sftp"; 195 description = '' 196 The sftp server executable. Can be a path or "internal-sftp" to use 197 the sftp server built into the sshd binary. 198 ''; 199 }; 200 201 sftpFlags = mkOption { 202 type = with types; listOf str; 203 default = []; 204 example = [ "-f AUTHPRIV" "-l INFO" ]; 205 description = '' 206 Commandline flags to add to sftp-server. 207 ''; 208 }; 209 210 ports = mkOption { 211 type = types.listOf types.port; 212 default = [22]; 213 description = '' 214 Specifies on which ports the SSH daemon listens. 215 ''; 216 }; 217 218 openFirewall = mkOption { 219 type = types.bool; 220 default = true; 221 description = '' 222 Whether to automatically open the specified ports in the firewall. 223 ''; 224 }; 225 226 listenAddresses = mkOption { 227 type = with types; listOf (submodule { 228 options = { 229 addr = mkOption { 230 type = types.nullOr types.str; 231 default = null; 232 description = '' 233 Host, IPv4 or IPv6 address to listen to. 234 ''; 235 }; 236 port = mkOption { 237 type = types.nullOr types.int; 238 default = null; 239 description = '' 240 Port to listen to. 241 ''; 242 }; 243 }; 244 }); 245 default = []; 246 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ]; 247 description = '' 248 List of addresses and ports to listen on (ListenAddress directive 249 in config). If port is not specified for address sshd will listen 250 on all ports specified by `ports` option. 251 NOTE: this will override default listening on all local addresses and port 22. 252 NOTE: setting this option won't automatically enable given ports 253 in firewall configuration. 254 ''; 255 }; 256 257 hostKeys = mkOption { 258 type = types.listOf types.attrs; 259 default = 260 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; } 261 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; } 262 ]; 263 example = 264 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; rounds = 100; openSSHFormat = true; } 265 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; rounds = 100; comment = "key comment"; } 266 ]; 267 description = '' 268 NixOS can automatically generate SSH host keys. This option 269 specifies the path, type and size of each key. See 270 {manpage}`ssh-keygen(1)` for supported types 271 and sizes. 272 ''; 273 }; 274 275 banner = mkOption { 276 type = types.nullOr types.lines; 277 default = null; 278 description = '' 279 Message to display to the remote user before authentication is allowed. 280 ''; 281 }; 282 283 authorizedKeysFiles = mkOption { 284 type = types.listOf types.str; 285 default = []; 286 description = '' 287 Specify the rules for which files to read on the host. 288 289 This is an advanced option. If you're looking to configure user 290 keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys) 291 or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles). 292 293 These are paths relative to the host root file system or home 294 directories and they are subject to certain token expansion rules. 295 See AuthorizedKeysFile in man sshd_config for details. 296 ''; 297 }; 298 299 authorizedKeysInHomedir = mkOption { 300 type = types.bool; 301 default = true; 302 description = '' 303 Enables the use of the `~/.ssh/authorized_keys` file. 304 305 Otherwise, the only files trusted by default are those in `/etc/ssh/authorized_keys.d`, 306 *i.e.* SSH keys from [](#opt-users.users._name_.openssh.authorizedKeys.keys). 307 ''; 308 }; 309 310 authorizedKeysCommand = mkOption { 311 type = types.str; 312 default = "none"; 313 description = '' 314 Specifies a program to be used to look up the user's public 315 keys. The program must be owned by root, not writable by group 316 or others and specified by an absolute path. 317 ''; 318 }; 319 320 authorizedKeysCommandUser = mkOption { 321 type = types.str; 322 default = "nobody"; 323 description = '' 324 Specifies the user under whose account the AuthorizedKeysCommand 325 is run. It is recommended to use a dedicated user that has no 326 other role on the host than running authorized keys commands. 327 ''; 328 }; 329 330 331 332 settings = mkOption { 333 description = "Configuration for `sshd_config(5)`."; 334 default = { }; 335 example = literalExpression '' 336 { 337 UseDns = true; 338 PasswordAuthentication = false; 339 } 340 ''; 341 type = types.submodule ({name, ...}: { 342 freeformType = settingsFormat.type; 343 options = { 344 AuthorizedPrincipalsFile = mkOption { 345 type = types.str; 346 default = "none"; # upstream default 347 description = '' 348 Specifies a file that lists principal names that are accepted for certificate authentication. The default 349 is `"none"`, i.e. not to use a principals file. 350 ''; 351 }; 352 LogLevel = mkOption { 353 type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ]; 354 default = "INFO"; # upstream default 355 description = '' 356 Gives the verbosity level that is used when logging messages from sshd(8). Logging with a DEBUG level 357 violates the privacy of users and is not recommended. 358 ''; 359 }; 360 UsePAM = mkEnableOption "PAM authentication" // { default = true; }; 361 UseDns = mkOption { 362 type = types.bool; 363 # apply if cfg.useDns then "yes" else "no" 364 default = false; 365 description = '' 366 Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for 367 the remote IP address maps back to the very same IP address. 368 If this option is set to no (the default) then only addresses and not host names may be used in 369 ~/.ssh/authorized_keys from and sshd_config Match Host directives. 370 ''; 371 }; 372 X11Forwarding = mkOption { 373 type = types.bool; 374 default = false; 375 description = '' 376 Whether to allow X11 connections to be forwarded. 377 ''; 378 }; 379 PasswordAuthentication = mkOption { 380 type = types.bool; 381 default = true; 382 description = '' 383 Specifies whether password authentication is allowed. 384 ''; 385 }; 386 PermitRootLogin = mkOption { 387 default = "prohibit-password"; 388 type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"]; 389 description = '' 390 Whether the root user can login using ssh. 391 ''; 392 }; 393 KbdInteractiveAuthentication = mkOption { 394 type = types.bool; 395 default = true; 396 description = '' 397 Specifies whether keyboard-interactive authentication is allowed. 398 ''; 399 }; 400 GatewayPorts = mkOption { 401 type = types.str; 402 default = "no"; 403 description = '' 404 Specifies whether remote hosts are allowed to connect to 405 ports forwarded for the client. See 406 {manpage}`sshd_config(5)`. 407 ''; 408 }; 409 KexAlgorithms = mkOption { 410 type = types.listOf types.str; 411 default = [ 412 "sntrup761x25519-sha512@openssh.com" 413 "curve25519-sha256" 414 "curve25519-sha256@libssh.org" 415 "diffie-hellman-group-exchange-sha256" 416 ]; 417 description = '' 418 Allowed key exchange algorithms 419 420 Uses the lower bound recommended in both 421 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 422 and 423 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 424 ''; 425 }; 426 Macs = mkOption { 427 type = types.listOf types.str; 428 default = [ 429 "hmac-sha2-512-etm@openssh.com" 430 "hmac-sha2-256-etm@openssh.com" 431 "umac-128-etm@openssh.com" 432 ]; 433 description = '' 434 Allowed MACs 435 436 Defaults to recommended settings from both 437 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 438 and 439 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 440 ''; 441 }; 442 StrictModes = mkOption { 443 type = types.bool; 444 default = true; 445 description = '' 446 Whether sshd should check file modes and ownership of directories 447 ''; 448 }; 449 Ciphers = mkOption { 450 type = types.listOf types.str; 451 default = [ 452 "chacha20-poly1305@openssh.com" 453 "aes256-gcm@openssh.com" 454 "aes128-gcm@openssh.com" 455 "aes256-ctr" 456 "aes192-ctr" 457 "aes128-ctr" 458 ]; 459 description = '' 460 Allowed ciphers 461 462 Defaults to recommended settings from both 463 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 464 and 465 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 466 ''; 467 }; 468 AllowUsers = mkOption { 469 type = with types; nullOr (listOf str); 470 default = null; 471 description = '' 472 If specified, login is allowed only for the listed users. 473 See {manpage}`sshd_config(5)` for details. 474 ''; 475 }; 476 DenyUsers = mkOption { 477 type = with types; nullOr (listOf str); 478 default = null; 479 description = '' 480 If specified, login is denied for all listed users. Takes 481 precedence over [](#opt-services.openssh.settings.AllowUsers). 482 See {manpage}`sshd_config(5)` for details. 483 ''; 484 }; 485 AllowGroups = mkOption { 486 type = with types; nullOr (listOf str); 487 default = null; 488 description = '' 489 If specified, login is allowed only for users part of the 490 listed groups. 491 See {manpage}`sshd_config(5)` for details. 492 ''; 493 }; 494 DenyGroups = mkOption { 495 type = with types; nullOr (listOf str); 496 default = null; 497 description = '' 498 If specified, login is denied for all users part of the listed 499 groups. Takes precedence over 500 [](#opt-services.openssh.settings.AllowGroups). See 501 {manpage}`sshd_config(5)` for details. 502 ''; 503 }; 504 # Disabled by default, since pam_motd handles this. 505 PrintMotd = mkEnableOption "printing /etc/motd when a user logs in interactively"; 506 }; 507 }); 508 }; 509 510 extraConfig = mkOption { 511 type = types.lines; 512 default = ""; 513 description = "Verbatim contents of {file}`sshd_config`."; 514 }; 515 516 moduliFile = mkOption { 517 example = "/etc/my-local-ssh-moduli;"; 518 type = types.path; 519 description = '' 520 Path to `moduli` file to install in 521 `/etc/ssh/moduli`. If this option is unset, then 522 the `moduli` file shipped with OpenSSH will be used. 523 ''; 524 }; 525 526 }; 527 528 users.users = mkOption { 529 type = with types; attrsOf (submodule userOptions); 530 }; 531 532 }; 533 534 535 ###### implementation 536 537 config = mkIf cfg.enable { 538 539 users.users.sshd = 540 { 541 isSystemUser = true; 542 group = "sshd"; 543 description = "SSH privilege separation user"; 544 }; 545 users.groups.sshd = {}; 546 547 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli"; 548 services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server"; 549 550 environment.etc = authKeysFiles // authPrincipalsFiles // 551 { "ssh/moduli".source = cfg.moduliFile; 552 "ssh/sshd_config".source = sshconf; 553 }; 554 555 systemd = 556 let 557 service = 558 { description = "SSH Daemon"; 559 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target"; 560 after = [ "network.target" ]; 561 stopIfChanged = false; 562 path = [ cfgc.package pkgs.gawk ]; 563 environment.LD_LIBRARY_PATH = nssModulesPath; 564 565 restartTriggers = optionals (!cfg.startWhenNeeded) [ 566 config.environment.etc."ssh/sshd_config".source 567 ]; 568 569 preStart = 570 '' 571 # Make sure we don't write to stdout, since in case of 572 # socket activation, it goes to the remote side (#19589). 573 exec >&2 574 575 ${flip concatMapStrings cfg.hostKeys (k: '' 576 if ! [ -s "${k.path}" ]; then 577 if ! [ -h "${k.path}" ]; then 578 rm -f "${k.path}" 579 fi 580 mkdir -m 0755 -p "$(dirname '${k.path}')" 581 ssh-keygen \ 582 -t "${k.type}" \ 583 ${optionalString (k ? bits) "-b ${toString k.bits}"} \ 584 ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \ 585 ${optionalString (k ? comment) "-C '${k.comment}'"} \ 586 ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \ 587 -f "${k.path}" \ 588 -N "" 589 fi 590 '')} 591 ''; 592 593 serviceConfig = 594 { ExecStart = 595 (optionalString cfg.startWhenNeeded "-") + 596 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") + 597 "-D " + # don't detach into a daemon process 598 "-f /etc/ssh/sshd_config"; 599 KillMode = "process"; 600 } // (if cfg.startWhenNeeded then { 601 StandardInput = "socket"; 602 StandardError = "journal"; 603 } else { 604 Restart = "always"; 605 Type = "simple"; 606 }); 607 608 }; 609 in 610 611 if cfg.startWhenNeeded then { 612 613 sockets.sshd = 614 { description = "SSH Socket"; 615 wantedBy = [ "sockets.target" ]; 616 socketConfig.ListenStream = if cfg.listenAddresses != [] then 617 concatMap 618 ({ addr, port }: 619 if port != null then [ "${addr}:${toString port}" ] 620 else map (p: "${addr}:${toString p}") cfg.ports) 621 cfg.listenAddresses 622 else 623 cfg.ports; 624 socketConfig.Accept = true; 625 # Prevent brute-force attacks from shutting down socket 626 socketConfig.TriggerLimitIntervalSec = 0; 627 }; 628 629 services."sshd@" = service; 630 631 } else { 632 633 services.sshd = service; 634 635 }; 636 637 networking.firewall.allowedTCPPorts = optionals cfg.openFirewall cfg.ports; 638 639 security.pam.services.sshd = lib.mkIf cfg.settings.UsePAM 640 { startSession = true; 641 showMotd = true; 642 unixAuth = cfg.settings.PasswordAuthentication; 643 }; 644 645 # These values are merged with the ones defined externally, see: 646 # https://github.com/NixOS/nixpkgs/pull/10155 647 # https://github.com/NixOS/nixpkgs/pull/41745 648 services.openssh.authorizedKeysFiles = 649 lib.optional cfg.authorizedKeysInHomedir "%h/.ssh/authorized_keys" ++ [ "/etc/ssh/authorized_keys.d/%u" ]; 650 651 services.openssh.settings.AuthorizedPrincipalsFile = mkIf (authPrincipalsFiles != {}) "/etc/ssh/authorized_principals.d/%u"; 652 653 services.openssh.extraConfig = mkOrder 0 654 '' 655 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner} 656 657 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"} 658 ${concatMapStrings (port: '' 659 Port ${toString port} 660 '') cfg.ports} 661 662 ${concatMapStrings ({ port, addr, ... }: '' 663 ListenAddress ${addr}${optionalString (port != null) (":" + toString port)} 664 '') cfg.listenAddresses} 665 666 ${optionalString cfgc.setXAuthLocation '' 667 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth 668 ''} 669 ${optionalString cfg.allowSFTP '' 670 Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags} 671 ''} 672 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} 673 ${optionalString (cfg.authorizedKeysCommand != "none") '' 674 AuthorizedKeysCommand ${cfg.authorizedKeysCommand} 675 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser} 676 ''} 677 678 ${flip concatMapStrings cfg.hostKeys (k: '' 679 HostKey ${k.path} 680 '')} 681 ''; 682 683 system.checks = [ 684 (pkgs.runCommand "check-sshd-config" 685 { 686 nativeBuildInputs = [ validationPackage ]; 687 } '' 688 ${concatMapStringsSep "\n" 689 (lport: "sshd -G -T -C lport=${toString lport} -f ${sshconf} > /dev/null") 690 cfg.ports} 691 ${concatMapStringsSep "\n" 692 (la: 693 concatMapStringsSep "\n" 694 (port: "sshd -G -T -C ${escapeShellArg "laddr=${la.addr},lport=${toString port}"} -f ${sshconf} > /dev/null") 695 (if la.port != null then [ la.port ] else cfg.ports) 696 ) 697 cfg.listenAddresses} 698 touch $out 699 '') 700 ]; 701 702 assertions = [{ assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true; 703 message = "cannot enable X11 forwarding without setting xauth location";} 704 (let 705 duplicates = 706 # Filter out the groups with more than 1 element 707 lib.filter (l: lib.length l > 1) ( 708 # Grab the groups, we don't care about the group identifiers 709 lib.attrValues ( 710 # Group the settings that are the same in lower case 711 lib.groupBy lib.strings.toLower (attrNames cfg.settings) 712 ) 713 ); 714 formattedDuplicates = lib.concatMapStringsSep ", " (dupl: "(${lib.concatStringsSep ", " dupl})") duplicates; 715 in 716 { 717 assertion = lib.length duplicates == 0; 718 message = ''Duplicate sshd config key; does your capitalization match the option's? Duplicate keys: ${formattedDuplicates}''; 719 })] 720 ++ forEach cfg.listenAddresses ({ addr, ... }: { 721 assertion = addr != null; 722 message = "addr must be specified in each listenAddresses entry"; 723 }); 724 }; 725 726}