at 23.05-pre 19 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 sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } '' 16 cat >$out <<EOL 17 ${cfg.extraConfig} 18 EOL 19 20 ssh-keygen -q -f mock-hostkey -N "" 21 sshd -t -f $out -h mock-hostkey 22 ''; 23 24 cfg = config.services.openssh; 25 cfgc = config.programs.ssh; 26 27 nssModulesPath = config.system.nssModules.path; 28 29 userOptions = { 30 31 options.openssh.authorizedKeys = { 32 keys = mkOption { 33 type = types.listOf types.singleLineStr; 34 default = []; 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 40 `keyFiles` options. 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. 43 ''; 44 example = [ 45 "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host" 46 "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar" 47 ]; 48 }; 49 50 keyFiles = mkOption { 51 type = types.listOf types.path; 52 default = []; 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. 59 ''; 60 }; 61 }; 62 63 }; 64 65 authKeysFiles = let 66 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" { 67 mode = "0444"; 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} 71 ''; 72 }; 73 usersWithKeys = attrValues (flip filterAttrs config.users.users (n: u: 74 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0 75 )); 76 in listToAttrs (map mkAuthKeyFile usersWithKeys); 77 78in 79 80{ 81 imports = [ 82 (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ]) 83 (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ]) 84 (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ]) 85 ]; 86 87 ###### interface 88 89 options = { 90 91 services.openssh = { 92 93 enable = mkOption { 94 type = types.bool; 95 default = false; 96 description = lib.mdDoc '' 97 Whether to enable the OpenSSH secure shell daemon, which 98 allows secure remote logins. 99 ''; 100 }; 101 102 startWhenNeeded = mkOption { 103 type = types.bool; 104 default = false; 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. 109 ''; 110 }; 111 112 forwardX11 = mkOption { 113 type = types.bool; 114 default = false; 115 description = lib.mdDoc '' 116 Whether to allow X11 connections to be forwarded. 117 ''; 118 }; 119 120 allowSFTP = mkOption { 121 type = types.bool; 122 default = true; 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 126 {command}`sshfs`. 127 ''; 128 }; 129 130 sftpServerExecutable = mkOption { 131 type = types.str; 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. 136 ''; 137 }; 138 139 sftpFlags = mkOption { 140 type = with types; listOf str; 141 default = []; 142 example = [ "-f AUTHPRIV" "-l INFO" ]; 143 description = lib.mdDoc '' 144 Commandline flags to add to sftp-server. 145 ''; 146 }; 147 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. 153 ''; 154 }; 155 156 gatewayPorts = mkOption { 157 type = types.str; 158 default = "no"; 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)`. 163 ''; 164 }; 165 166 ports = mkOption { 167 type = types.listOf types.port; 168 default = [22]; 169 description = lib.mdDoc '' 170 Specifies on which ports the SSH daemon listens. 171 ''; 172 }; 173 174 openFirewall = mkOption { 175 type = types.bool; 176 default = true; 177 description = lib.mdDoc '' 178 Whether to automatically open the specified ports in the firewall. 179 ''; 180 }; 181 182 listenAddresses = mkOption { 183 type = with types; listOf (submodule { 184 options = { 185 addr = mkOption { 186 type = types.nullOr types.str; 187 default = null; 188 description = lib.mdDoc '' 189 Host, IPv4 or IPv6 address to listen to. 190 ''; 191 }; 192 port = mkOption { 193 type = types.nullOr types.int; 194 default = null; 195 description = lib.mdDoc '' 196 Port to listen to. 197 ''; 198 }; 199 }; 200 }); 201 default = []; 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. 210 ''; 211 }; 212 213 passwordAuthentication = mkOption { 214 type = types.bool; 215 default = true; 216 description = lib.mdDoc '' 217 Specifies whether password authentication is allowed. 218 ''; 219 }; 220 221 kbdInteractiveAuthentication = mkOption { 222 type = types.bool; 223 default = true; 224 description = lib.mdDoc '' 225 Specifies whether keyboard-interactive authentication is allowed. 226 ''; 227 }; 228 229 hostKeys = mkOption { 230 type = types.listOf types.attrs; 231 default = 232 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; } 233 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; } 234 ]; 235 example = 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"; } 238 ]; 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 243 and sizes. 244 ''; 245 }; 246 247 banner = mkOption { 248 type = types.nullOr types.lines; 249 default = null; 250 description = lib.mdDoc '' 251 Message to display to the remote user before authentication is allowed. 252 ''; 253 }; 254 255 authorizedKeysFiles = mkOption { 256 type = types.listOf types.str; 257 default = []; 258 description = lib.mdDoc '' 259 Specify the rules for which files to read on the host. 260 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). 264 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. 268 ''; 269 }; 270 271 authorizedKeysCommand = mkOption { 272 type = types.str; 273 default = "none"; 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. 278 ''; 279 }; 280 281 authorizedKeysCommandUser = mkOption { 282 type = types.str; 283 default = "nobody"; 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. 288 ''; 289 }; 290 291 kexAlgorithms = mkOption { 292 type = types.listOf types.str; 293 default = [ 294 "sntrup761x25519-sha512@openssh.com" 295 "curve25519-sha256" 296 "curve25519-sha256@libssh.org" 297 "diffie-hellman-group-exchange-sha256" 298 ]; 299 description = lib.mdDoc '' 300 Allowed key exchange algorithms 301 302 Uses the lower bound recommended in both 303 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 304 and 305 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 306 ''; 307 }; 308 309 ciphers = mkOption { 310 type = types.listOf types.str; 311 default = [ 312 "chacha20-poly1305@openssh.com" 313 "aes256-gcm@openssh.com" 314 "aes128-gcm@openssh.com" 315 "aes256-ctr" 316 "aes192-ctr" 317 "aes128-ctr" 318 ]; 319 description = lib.mdDoc '' 320 Allowed ciphers 321 322 Defaults to recommended settings from both 323 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 324 and 325 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 326 ''; 327 }; 328 329 macs = mkOption { 330 type = types.listOf types.str; 331 default = [ 332 "hmac-sha2-512-etm@openssh.com" 333 "hmac-sha2-256-etm@openssh.com" 334 "umac-128-etm@openssh.com" 335 "hmac-sha2-512" 336 "hmac-sha2-256" 337 "umac-128@openssh.com" 338 ]; 339 description = lib.mdDoc '' 340 Allowed MACs 341 342 Defaults to recommended settings from both 343 <https://stribika.github.io/2015/01/04/secure-secure-shell.html> 344 and 345 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> 346 ''; 347 }; 348 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. 357 ''; 358 }; 359 360 useDns = mkOption { 361 type = types.bool; 362 default = false; 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. 368 ''; 369 }; 370 371 extraConfig = mkOption { 372 type = types.lines; 373 default = ""; 374 description = lib.mdDoc "Verbatim contents of {file}`sshd_config`."; 375 }; 376 377 moduliFile = mkOption { 378 example = "/etc/my-local-ssh-moduli;"; 379 type = types.path; 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. 384 ''; 385 }; 386 387 }; 388 389 users.users = mkOption { 390 type = with types; attrsOf (submodule userOptions); 391 }; 392 393 }; 394 395 396 ###### implementation 397 398 config = mkIf cfg.enable { 399 400 users.users.sshd = 401 { 402 isSystemUser = true; 403 group = "sshd"; 404 description = "SSH privilege separation user"; 405 }; 406 users.groups.sshd = {}; 407 408 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli"; 409 services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server"; 410 411 environment.etc = authKeysFiles // 412 { "ssh/moduli".source = cfg.moduliFile; 413 "ssh/sshd_config".source = sshconf; 414 }; 415 416 systemd = 417 let 418 service = 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; 425 426 restartTriggers = optionals (!cfg.startWhenNeeded) [ 427 config.environment.etc."ssh/sshd_config".source 428 ]; 429 430 preStart = 431 '' 432 # Make sure we don't write to stdout, since in case of 433 # socket activation, it goes to the remote side (#19589). 434 exec >&2 435 436 ${flip concatMapStrings cfg.hostKeys (k: '' 437 if ! [ -s "${k.path}" ]; then 438 if ! [ -h "${k.path}" ]; then 439 rm -f "${k.path}" 440 fi 441 mkdir -m 0755 -p "$(dirname '${k.path}')" 442 ssh-keygen \ 443 -t "${k.type}" \ 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 ""} \ 448 -f "${k.path}" \ 449 -N "" 450 fi 451 '')} 452 ''; 453 454 serviceConfig = 455 { ExecStart = 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"; 464 } else { 465 Restart = "always"; 466 Type = "simple"; 467 }); 468 469 }; 470 in 471 472 if cfg.startWhenNeeded then { 473 474 sockets.sshd = 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 479 else 480 cfg.ports; 481 socketConfig.Accept = true; 482 # Prevent brute-force attacks from shutting down socket 483 socketConfig.TriggerLimitIntervalSec = 0; 484 }; 485 486 services."sshd@" = service; 487 488 } else { 489 490 services.sshd = service; 491 492 }; 493 494 networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else []; 495 496 security.pam.services.sshd = 497 { startSession = true; 498 showMotd = true; 499 unixAuth = cfg.passwordAuthentication; 500 }; 501 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" ]; 507 508 services.openssh.extraConfig = mkOrder 0 509 '' 510 UsePAM yes 511 512 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner} 513 514 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"} 515 ${concatMapStrings (port: '' 516 Port ${toString port} 517 '') cfg.ports} 518 519 ${concatMapStrings ({ port, addr, ... }: '' 520 ListenAddress ${addr}${if port != null then ":" + toString port else ""} 521 '') cfg.listenAddresses} 522 523 ${optionalString cfgc.setXAuthLocation '' 524 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth 525 ''} 526 527 X11Forwarding ${if cfg.forwardX11 then "yes" else "no"} 528 529 ${optionalString cfg.allowSFTP '' 530 Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags} 531 ''} 532 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"} 537 538 PrintMotd no # handled by pam_motd 539 540 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} 541 ${optionalString (cfg.authorizedKeysCommand != "none") '' 542 AuthorizedKeysCommand ${cfg.authorizedKeysCommand} 543 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser} 544 ''} 545 546 ${flip concatMapStrings cfg.hostKeys (k: '' 547 HostKey ${k.path} 548 '')} 549 550 KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms} 551 Ciphers ${concatStringsSep "," cfg.ciphers} 552 MACs ${concatStringsSep "," cfg.macs} 553 554 LogLevel ${cfg.logLevel} 555 556 UseDNS ${if cfg.useDns then "yes" else "no"} 557 558 ''; 559 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"; 565 }); 566 567 }; 568 569}