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