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