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