1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.services.openssh; 8 cfgc = config.programs.ssh; 9 10 nssModulesPath = config.system.nssModules.path; 11 12 userOptions = { 13 14 openssh.authorizedKeys = { 15 keys = mkOption { 16 type = types.listOf types.str; 17 default = []; 18 description = '' 19 A list of verbatim OpenSSH public keys that should be added to the 20 user's authorized keys. The keys are added to a file that the SSH 21 daemon reads in addition to the the user's authorized_keys file. 22 You can combine the <literal>keys</literal> and 23 <literal>keyFiles</literal> options. 24 ''; 25 }; 26 27 keyFiles = mkOption { 28 type = types.listOf types.path; 29 default = []; 30 description = '' 31 A list of files each containing one OpenSSH public key that should be 32 added to the user's authorized keys. The contents of the files are 33 read at build time and added to a file that the SSH daemon reads in 34 addition to the the user's authorized_keys file. You can combine the 35 <literal>keyFiles</literal> and <literal>keys</literal> options. 36 ''; 37 }; 38 }; 39 40 }; 41 42 authKeysFiles = let 43 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" { 44 mode = "0444"; 45 source = pkgs.writeText "${u.name}-authorized_keys" '' 46 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys} 47 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles} 48 ''; 49 }; 50 usersWithKeys = attrValues (flip filterAttrs config.users.extraUsers (n: u: 51 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0 52 )); 53 in listToAttrs (map mkAuthKeyFile usersWithKeys); 54 55 supportOldHostKeys = !versionAtLeast config.system.stateVersion "15.07"; 56 57in 58 59{ 60 61 ###### interface 62 63 options = { 64 65 services.openssh = { 66 67 enable = mkOption { 68 type = types.bool; 69 default = false; 70 description = '' 71 Whether to enable the OpenSSH secure shell daemon, which 72 allows secure remote logins. 73 ''; 74 }; 75 76 startWhenNeeded = mkOption { 77 type = types.bool; 78 default = false; 79 description = '' 80 If set, <command>sshd</command> is socket-activated; that 81 is, instead of having it permanently running as a daemon, 82 systemd will start an instance for each incoming connection. 83 ''; 84 }; 85 86 forwardX11 = mkOption { 87 type = types.bool; 88 default = cfgc.setXAuthLocation; 89 description = '' 90 Whether to allow X11 connections to be forwarded. 91 ''; 92 }; 93 94 allowSFTP = mkOption { 95 type = types.bool; 96 default = true; 97 description = '' 98 Whether to enable the SFTP subsystem in the SSH daemon. This 99 enables the use of commands such as <command>sftp</command> and 100 <command>sshfs</command>. 101 ''; 102 }; 103 104 permitRootLogin = mkOption { 105 default = "without-password"; 106 type = types.enum ["yes" "without-password" "forced-commands-only" "no"]; 107 description = '' 108 Whether the root user can login using ssh. 109 ''; 110 }; 111 112 gatewayPorts = mkOption { 113 type = types.str; 114 default = "no"; 115 description = '' 116 Specifies whether remote hosts are allowed to connect to 117 ports forwarded for the client. See 118 <citerefentry><refentrytitle>sshd_config</refentrytitle> 119 <manvolnum>5</manvolnum></citerefentry>. 120 ''; 121 }; 122 123 ports = mkOption { 124 type = types.listOf types.int; 125 default = [22]; 126 description = '' 127 Specifies on which ports the SSH daemon listens. 128 ''; 129 }; 130 131 listenAddresses = mkOption { 132 type = types.listOf types.optionSet; 133 default = []; 134 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ]; 135 description = '' 136 List of addresses and ports to listen on (ListenAddress directive 137 in config). If port is not specified for address sshd will listen 138 on all ports specified by <literal>ports</literal> option. 139 NOTE: this will override default listening on all local addresses and port 22. 140 NOTE: setting this option won't automatically enable given ports 141 in firewall configuration. 142 ''; 143 options = { 144 addr = mkOption { 145 type = types.nullOr types.str; 146 default = null; 147 description = '' 148 Host, IPv4 or IPv6 address to listen to. 149 ''; 150 }; 151 port = mkOption { 152 type = types.nullOr types.int; 153 default = null; 154 description = '' 155 Port to listen to. 156 ''; 157 }; 158 }; 159 }; 160 161 passwordAuthentication = mkOption { 162 type = types.bool; 163 default = true; 164 description = '' 165 Specifies whether password authentication is allowed. 166 ''; 167 }; 168 169 challengeResponseAuthentication = mkOption { 170 type = types.bool; 171 default = true; 172 description = '' 173 Specifies whether challenge/response authentication is allowed. 174 ''; 175 }; 176 177 hostKeys = mkOption { 178 type = types.listOf types.attrs; 179 default = 180 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; } 181 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; } 182 ] ++ optionals supportOldHostKeys 183 [ { type = "dsa"; path = "/etc/ssh/ssh_host_dsa_key"; } 184 { type = "ecdsa"; bits = 521; path = "/etc/ssh/ssh_host_ecdsa_key"; } 185 ]; 186 description = '' 187 NixOS can automatically generate SSH host keys. This option 188 specifies the path, type and size of each key. See 189 <citerefentry><refentrytitle>ssh-keygen</refentrytitle> 190 <manvolnum>1</manvolnum></citerefentry> for supported types 191 and sizes. 192 ''; 193 }; 194 195 authorizedKeysFiles = mkOption { 196 type = types.listOf types.str; 197 default = []; 198 description = "Files from with authorized keys are read."; 199 }; 200 201 extraConfig = mkOption { 202 type = types.lines; 203 default = ""; 204 description = "Verbatim contents of <filename>sshd_config</filename>."; 205 }; 206 207 moduliFile = mkOption { 208 example = "services.openssh.moduliFile = /etc/my-local-ssh-moduli;"; 209 type = types.path; 210 description = '' 211 Path to <literal>moduli</literal> file to install in 212 <literal>/etc/ssh/moduli</literal>. If this option is unset, then 213 the <literal>moduli</literal> file shipped with OpenSSH will be used. 214 ''; 215 }; 216 217 }; 218 219 users.users = mkOption { 220 options = [ userOptions ]; 221 }; 222 223 }; 224 225 226 ###### implementation 227 228 config = mkIf cfg.enable { 229 230 users.extraUsers.sshd = 231 { isSystemUser = true; 232 description = "SSH privilege separation user"; 233 }; 234 235 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli"; 236 237 environment.etc = authKeysFiles // 238 { "ssh/moduli".source = cfg.moduliFile; }; 239 240 systemd = 241 let 242 service = 243 { description = "SSH Daemon"; 244 245 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target"; 246 247 stopIfChanged = false; 248 249 path = [ cfgc.package pkgs.gawk ]; 250 251 environment.LD_LIBRARY_PATH = nssModulesPath; 252 253 preStart = 254 '' 255 mkdir -m 0755 -p /etc/ssh 256 257 ${flip concatMapStrings cfg.hostKeys (k: '' 258 if ! [ -f "${k.path}" ]; then 259 ssh-keygen -t "${k.type}" ${if k ? bits then "-b ${toString k.bits}" else ""} -f "${k.path}" -N "" 260 fi 261 '')} 262 ''; 263 264 serviceConfig = 265 { ExecStart = 266 "${cfgc.package}/sbin/sshd " + (optionalString cfg.startWhenNeeded "-i ") + 267 "-f ${pkgs.writeText "sshd_config" cfg.extraConfig}"; 268 KillMode = "process"; 269 } // (if cfg.startWhenNeeded then { 270 StandardInput = "socket"; 271 } else { 272 Restart = "always"; 273 Type = "forking"; 274 PIDFile = "/run/sshd.pid"; 275 }); 276 }; 277 in 278 279 if cfg.startWhenNeeded then { 280 281 sockets.sshd = 282 { description = "SSH Socket"; 283 wantedBy = [ "sockets.target" ]; 284 socketConfig.ListenStream = cfg.ports; 285 socketConfig.Accept = true; 286 }; 287 288 services."sshd@" = service; 289 290 } else { 291 292 services.sshd = service; 293 294 }; 295 296 networking.firewall.allowedTCPPorts = cfg.ports; 297 298 security.pam.services.sshd = 299 { startSession = true; 300 showMotd = true; 301 unixAuth = cfg.passwordAuthentication; 302 }; 303 304 services.openssh.authorizedKeysFiles = 305 [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ]; 306 307 services.openssh.extraConfig = mkOrder 0 308 '' 309 PidFile /run/sshd.pid 310 311 Protocol 2 312 313 UsePAM yes 314 315 UsePrivilegeSeparation sandbox 316 317 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"} 318 ${concatMapStrings (port: '' 319 Port ${toString port} 320 '') cfg.ports} 321 322 ${concatMapStrings ({ port, addr, ... }: '' 323 ListenAddress ${addr}${if port != null then ":" + toString port else ""} 324 '') cfg.listenAddresses} 325 326 ${optionalString cfgc.setXAuthLocation '' 327 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth 328 ''} 329 330 ${if cfg.forwardX11 then '' 331 X11Forwarding yes 332 '' else '' 333 X11Forwarding no 334 ''} 335 336 ${optionalString cfg.allowSFTP '' 337 Subsystem sftp ${cfgc.package}/libexec/sftp-server 338 ''} 339 340 PermitRootLogin ${cfg.permitRootLogin} 341 GatewayPorts ${cfg.gatewayPorts} 342 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"} 343 ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"} 344 345 PrintMotd no # handled by pam_motd 346 347 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles} 348 349 ${flip concatMapStrings cfg.hostKeys (k: '' 350 HostKey ${k.path} 351 '')} 352 353 # Allow DSA client keys for now. (These were deprecated 354 # in OpenSSH 7.0.) 355 PubkeyAcceptedKeyTypes +ssh-dss 356 357 # Re-enable DSA host keys for now. 358 ${optionalString supportOldHostKeys '' 359 HostKeyAlgorithms +ssh-dss 360 ''} 361 ''; 362 363 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true; 364 message = "cannot enable X11 forwarding without setting xauth location";}] 365 ++ flip map cfg.listenAddresses ({ addr, port, ... }: { 366 assertion = addr != null; 367 message = "addr must be specified in each listenAddresses entry"; 368 }); 369 370 }; 371 372}