at 18.03-beta 20 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.nginx; 7 virtualHosts = mapAttrs (vhostName: vhostConfig: 8 let 9 serverName = if vhostConfig.serverName != null 10 then vhostConfig.serverName 11 else vhostName; 12 in 13 vhostConfig // { 14 inherit serverName; 15 } // (optionalAttrs vhostConfig.enableACME { 16 sslCertificate = "/var/lib/acme/${serverName}/fullchain.pem"; 17 sslCertificateKey = "/var/lib/acme/${serverName}/key.pem"; 18 }) // (optionalAttrs (vhostConfig.useACMEHost != null) { 19 sslCertificate = "/var/lib/acme/${vhostConfig.useACMEHost}/fullchain.pem"; 20 sslCertificateKey = "/var/lib/acme/${vhostConfig.useACMEHost}/key.pem"; 21 }) 22 ) cfg.virtualHosts; 23 enableIPv6 = config.networking.enableIPv6; 24 25 recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" '' 26 proxy_set_header Host $host; 27 proxy_set_header X-Real-IP $remote_addr; 28 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 proxy_set_header X-Forwarded-Proto $scheme; 30 proxy_set_header X-Forwarded-Host $host; 31 proxy_set_header X-Forwarded-Server $host; 32 proxy_set_header Accept-Encoding ""; 33 ''; 34 35 upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: '' 36 upstream ${name} { 37 ${toString (flip mapAttrsToList upstream.servers (name: server: '' 38 server ${name} ${optionalString server.backup "backup"}; 39 ''))} 40 } 41 '')); 42 43 configFile = pkgs.writeText "nginx.conf" '' 44 user ${cfg.user} ${cfg.group}; 45 error_log stderr; 46 daemon off; 47 48 ${cfg.config} 49 50 ${optionalString (cfg.eventsConfig != "" || cfg.config == "") '' 51 events { 52 ${cfg.eventsConfig} 53 } 54 ''} 55 56 ${optionalString (cfg.httpConfig == "" && cfg.config == "") '' 57 http { 58 include ${cfg.package}/conf/mime.types; 59 include ${cfg.package}/conf/fastcgi.conf; 60 include ${cfg.package}/conf/uwsgi_params; 61 62 ${optionalString (cfg.resolver.addresses != []) '' 63 resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"}; 64 ''} 65 ${upstreamConfig} 66 67 ${optionalString (cfg.recommendedOptimisation) '' 68 # optimisation 69 sendfile on; 70 tcp_nopush on; 71 tcp_nodelay on; 72 keepalive_timeout 65; 73 types_hash_max_size 2048; 74 ''} 75 76 ssl_protocols ${cfg.sslProtocols}; 77 ssl_ciphers ${cfg.sslCiphers}; 78 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"} 79 80 ${optionalString (cfg.recommendedTlsSettings) '' 81 ssl_session_cache shared:SSL:42m; 82 ssl_session_timeout 23m; 83 ssl_ecdh_curve secp384r1; 84 ssl_prefer_server_ciphers on; 85 ssl_stapling on; 86 ssl_stapling_verify on; 87 ''} 88 89 ${optionalString (cfg.recommendedGzipSettings) '' 90 gzip on; 91 gzip_disable "msie6"; 92 gzip_proxied any; 93 gzip_comp_level 9; 94 gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 95 gzip_vary on; 96 ''} 97 98 ${optionalString (cfg.recommendedProxySettings) '' 99 proxy_redirect off; 100 proxy_connect_timeout 90; 101 proxy_send_timeout 90; 102 proxy_read_timeout 90; 103 proxy_http_version 1.0; 104 include ${recommendedProxyConfig}; 105 ''} 106 107 # $connection_upgrade is used for websocket proxying 108 map $http_upgrade $connection_upgrade { 109 default upgrade; 110 ''' close; 111 } 112 client_max_body_size ${cfg.clientMaxBodySize}; 113 114 server_tokens ${if cfg.serverTokens then "on" else "off"}; 115 116 ${cfg.commonHttpConfig} 117 118 ${vhosts} 119 120 ${optionalString cfg.statusPage '' 121 server { 122 listen 80; 123 ${optionalString enableIPv6 "listen [::]:80;" } 124 125 server_name localhost; 126 127 location /nginx_status { 128 stub_status on; 129 access_log off; 130 allow 127.0.0.1; 131 ${optionalString enableIPv6 "allow ::1;"} 132 deny all; 133 } 134 } 135 ''} 136 137 ${cfg.appendHttpConfig} 138 }''} 139 140 ${optionalString (cfg.httpConfig != "") '' 141 http { 142 include ${cfg.package}/conf/mime.types; 143 include ${cfg.package}/conf/fastcgi.conf; 144 include ${cfg.package}/conf/uwsgi_params; 145 ${cfg.httpConfig} 146 }''} 147 148 ${cfg.appendConfig} 149 ''; 150 151 vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost: 152 let 153 onlySSL = vhost.onlySSL || vhost.enableSSL; 154 hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL; 155 156 defaultListen = 157 if vhost.listen != [] then vhost.listen 158 else ((optionals hasSSL ( 159 singleton { addr = "0.0.0.0"; port = 443; ssl = true; } 160 ++ optional enableIPv6 { addr = "[::]"; port = 443; ssl = true; } 161 )) ++ optionals (!onlySSL) ( 162 singleton { addr = "0.0.0.0"; port = 80; ssl = false; } 163 ++ optional enableIPv6 { addr = "[::]"; port = 80; ssl = false; } 164 )); 165 166 hostListen = 167 if vhost.forceSSL 168 then filter (x: x.ssl) defaultListen 169 else defaultListen; 170 171 listenString = { addr, port, ssl, ... }: 172 "listen ${addr}:${toString port} " 173 + optionalString ssl "ssl " 174 + optionalString (ssl && vhost.http2) "http2 " 175 + optionalString vhost.default "default_server " 176 + ";"; 177 178 redirectListen = filter (x: !x.ssl) defaultListen; 179 180 acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) '' 181 location /.well-known/acme-challenge { 182 ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"} 183 root ${vhost.acmeRoot}; 184 auth_basic off; 185 } 186 ${optionalString (vhost.acmeFallbackHost != null) '' 187 location @acme-fallback { 188 auth_basic off; 189 proxy_pass http://${vhost.acmeFallbackHost}; 190 } 191 ''} 192 ''; 193 194 in '' 195 ${optionalString vhost.forceSSL '' 196 server { 197 ${concatMapStringsSep "\n" listenString redirectListen} 198 199 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; 200 ${acmeLocation} 201 location / { 202 return 301 https://$host$request_uri; 203 } 204 } 205 ''} 206 207 server { 208 ${concatMapStringsSep "\n" listenString hostListen} 209 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; 210 ${acmeLocation} 211 ${optionalString (vhost.root != null) "root ${vhost.root};"} 212 ${optionalString (vhost.globalRedirect != null) '' 213 return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri; 214 ''} 215 ${optionalString hasSSL '' 216 ssl_certificate ${vhost.sslCertificate}; 217 ssl_certificate_key ${vhost.sslCertificateKey}; 218 ''} 219 220 ${optionalString (vhost.basicAuth != {}) (mkBasicAuth vhostName vhost.basicAuth)} 221 222 ${mkLocations vhost.locations} 223 224 ${vhost.extraConfig} 225 } 226 '' 227 ) virtualHosts); 228 mkLocations = locations: concatStringsSep "\n" (mapAttrsToList (location: config: '' 229 location ${location} { 230 ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning) 231 "proxy_pass ${config.proxyPass};" 232 } 233 ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) '' 234 set $nix_proxy_target "${config.proxyPass}"; 235 proxy_pass $nix_proxy_target; 236 ''} 237 ${optionalString config.proxyWebsockets '' 238 proxy_http_version 1.1; 239 proxy_set_header Upgrade $http_upgrade; 240 proxy_set_header Connection $connection_upgrade; 241 ''} 242 ${optionalString (config.index != null) "index ${config.index};"} 243 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"} 244 ${optionalString (config.root != null) "root ${config.root};"} 245 ${optionalString (config.alias != null) "alias ${config.alias};"} 246 ${config.extraConfig} 247 ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"} 248 } 249 '') locations); 250 mkBasicAuth = vhostName: authDef: let 251 htpasswdFile = pkgs.writeText "${vhostName}.htpasswd" ( 252 concatStringsSep "\n" (mapAttrsToList (user: password: '' 253 ${user}:{PLAIN}${password} 254 '') authDef) 255 ); 256 in '' 257 auth_basic secured; 258 auth_basic_user_file ${htpasswdFile}; 259 ''; 260in 261 262{ 263 options = { 264 services.nginx = { 265 enable = mkEnableOption "Nginx Web Server"; 266 267 statusPage = mkOption { 268 default = false; 269 type = types.bool; 270 description = " 271 Enable status page reachable from localhost on http://127.0.0.1/nginx_status. 272 "; 273 }; 274 275 recommendedTlsSettings = mkOption { 276 default = false; 277 type = types.bool; 278 description = " 279 Enable recommended TLS settings. 280 "; 281 }; 282 283 recommendedOptimisation = mkOption { 284 default = false; 285 type = types.bool; 286 description = " 287 Enable recommended optimisation settings. 288 "; 289 }; 290 291 recommendedGzipSettings = mkOption { 292 default = false; 293 type = types.bool; 294 description = " 295 Enable recommended gzip settings. 296 "; 297 }; 298 299 recommendedProxySettings = mkOption { 300 default = false; 301 type = types.bool; 302 description = " 303 Enable recommended proxy settings. 304 "; 305 }; 306 307 package = mkOption { 308 default = pkgs.nginxStable; 309 defaultText = "pkgs.nginxStable"; 310 type = types.package; 311 description = " 312 Nginx package to use. This defaults to the stable version. Note 313 that the nginx team recommends to use the mainline version which 314 available in nixpkgs as <literal>nginxMainline</literal>. 315 "; 316 }; 317 318 config = mkOption { 319 default = ""; 320 description = " 321 Verbatim nginx.conf configuration. 322 This is mutually exclusive with the structured configuration 323 via virtualHosts and the recommendedXyzSettings configuration 324 options. See appendConfig for appending to the generated http block. 325 "; 326 }; 327 328 appendConfig = mkOption { 329 type = types.lines; 330 default = ""; 331 description = '' 332 Configuration lines appended to the generated Nginx 333 configuration file. Commonly used by different modules 334 providing http snippets. <option>appendConfig</option> 335 can be specified more than once and it's value will be 336 concatenated (contrary to <option>config</option> which 337 can be set only once). 338 ''; 339 }; 340 341 commonHttpConfig = mkOption { 342 type = types.lines; 343 default = ""; 344 example = '' 345 resolver 127.0.0.1 valid=5s; 346 347 log_format myformat '$remote_addr - $remote_user [$time_local] ' 348 '"$request" $status $body_bytes_sent ' 349 '"$http_referer" "$http_user_agent"'; 350 ''; 351 description = '' 352 With nginx you must provide common http context definitions before 353 they are used, e.g. log_format, resolver, etc. inside of server 354 or location contexts. Use this attribute to set these definitions 355 at the appropriate location. 356 ''; 357 }; 358 359 httpConfig = mkOption { 360 type = types.lines; 361 default = ""; 362 description = " 363 Configuration lines to be set inside the http block. 364 This is mutually exclusive with the structured configuration 365 via virtualHosts and the recommendedXyzSettings configuration 366 options. See appendHttpConfig for appending to the generated http block. 367 "; 368 }; 369 370 eventsConfig = mkOption { 371 type = types.lines; 372 default = ""; 373 description = '' 374 Configuration lines to be set inside the events block. 375 ''; 376 }; 377 378 appendHttpConfig = mkOption { 379 type = types.lines; 380 default = ""; 381 description = " 382 Configuration lines to be appended to the generated http block. 383 This is mutually exclusive with using config and httpConfig for 384 specifying the whole http block verbatim. 385 "; 386 }; 387 388 stateDir = mkOption { 389 default = "/var/spool/nginx"; 390 description = " 391 Directory holding all state for nginx to run. 392 "; 393 }; 394 395 user = mkOption { 396 type = types.str; 397 default = "nginx"; 398 description = "User account under which nginx runs."; 399 }; 400 401 group = mkOption { 402 type = types.str; 403 default = "nginx"; 404 description = "Group account under which nginx runs."; 405 }; 406 407 serverTokens = mkOption { 408 type = types.bool; 409 default = false; 410 description = "Show nginx version in headers and error pages."; 411 }; 412 413 clientMaxBodySize = mkOption { 414 type = types.string; 415 default = "10m"; 416 description = "Set nginx global client_max_body_size."; 417 }; 418 419 sslCiphers = mkOption { 420 type = types.str; 421 default = "EECDH+aRSA+AESGCM:EDH+aRSA:EECDH+aRSA:+AES256:+AES128:+SHA1:!CAMELLIA:!SEED:!3DES:!DES:!RC4:!eNULL"; 422 description = "Ciphers to choose from when negotiating tls handshakes."; 423 }; 424 425 sslProtocols = mkOption { 426 type = types.str; 427 default = "TLSv1.2"; 428 example = "TLSv1 TLSv1.1 TLSv1.2"; 429 description = "Allowed TLS protocol versions."; 430 }; 431 432 sslDhparam = mkOption { 433 type = types.nullOr types.path; 434 default = null; 435 example = "/path/to/dhparams.pem"; 436 description = "Path to DH parameters file."; 437 }; 438 439 proxyResolveWhileRunning = mkOption { 440 type = types.bool; 441 default = false; 442 description = '' 443 Resolves domains of proxyPass targets at runtime 444 and not only at start, you have to set 445 services.nginx.resolver, too. 446 ''; 447 }; 448 449 resolver = mkOption { 450 type = types.submodule { 451 options = { 452 addresses = mkOption { 453 type = types.listOf types.str; 454 default = []; 455 example = literalExample ''[ "[::1]" "127.0.0.1:5353" ]''; 456 description = "List of resolvers to use"; 457 }; 458 valid = mkOption { 459 type = types.str; 460 default = ""; 461 example = "30s"; 462 description = '' 463 By default, nginx caches answers using the TTL value of a response. 464 An optional valid parameter allows overriding it 465 ''; 466 }; 467 }; 468 }; 469 description = '' 470 Configures name servers used to resolve names of upstream servers into addresses 471 ''; 472 default = {}; 473 }; 474 475 upstreams = mkOption { 476 type = types.attrsOf (types.submodule { 477 options = { 478 servers = mkOption { 479 type = types.attrsOf (types.submodule { 480 options = { 481 backup = mkOption { 482 type = types.bool; 483 default = false; 484 description = '' 485 Marks the server as a backup server. It will be passed 486 requests when the primary servers are unavailable. 487 ''; 488 }; 489 }; 490 }); 491 description = '' 492 Defines the address and other parameters of the upstream servers. 493 ''; 494 default = {}; 495 }; 496 }; 497 }); 498 description = '' 499 Defines a group of servers to use as proxy target. 500 ''; 501 default = {}; 502 }; 503 504 virtualHosts = mkOption { 505 type = types.attrsOf (types.submodule (import ./vhost-options.nix { 506 inherit config lib; 507 })); 508 default = { 509 localhost = {}; 510 }; 511 example = literalExample '' 512 { 513 "hydra.example.com" = { 514 forceSSL = true; 515 enableACME = true; 516 locations."/" = { 517 proxyPass = "http://localhost:3000"; 518 }; 519 }; 520 }; 521 ''; 522 description = "Declarative vhost config"; 523 }; 524 }; 525 }; 526 527 config = mkIf cfg.enable { 528 # TODO: test user supplied config file pases syntax test 529 530 warnings = 531 let 532 deprecatedSSL = name: config: optional config.enableSSL 533 '' 534 config.services.nginx.virtualHosts.<name>.enableSSL is deprecated, 535 use config.services.nginx.virtualHosts.<name>.onlySSL instead. 536 ''; 537 538 in flatten (mapAttrsToList deprecatedSSL virtualHosts); 539 540 assertions = 541 let 542 hostOrAliasIsNull = l: l.root == null || l.alias == null; 543 in [ 544 { 545 assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts); 546 message = "Only one of nginx root or alias can be specified on a location."; 547 } 548 549 { 550 assertion = all (conf: with conf; 551 !(addSSL && (onlySSL || enableSSL)) && 552 !(forceSSL && (onlySSL || enableSSL)) && 553 !(addSSL && forceSSL) 554 ) (attrValues virtualHosts); 555 message = '' 556 Options services.nginx.service.virtualHosts.<name>.addSSL, 557 services.nginx.virtualHosts.<name>.onlySSL and services.nginx.virtualHosts.<name>.forceSSL 558 are mutually exclusive. 559 ''; 560 } 561 562 { 563 assertion = all (conf: !(conf.enableACME && conf.useACMEHost != null)) (attrValues virtualHosts); 564 message = '' 565 Options services.nginx.service.virtualHosts.<name>.enableACME and 566 services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive. 567 ''; 568 } 569 ]; 570 571 systemd.services.nginx = { 572 description = "Nginx Web Server"; 573 after = [ "network.target" ]; 574 wantedBy = [ "multi-user.target" ]; 575 stopIfChanged = false; 576 preStart = 577 '' 578 mkdir -p ${cfg.stateDir}/logs 579 chmod 700 ${cfg.stateDir} 580 chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir} 581 ${cfg.package}/bin/nginx -c ${configFile} -p ${cfg.stateDir} -t 582 ''; 583 serviceConfig = { 584 ExecStart = "${cfg.package}/bin/nginx -c ${configFile} -p ${cfg.stateDir}"; 585 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 586 Restart = "always"; 587 RestartSec = "10s"; 588 StartLimitInterval = "1min"; 589 }; 590 }; 591 592 security.acme.certs = filterAttrs (n: v: v != {}) ( 593 let 594 vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts; 595 acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME && vhostConfig.useACMEHost == null) vhostsConfigs; 596 acmePairs = map (vhostConfig: { name = vhostConfig.serverName; value = { 597 user = cfg.user; 598 group = lib.mkDefault cfg.group; 599 webroot = vhostConfig.acmeRoot; 600 extraDomains = genAttrs vhostConfig.serverAliases (alias: null); 601 postRun = '' 602 systemctl reload nginx 603 ''; 604 }; }) acmeEnabledVhosts; 605 in 606 listToAttrs acmePairs 607 ); 608 609 users.extraUsers = optionalAttrs (cfg.user == "nginx") (singleton 610 { name = "nginx"; 611 group = cfg.group; 612 uid = config.ids.uids.nginx; 613 }); 614 615 users.extraGroups = optionalAttrs (cfg.group == "nginx") (singleton 616 { name = "nginx"; 617 gid = config.ids.gids.nginx; 618 }); 619 }; 620}