at master 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 fortunately 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.port; 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 openSSHFormat = true; 370 } 371 { 372 type = "ed25519"; 373 path = "/etc/ssh/ssh_host_ed25519_key"; 374 comment = "key comment"; 375 } 376 ]; 377 description = '' 378 NixOS can automatically generate SSH host keys. This option 379 specifies the path, type and size of each key. See 380 {manpage}`ssh-keygen(1)` for supported types 381 and sizes. 382 ''; 383 }; 384 385 banner = lib.mkOption { 386 type = lib.types.nullOr lib.types.lines; 387 default = null; 388 description = '' 389 Message to display to the remote user before authentication is allowed. 390 ''; 391 }; 392 393 authorizedKeysFiles = lib.mkOption { 394 type = lib.types.listOf lib.types.str; 395 default = [ ]; 396 description = '' 397 Specify the rules for which files to read on the host. 398 399 This is an advanced option. If you're looking to configure user 400 keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys) 401 or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles). 402 403 These are paths relative to the host root file system or home 404 directories and they are subject to certain token expansion rules. 405 See AuthorizedKeysFile in man sshd_config for details. 406 ''; 407 }; 408 409 authorizedKeysInHomedir = lib.mkOption { 410 type = lib.types.bool; 411 default = true; 412 description = '' 413 Enables the use of the `~/.ssh/authorized_keys` file. 414 415 Otherwise, the only files trusted by default are those in `/etc/ssh/authorized_keys.d`, 416 *i.e.* SSH keys from [](#opt-users.users._name_.openssh.authorizedKeys.keys). 417 ''; 418 }; 419 420 authorizedKeysCommand = lib.mkOption { 421 type = lib.types.str; 422 default = "none"; 423 description = '' 424 Specifies a program to be used to look up the user's public 425 keys. The program must be owned by root, not writable by group 426 or others and specified by an absolute path. 427 ''; 428 }; 429 430 authorizedKeysCommandUser = lib.mkOption { 431 type = lib.types.str; 432 default = "nobody"; 433 description = '' 434 Specifies the user under whose account the AuthorizedKeysCommand 435 is run. It is recommended to use a dedicated user that has no 436 other role on the host than running authorized keys commands. 437 ''; 438 }; 439 440 settings = lib.mkOption { 441 description = "Configuration for `sshd_config(5)`."; 442 default = { }; 443 example = lib.literalExpression '' 444 { 445 UseDns = true; 446 PasswordAuthentication = false; 447 } 448 ''; 449 type = lib.types.submodule ( 450 { name, ... }: 451 { 452 freeformType = settingsFormat.type; 453 options = { 454 AuthorizedPrincipalsFile = lib.mkOption { 455 type = lib.types.nullOr lib.types.str; 456 default = "none"; # upstream default 457 description = '' 458 Specifies a file that lists principal names that are accepted for certificate authentication. The default 459 is `"none"`, i.e. not to use a principals file. 460 ''; 461 }; 462 LogLevel = lib.mkOption { 463 type = lib.types.nullOr ( 464 lib.types.enum [ 465 "QUIET" 466 "FATAL" 467 "ERROR" 468 "INFO" 469 "VERBOSE" 470 "DEBUG" 471 "DEBUG1" 472 "DEBUG2" 473 "DEBUG3" 474 ] 475 ); 476 default = "INFO"; # upstream default 477 description = '' 478 Gives the verbosity level that is used when logging messages from {manpage}`sshd(8)`. Logging with a DEBUG level 479 violates the privacy of users and is not recommended. 480 ''; 481 }; 482 UsePAM = lib.mkEnableOption "PAM authentication" // { 483 default = true; 484 type = lib.types.nullOr lib.types.bool; 485 }; 486 UseDns = lib.mkOption { 487 type = lib.types.nullOr lib.types.bool; 488 # apply if cfg.useDns then "yes" else "no" 489 default = false; 490 description = '' 491 Specifies whether {manpage}`sshd(8)` should look up the remote host name, and to check that the resolved host name for 492 the remote IP address maps back to the very same IP address. 493 If this option is set to no (the default) then only addresses and not host names may be used in 494 ~/.ssh/authorized_keys from and sshd_config Match Host directives. 495 ''; 496 }; 497 X11Forwarding = lib.mkOption { 498 type = lib.types.nullOr lib.types.bool; 499 default = false; 500 description = '' 501 Whether to allow X11 connections to be forwarded. 502 ''; 503 }; 504 PasswordAuthentication = lib.mkOption { 505 type = lib.types.nullOr lib.types.bool; 506 default = true; 507 description = '' 508 Specifies whether password authentication is allowed. 509 ''; 510 }; 511 PermitRootLogin = lib.mkOption { 512 default = "prohibit-password"; 513 type = lib.types.nullOr ( 514 lib.types.enum [ 515 "yes" 516 "without-password" 517 "prohibit-password" 518 "forced-commands-only" 519 "no" 520 ] 521 ); 522 description = '' 523 Whether the root user can login using ssh. 524 ''; 525 }; 526 KbdInteractiveAuthentication = lib.mkOption { 527 type = lib.types.nullOr lib.types.bool; 528 default = true; 529 description = '' 530 Specifies whether keyboard-interactive authentication is allowed. 531 ''; 532 }; 533 GatewayPorts = lib.mkOption { 534 type = lib.types.nullOr lib.types.str; 535 default = "no"; 536 description = '' 537 Specifies whether remote hosts are allowed to connect to 538 ports forwarded for the client. See 539 {manpage}`sshd_config(5)`. 540 ''; 541 }; 542 KexAlgorithms = lib.mkOption { 543 type = lib.types.nullOr (lib.types.listOf lib.types.str); 544 default = [ 545 "mlkem768x25519-sha256" 546 "sntrup761x25519-sha512" 547 "sntrup761x25519-sha512@openssh.com" 548 "curve25519-sha256" 549 "curve25519-sha256@libssh.org" 550 "diffie-hellman-group-exchange-sha256" 551 ]; 552 description = '' 553 Allowed key exchange algorithms 554 555 Uses the lower bound recommended in both 556 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 557 and 558 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 559 ''; 560 }; 561 Macs = lib.mkOption { 562 type = lib.types.nullOr (lib.types.listOf lib.types.str); 563 default = [ 564 "hmac-sha2-512-etm@openssh.com" 565 "hmac-sha2-256-etm@openssh.com" 566 "umac-128-etm@openssh.com" 567 ]; 568 description = '' 569 Allowed MACs 570 571 Defaults to recommended settings from both 572 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 573 and 574 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 575 ''; 576 }; 577 StrictModes = lib.mkOption { 578 type = lib.types.nullOr (lib.types.bool); 579 default = true; 580 description = '' 581 Whether sshd should check file modes and ownership of directories 582 ''; 583 }; 584 Ciphers = lib.mkOption { 585 type = lib.types.nullOr (lib.types.listOf lib.types.str); 586 default = [ 587 "chacha20-poly1305@openssh.com" 588 "aes256-gcm@openssh.com" 589 "aes128-gcm@openssh.com" 590 "aes256-ctr" 591 "aes192-ctr" 592 "aes128-ctr" 593 ]; 594 description = '' 595 Allowed ciphers 596 597 Defaults to recommended settings from both 598 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 599 and 600 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 601 ''; 602 }; 603 AllowUsers = lib.mkOption { 604 type = with lib.types; nullOr (listOf str); 605 default = null; 606 description = '' 607 If specified, login is allowed only for the listed users. 608 See {manpage}`sshd_config(5)` for details. 609 ''; 610 }; 611 DenyUsers = lib.mkOption { 612 type = with lib.types; nullOr (listOf str); 613 default = null; 614 description = '' 615 If specified, login is denied for all listed users. Takes 616 precedence over [](#opt-services.openssh.settings.AllowUsers). 617 See {manpage}`sshd_config(5)` for details. 618 ''; 619 }; 620 AllowGroups = lib.mkOption { 621 type = with lib.types; nullOr (listOf str); 622 default = null; 623 description = '' 624 If specified, login is allowed only for users part of the 625 listed groups. 626 See {manpage}`sshd_config(5)` for details. 627 ''; 628 }; 629 DenyGroups = lib.mkOption { 630 type = with lib.types; nullOr (listOf str); 631 default = null; 632 description = '' 633 If specified, login is denied for all users part of the listed 634 groups. Takes precedence over 635 [](#opt-services.openssh.settings.AllowGroups). See 636 {manpage}`sshd_config(5)` for details. 637 ''; 638 }; 639 # Disabled by default, since pam_motd handles this. 640 PrintMotd = lib.mkEnableOption "printing /etc/motd when a user logs in interactively" // { 641 type = lib.types.nullOr lib.types.bool; 642 }; 643 }; 644 } 645 ); 646 }; 647 648 extraConfig = lib.mkOption { 649 type = lib.types.lines; 650 default = ""; 651 description = "Verbatim contents of {file}`sshd_config`."; 652 }; 653 654 moduliFile = lib.mkOption { 655 example = "/etc/my-local-ssh-moduli;"; 656 type = lib.types.path; 657 description = '' 658 Path to `moduli` file to install in 659 `/etc/ssh/moduli`. If this option is unset, then 660 the `moduli` file shipped with OpenSSH will be used. 661 ''; 662 }; 663 664 }; 665 666 users.users = lib.mkOption { 667 type = with lib.types; attrsOf (submodule userOptions); 668 }; 669 670 }; 671 672 ###### implementation 673 674 config = lib.mkIf cfg.enable { 675 676 users.users.sshd = { 677 isSystemUser = true; 678 group = "sshd"; 679 description = "SSH privilege separation user"; 680 }; 681 users.groups.sshd = { }; 682 683 services.openssh.moduliFile = lib.mkDefault "${cfg.package}/etc/ssh/moduli"; 684 services.openssh.sftpServerExecutable = lib.mkDefault "${cfg.package}/libexec/sftp-server"; 685 686 environment.etc = 687 authKeysFiles 688 // authPrincipalsFiles 689 // { 690 "ssh/moduli".source = cfg.moduliFile; 691 "ssh/sshd_config".source = sshconf; 692 }; 693 694 systemd.tmpfiles.settings."ssh-root-provision" = { 695 "/root"."d-" = { 696 user = "root"; 697 group = ":root"; 698 mode = ":700"; 699 }; 700 "/root/.ssh"."d-" = { 701 user = "root"; 702 group = ":root"; 703 mode = ":700"; 704 }; 705 "/root/.ssh/authorized_keys"."f^" = { 706 user = "root"; 707 group = ":root"; 708 mode = ":600"; 709 argument = "ssh.authorized_keys.root"; 710 }; 711 }; 712 713 systemd = { 714 sockets.sshd = lib.mkIf cfg.startWhenNeeded { 715 description = "SSH Socket"; 716 wantedBy = [ "sockets.target" ]; 717 socketConfig.ListenStream = 718 if cfg.listenAddresses != [ ] then 719 lib.concatMap ( 720 { addr, port }: 721 if port != null then [ "${addr}:${toString port}" ] else map (p: "${addr}:${toString p}") cfg.ports 722 ) cfg.listenAddresses 723 else 724 cfg.ports; 725 socketConfig.Accept = true; 726 # Prevent brute-force attacks from shutting down socket 727 socketConfig.TriggerLimitIntervalSec = 0; 728 }; 729 730 services."sshd@" = { 731 description = "SSH per-connection Daemon"; 732 after = [ 733 "network.target" 734 "sshd-keygen.service" 735 ]; 736 wants = [ "sshd-keygen.service" ]; 737 stopIfChanged = false; 738 path = [ cfg.package ]; 739 environment.LD_LIBRARY_PATH = nssModulesPath; 740 741 serviceConfig = { 742 ExecStart = lib.concatStringsSep " " [ 743 "-${lib.getExe' cfg.package "sshd"}" 744 "-i" 745 "-D" 746 "-f /etc/ssh/sshd_config" 747 ]; 748 KillMode = "process"; 749 StandardInput = "socket"; 750 StandardError = "journal"; 751 }; 752 }; 753 754 services.sshd = lib.mkIf (!cfg.startWhenNeeded) { 755 description = "SSH Daemon"; 756 wantedBy = [ "multi-user.target" ]; 757 after = [ 758 "network.target" 759 "sshd-keygen.service" 760 ]; 761 wants = [ "sshd-keygen.service" ]; 762 stopIfChanged = false; 763 path = [ cfg.package ]; 764 environment.LD_LIBRARY_PATH = nssModulesPath; 765 766 restartTriggers = [ config.environment.etc."ssh/sshd_config".source ]; 767 768 serviceConfig = { 769 Type = "notify-reload"; 770 Restart = "always"; 771 ExecStart = lib.concatStringsSep " " [ 772 (lib.getExe' cfg.package "sshd") 773 "-D" 774 "-f" 775 "/etc/ssh/sshd_config" 776 ]; 777 KillMode = "process"; 778 }; 779 }; 780 781 services.sshd-keygen = { 782 description = "SSH Host Keys Generation"; 783 unitConfig = { 784 ConditionFileNotEmpty = map (k: "|!${k.path}") cfg.hostKeys; 785 }; 786 serviceConfig = { 787 Type = "oneshot"; 788 }; 789 path = [ cfg.package ]; 790 script = lib.flip lib.concatMapStrings cfg.hostKeys (k: '' 791 if ! [ -s "${k.path}" ]; then 792 if ! [ -h "${k.path}" ]; then 793 rm -f "${k.path}" 794 fi 795 mkdir -p "$(dirname '${k.path}')" 796 chmod 0755 "$(dirname '${k.path}')" 797 ssh-keygen \ 798 -t "${k.type}" \ 799 ${lib.optionalString (k ? bits) "-b ${toString k.bits}"} \ 800 ${lib.optionalString (k ? comment) "-C '${k.comment}'"} \ 801 ${lib.optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \ 802 -f "${k.path}" \ 803 -N "" 804 fi 805 ''); 806 }; 807 }; 808 809 networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall cfg.ports; 810 811 security.pam.services.sshd = lib.mkIf cfg.settings.UsePAM { 812 startSession = true; 813 showMotd = true; 814 unixAuth = if cfg.settings.PasswordAuthentication == true then true else false; 815 }; 816 817 # These values are merged with the ones defined externally, see: 818 # https://github.com/NixOS/nixpkgs/pull/10155 819 # https://github.com/NixOS/nixpkgs/pull/41745 820 services.openssh.authorizedKeysFiles = 821 lib.optional cfg.authorizedKeysInHomedir "%h/.ssh/authorized_keys" 822 ++ [ "/etc/ssh/authorized_keys.d/%u" ]; 823 824 services.openssh.settings.AuthorizedPrincipalsFile = lib.mkIf ( 825 authPrincipalsFiles != { } 826 ) "/etc/ssh/authorized_principals.d/%u"; 827 828 services.openssh.extraConfig = lib.mkOrder 0 '' 829 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner} 830 831 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"} 832 ${lib.concatMapStrings (port: '' 833 Port ${toString port} 834 '') cfg.ports} 835 836 ${lib.concatMapStrings ( 837 { port, addr, ... }: 838 '' 839 ListenAddress ${addr}${lib.optionalString (port != null) (":" + toString port)} 840 '' 841 ) cfg.listenAddresses} 842 843 ${lib.optionalString cfgc.setXAuthLocation '' 844 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth 845 ''} 846 ${lib.optionalString cfg.allowSFTP '' 847 Subsystem sftp ${cfg.sftpServerExecutable} ${lib.concatStringsSep " " cfg.sftpFlags} 848 ''} 849 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} 850 ${lib.optionalString (cfg.authorizedKeysCommand != "none") '' 851 AuthorizedKeysCommand ${cfg.authorizedKeysCommand} 852 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser} 853 ''} 854 855 ${lib.flip lib.concatMapStrings cfg.hostKeys (k: '' 856 HostKey ${k.path} 857 '')} 858 ''; 859 860 system.checks = [ 861 (pkgs.runCommand "check-sshd-config" 862 { 863 nativeBuildInputs = [ validationPackage ]; 864 } 865 '' 866 ${lib.concatMapStringsSep "\n" ( 867 lport: "sshd -G -T -C lport=${toString lport} -f ${sshconf} > /dev/null" 868 ) cfg.ports} 869 ${lib.concatMapStringsSep "\n" ( 870 la: 871 lib.concatMapStringsSep "\n" ( 872 port: 873 "sshd -G -T -C ${lib.escapeShellArg "laddr=${la.addr},lport=${toString port}"} -f ${sshconf} > /dev/null" 874 ) (if la.port != null then [ la.port ] else cfg.ports) 875 ) cfg.listenAddresses} 876 touch $out 877 '' 878 ) 879 ]; 880 881 assertions = [ 882 { 883 assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true; 884 message = "cannot enable X11 forwarding without setting xauth location"; 885 } 886 { 887 assertion = 888 (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}") 889 != null 890 -> cfgc.package.withKerberos; 891 message = "cannot enable Kerberos authentication without using a package with Kerberos support"; 892 } 893 { 894 assertion = 895 (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}") 896 != null 897 -> cfgc.package.withKerberos; 898 message = "cannot enable GSSAPI authentication without using a package with Kerberos support"; 899 } 900 ( 901 let 902 duplicates = 903 # Filter out the groups with more than 1 element 904 lib.filter (l: lib.length l > 1) ( 905 # Grab the groups, we don't care about the group identifiers 906 lib.attrValues ( 907 # Group the settings that are the same in lower case 908 lib.groupBy lib.strings.toLower (lib.attrNames cfg.settings) 909 ) 910 ); 911 formattedDuplicates = lib.concatMapStringsSep ", " ( 912 dupl: "(${lib.concatStringsSep ", " dupl})" 913 ) duplicates; 914 in 915 { 916 assertion = lib.length duplicates == 0; 917 message = ''Duplicate sshd config key; does your capitalization match the option's? Duplicate keys: ${formattedDuplicates}''; 918 } 919 ) 920 ] 921 ++ lib.forEach cfg.listenAddresses ( 922 { addr, ... }: 923 { 924 assertion = addr != null; 925 message = "addr must be specified in each listenAddresses entry"; 926 } 927 ); 928 }; 929 930}