at 21.11-pre 32 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.nginx; 7 certs = config.security.acme.certs; 8 vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts; 9 acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs; 10 dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts); 11 virtualHosts = mapAttrs (vhostName: vhostConfig: 12 let 13 serverName = if vhostConfig.serverName != null 14 then vhostConfig.serverName 15 else vhostName; 16 certName = if vhostConfig.useACMEHost != null 17 then vhostConfig.useACMEHost 18 else serverName; 19 in 20 vhostConfig // { 21 inherit serverName certName; 22 } // (optionalAttrs (vhostConfig.enableACME || vhostConfig.useACMEHost != null) { 23 sslCertificate = "${certs.${certName}.directory}/fullchain.pem"; 24 sslCertificateKey = "${certs.${certName}.directory}/key.pem"; 25 sslTrustedCertificate = "${certs.${certName}.directory}/chain.pem"; 26 }) 27 ) cfg.virtualHosts; 28 enableIPv6 = config.networking.enableIPv6; 29 30 defaultFastcgiParams = { 31 SCRIPT_FILENAME = "$document_root$fastcgi_script_name"; 32 QUERY_STRING = "$query_string"; 33 REQUEST_METHOD = "$request_method"; 34 CONTENT_TYPE = "$content_type"; 35 CONTENT_LENGTH = "$content_length"; 36 37 SCRIPT_NAME = "$fastcgi_script_name"; 38 REQUEST_URI = "$request_uri"; 39 DOCUMENT_URI = "$document_uri"; 40 DOCUMENT_ROOT = "$document_root"; 41 SERVER_PROTOCOL = "$server_protocol"; 42 REQUEST_SCHEME = "$scheme"; 43 HTTPS = "$https if_not_empty"; 44 45 GATEWAY_INTERFACE = "CGI/1.1"; 46 SERVER_SOFTWARE = "nginx/$nginx_version"; 47 48 REMOTE_ADDR = "$remote_addr"; 49 REMOTE_PORT = "$remote_port"; 50 SERVER_ADDR = "$server_addr"; 51 SERVER_PORT = "$server_port"; 52 SERVER_NAME = "$server_name"; 53 54 REDIRECT_STATUS = "200"; 55 }; 56 57 recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" '' 58 proxy_set_header Host $host; 59 proxy_set_header X-Real-IP $remote_addr; 60 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 61 proxy_set_header X-Forwarded-Proto $scheme; 62 proxy_set_header X-Forwarded-Host $host; 63 proxy_set_header X-Forwarded-Server $host; 64 ''; 65 66 upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: '' 67 upstream ${name} { 68 ${toString (flip mapAttrsToList upstream.servers (name: server: '' 69 server ${name} ${optionalString server.backup "backup"}; 70 ''))} 71 ${upstream.extraConfig} 72 } 73 '')); 74 75 commonHttpConfig = '' 76 # The mime type definitions included with nginx are very incomplete, so 77 # we use a list of mime types from the mailcap package, which is also 78 # used by most other Linux distributions by default. 79 include ${pkgs.mailcap}/etc/nginx/mime.types; 80 include ${cfg.package}/conf/fastcgi.conf; 81 include ${cfg.package}/conf/uwsgi_params; 82 83 default_type application/octet-stream; 84 ''; 85 86 configFile = pkgs.writers.writeNginxConfig "nginx.conf" '' 87 pid /run/nginx/nginx.pid; 88 error_log ${cfg.logError}; 89 daemon off; 90 91 ${cfg.config} 92 93 ${optionalString (cfg.eventsConfig != "" || cfg.config == "") '' 94 events { 95 ${cfg.eventsConfig} 96 } 97 ''} 98 99 ${optionalString (cfg.httpConfig == "" && cfg.config == "") '' 100 http { 101 ${commonHttpConfig} 102 103 ${optionalString (cfg.resolver.addresses != []) '' 104 resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"} ${optionalString (!cfg.resolver.ipv6) "ipv6=off"}; 105 ''} 106 ${upstreamConfig} 107 108 ${optionalString (cfg.recommendedOptimisation) '' 109 # optimisation 110 sendfile on; 111 tcp_nopush on; 112 tcp_nodelay on; 113 keepalive_timeout 65; 114 types_hash_max_size 4096; 115 ''} 116 117 ssl_protocols ${cfg.sslProtocols}; 118 ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"} 119 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"} 120 121 ${optionalString (cfg.recommendedTlsSettings) '' 122 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate 123 124 ssl_session_timeout 1d; 125 ssl_session_cache shared:SSL:10m; 126 # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135 127 ssl_session_tickets off; 128 # We don't enable insecure ciphers by default, so this allows 129 # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260 130 ssl_prefer_server_ciphers off; 131 132 # OCSP stapling 133 ssl_stapling on; 134 ssl_stapling_verify on; 135 ''} 136 137 ${optionalString (cfg.recommendedGzipSettings) '' 138 gzip on; 139 gzip_proxied any; 140 gzip_comp_level 5; 141 gzip_types 142 application/atom+xml 143 application/javascript 144 application/json 145 application/xml 146 application/xml+rss 147 image/svg+xml 148 text/css 149 text/javascript 150 text/plain 151 text/xml; 152 gzip_vary on; 153 ''} 154 155 ${optionalString (cfg.recommendedProxySettings) '' 156 proxy_redirect off; 157 proxy_connect_timeout ${cfg.proxyTimeout}; 158 proxy_send_timeout ${cfg.proxyTimeout}; 159 proxy_read_timeout ${cfg.proxyTimeout}; 160 proxy_http_version 1.1; 161 include ${recommendedProxyConfig}; 162 ''} 163 164 ${optionalString (cfg.mapHashBucketSize != null) '' 165 map_hash_bucket_size ${toString cfg.mapHashBucketSize}; 166 ''} 167 168 ${optionalString (cfg.mapHashMaxSize != null) '' 169 map_hash_max_size ${toString cfg.mapHashMaxSize}; 170 ''} 171 172 # $connection_upgrade is used for websocket proxying 173 map $http_upgrade $connection_upgrade { 174 default upgrade; 175 ''' close; 176 } 177 client_max_body_size ${cfg.clientMaxBodySize}; 178 179 server_tokens ${if cfg.serverTokens then "on" else "off"}; 180 181 ${cfg.commonHttpConfig} 182 183 ${vhosts} 184 185 ${optionalString cfg.statusPage '' 186 server { 187 listen 80; 188 ${optionalString enableIPv6 "listen [::]:80;" } 189 190 server_name localhost; 191 192 location /nginx_status { 193 stub_status on; 194 access_log off; 195 allow 127.0.0.1; 196 ${optionalString enableIPv6 "allow ::1;"} 197 deny all; 198 } 199 } 200 ''} 201 202 ${cfg.appendHttpConfig} 203 }''} 204 205 ${optionalString (cfg.httpConfig != "") '' 206 http { 207 ${commonHttpConfig} 208 ${cfg.httpConfig} 209 }''} 210 211 ${optionalString (cfg.streamConfig != "") '' 212 stream { 213 ${cfg.streamConfig} 214 } 215 ''} 216 217 ${cfg.appendConfig} 218 ''; 219 220 configPath = if cfg.enableReload 221 then "/etc/nginx/nginx.conf" 222 else configFile; 223 224 execCommand = "${cfg.package}/bin/nginx -c '${configPath}'"; 225 226 vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost: 227 let 228 onlySSL = vhost.onlySSL || vhost.enableSSL; 229 hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL; 230 231 defaultListen = 232 if vhost.listen != [] then vhost.listen 233 else ((optionals hasSSL ( 234 singleton { addr = "0.0.0.0"; port = 443; ssl = true; } 235 ++ optional enableIPv6 { addr = "[::]"; port = 443; ssl = true; } 236 )) ++ optionals (!onlySSL) ( 237 singleton { addr = "0.0.0.0"; port = 80; ssl = false; } 238 ++ optional enableIPv6 { addr = "[::]"; port = 80; ssl = false; } 239 )); 240 241 hostListen = 242 if vhost.forceSSL 243 then filter (x: x.ssl) defaultListen 244 else defaultListen; 245 246 listenString = { addr, port, ssl, extraParameters ? [], ... }: 247 "listen ${addr}:${toString port} " 248 + optionalString ssl "ssl " 249 + optionalString (ssl && vhost.http2) "http2 " 250 + optionalString vhost.default "default_server " 251 + optionalString (extraParameters != []) (concatStringsSep " " extraParameters) 252 + ";" 253 + (if ssl && vhost.http3 then '' 254 # UDP listener for **QUIC+HTTP/3 255 listen ${addr}:${toString port} http3 reuseport; 256 # Advertise that HTTP/3 is available 257 add_header Alt-Svc 'h3=":443"'; 258 # Sent when QUIC was used 259 add_header QUIC-Status $quic; 260 '' else ""); 261 262 redirectListen = filter (x: !x.ssl) defaultListen; 263 264 acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) '' 265 location /.well-known/acme-challenge { 266 ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"} 267 root ${vhost.acmeRoot}; 268 auth_basic off; 269 } 270 ${optionalString (vhost.acmeFallbackHost != null) '' 271 location @acme-fallback { 272 auth_basic off; 273 proxy_pass http://${vhost.acmeFallbackHost}; 274 } 275 ''} 276 ''; 277 278 in '' 279 ${optionalString vhost.forceSSL '' 280 server { 281 ${concatMapStringsSep "\n" listenString redirectListen} 282 283 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; 284 ${acmeLocation} 285 location / { 286 return 301 https://$host$request_uri; 287 } 288 } 289 ''} 290 291 server { 292 ${concatMapStringsSep "\n" listenString hostListen} 293 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; 294 ${acmeLocation} 295 ${optionalString (vhost.root != null) "root ${vhost.root};"} 296 ${optionalString (vhost.globalRedirect != null) '' 297 return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri; 298 ''} 299 ${optionalString hasSSL '' 300 ssl_certificate ${vhost.sslCertificate}; 301 ssl_certificate_key ${vhost.sslCertificateKey}; 302 ''} 303 ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) '' 304 ssl_trusted_certificate ${vhost.sslTrustedCertificate}; 305 ''} 306 307 ${mkBasicAuth vhostName vhost} 308 309 ${mkLocations vhost.locations} 310 311 ${vhost.extraConfig} 312 } 313 '' 314 ) virtualHosts); 315 mkLocations = locations: concatStringsSep "\n" (map (config: '' 316 location ${config.location} { 317 ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning) 318 "proxy_pass ${config.proxyPass};" 319 } 320 ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) '' 321 set $nix_proxy_target "${config.proxyPass}"; 322 proxy_pass $nix_proxy_target; 323 ''} 324 ${optionalString config.proxyWebsockets '' 325 proxy_http_version 1.1; 326 proxy_set_header Upgrade $http_upgrade; 327 proxy_set_header Connection $connection_upgrade; 328 ''} 329 ${concatStringsSep "\n" 330 (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'') 331 (optionalAttrs (config.fastcgiParams != {}) 332 (defaultFastcgiParams // config.fastcgiParams)))} 333 ${optionalString (config.index != null) "index ${config.index};"} 334 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"} 335 ${optionalString (config.root != null) "root ${config.root};"} 336 ${optionalString (config.alias != null) "alias ${config.alias};"} 337 ${optionalString (config.return != null) "return ${config.return};"} 338 ${config.extraConfig} 339 ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"} 340 ${mkBasicAuth "sublocation" config} 341 } 342 '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))); 343 344 mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let 345 auth_file = if zone.basicAuthFile != null 346 then zone.basicAuthFile 347 else mkHtpasswd name zone.basicAuth; 348 in '' 349 auth_basic secured; 350 auth_basic_user_file ${auth_file}; 351 ''); 352 mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" ( 353 concatStringsSep "\n" (mapAttrsToList (user: password: '' 354 ${user}:{PLAIN}${password} 355 '') authDef) 356 ); 357in 358 359{ 360 options = { 361 services.nginx = { 362 enable = mkEnableOption "Nginx Web Server"; 363 364 statusPage = mkOption { 365 default = false; 366 type = types.bool; 367 description = " 368 Enable status page reachable from localhost on http://127.0.0.1/nginx_status. 369 "; 370 }; 371 372 recommendedTlsSettings = mkOption { 373 default = false; 374 type = types.bool; 375 description = " 376 Enable recommended TLS settings. 377 "; 378 }; 379 380 recommendedOptimisation = mkOption { 381 default = false; 382 type = types.bool; 383 description = " 384 Enable recommended optimisation settings. 385 "; 386 }; 387 388 recommendedGzipSettings = mkOption { 389 default = false; 390 type = types.bool; 391 description = " 392 Enable recommended gzip settings. 393 "; 394 }; 395 396 recommendedProxySettings = mkOption { 397 default = false; 398 type = types.bool; 399 description = " 400 Enable recommended proxy settings. 401 "; 402 }; 403 404 proxyTimeout = mkOption { 405 type = types.str; 406 default = "60s"; 407 example = "20s"; 408 description = " 409 Change the proxy related timeouts in recommendedProxySettings. 410 "; 411 }; 412 413 package = mkOption { 414 default = pkgs.nginxStable; 415 defaultText = "pkgs.nginxStable"; 416 type = types.package; 417 apply = p: p.override { 418 modules = p.modules ++ cfg.additionalModules; 419 }; 420 description = " 421 Nginx package to use. This defaults to the stable version. Note 422 that the nginx team recommends to use the mainline version which 423 available in nixpkgs as <literal>nginxMainline</literal>. 424 "; 425 }; 426 427 additionalModules = mkOption { 428 default = []; 429 type = types.listOf (types.attrsOf types.anything); 430 example = literalExample "[ pkgs.nginxModules.brotli ]"; 431 description = '' 432 Additional <link xlink:href="https://www.nginx.com/resources/wiki/modules/">third-party nginx modules</link> 433 to install. Packaged modules are available in 434 <literal>pkgs.nginxModules</literal>. 435 ''; 436 }; 437 438 logError = mkOption { 439 default = "stderr"; 440 type = types.str; 441 description = " 442 Configures logging. 443 The first parameter defines a file that will store the log. The 444 special value stderr selects the standard error file. Logging to 445 syslog can be configured by specifying the syslog: prefix. 446 The second parameter determines the level of logging, and can be 447 one of the following: debug, info, notice, warn, error, crit, 448 alert, or emerg. Log levels above are listed in the order of 449 increasing severity. Setting a certain log level will cause all 450 messages of the specified and more severe log levels to be logged. 451 If this parameter is omitted then error is used. 452 "; 453 }; 454 455 preStart = mkOption { 456 type = types.lines; 457 default = ""; 458 description = " 459 Shell commands executed before the service's nginx is started. 460 "; 461 }; 462 463 config = mkOption { 464 type = types.str; 465 default = ""; 466 description = '' 467 Verbatim <filename>nginx.conf</filename> configuration. 468 This is mutually exclusive to any other config option for 469 <filename>nginx.conf</filename> except for 470 <itemizedlist> 471 <listitem><para><xref linkend="opt-services.nginx.appendConfig" /> 472 </para></listitem> 473 <listitem><para><xref linkend="opt-services.nginx.httpConfig" /> 474 </para></listitem> 475 <listitem><para><xref linkend="opt-services.nginx.logError" /> 476 </para></listitem> 477 </itemizedlist> 478 479 If additional verbatim config in addition to other options is needed, 480 <xref linkend="opt-services.nginx.appendConfig" /> should be used instead. 481 ''; 482 }; 483 484 appendConfig = mkOption { 485 type = types.lines; 486 default = ""; 487 description = '' 488 Configuration lines appended to the generated Nginx 489 configuration file. Commonly used by different modules 490 providing http snippets. <option>appendConfig</option> 491 can be specified more than once and it's value will be 492 concatenated (contrary to <option>config</option> which 493 can be set only once). 494 ''; 495 }; 496 497 commonHttpConfig = mkOption { 498 type = types.lines; 499 default = ""; 500 example = '' 501 resolver 127.0.0.1 valid=5s; 502 503 log_format myformat '$remote_addr - $remote_user [$time_local] ' 504 '"$request" $status $body_bytes_sent ' 505 '"$http_referer" "$http_user_agent"'; 506 ''; 507 description = '' 508 With nginx you must provide common http context definitions before 509 they are used, e.g. log_format, resolver, etc. inside of server 510 or location contexts. Use this attribute to set these definitions 511 at the appropriate location. 512 ''; 513 }; 514 515 httpConfig = mkOption { 516 type = types.lines; 517 default = ""; 518 description = " 519 Configuration lines to be set inside the http block. 520 This is mutually exclusive with the structured configuration 521 via virtualHosts and the recommendedXyzSettings configuration 522 options. See appendHttpConfig for appending to the generated http block. 523 "; 524 }; 525 526 streamConfig = mkOption { 527 type = types.lines; 528 default = ""; 529 example = '' 530 server { 531 listen 127.0.0.1:53 udp reuseport; 532 proxy_timeout 20s; 533 proxy_pass 192.168.0.1:53535; 534 } 535 ''; 536 description = " 537 Configuration lines to be set inside the stream block. 538 "; 539 }; 540 541 eventsConfig = mkOption { 542 type = types.lines; 543 default = ""; 544 description = '' 545 Configuration lines to be set inside the events block. 546 ''; 547 }; 548 549 appendHttpConfig = mkOption { 550 type = types.lines; 551 default = ""; 552 description = " 553 Configuration lines to be appended to the generated http block. 554 This is mutually exclusive with using config and httpConfig for 555 specifying the whole http block verbatim. 556 "; 557 }; 558 559 enableReload = mkOption { 560 default = false; 561 type = types.bool; 562 description = '' 563 Reload nginx when configuration file changes (instead of restart). 564 The configuration file is exposed at <filename>/etc/nginx/nginx.conf</filename>. 565 See also <literal>systemd.services.*.restartIfChanged</literal>. 566 ''; 567 }; 568 569 user = mkOption { 570 type = types.str; 571 default = "nginx"; 572 description = "User account under which nginx runs."; 573 }; 574 575 group = mkOption { 576 type = types.str; 577 default = "nginx"; 578 description = "Group account under which nginx runs."; 579 }; 580 581 serverTokens = mkOption { 582 type = types.bool; 583 default = false; 584 description = "Show nginx version in headers and error pages."; 585 }; 586 587 clientMaxBodySize = mkOption { 588 type = types.str; 589 default = "10m"; 590 description = "Set nginx global client_max_body_size."; 591 }; 592 593 sslCiphers = mkOption { 594 type = types.nullOr types.str; 595 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate 596 default = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; 597 description = "Ciphers to choose from when negotiating TLS handshakes."; 598 }; 599 600 sslProtocols = mkOption { 601 type = types.str; 602 default = "TLSv1.2 TLSv1.3"; 603 example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"; 604 description = "Allowed TLS protocol versions."; 605 }; 606 607 sslDhparam = mkOption { 608 type = types.nullOr types.path; 609 default = null; 610 example = "/path/to/dhparams.pem"; 611 description = "Path to DH parameters file."; 612 }; 613 614 proxyResolveWhileRunning = mkOption { 615 type = types.bool; 616 default = false; 617 description = '' 618 Resolves domains of proxyPass targets at runtime 619 and not only at start, you have to set 620 services.nginx.resolver, too. 621 ''; 622 }; 623 624 mapHashBucketSize = mkOption { 625 type = types.nullOr (types.enum [ 32 64 128 ]); 626 default = null; 627 description = '' 628 Sets the bucket size for the map variables hash tables. Default 629 value depends on the processors cache line size. 630 ''; 631 }; 632 633 mapHashMaxSize = mkOption { 634 type = types.nullOr types.ints.positive; 635 default = null; 636 description = '' 637 Sets the maximum size of the map variables hash tables. 638 ''; 639 }; 640 641 resolver = mkOption { 642 type = types.submodule { 643 options = { 644 addresses = mkOption { 645 type = types.listOf types.str; 646 default = []; 647 example = literalExample ''[ "[::1]" "127.0.0.1:5353" ]''; 648 description = "List of resolvers to use"; 649 }; 650 valid = mkOption { 651 type = types.str; 652 default = ""; 653 example = "30s"; 654 description = '' 655 By default, nginx caches answers using the TTL value of a response. 656 An optional valid parameter allows overriding it 657 ''; 658 }; 659 ipv6 = mkOption { 660 type = types.bool; 661 default = true; 662 description = '' 663 By default, nginx will look up both IPv4 and IPv6 addresses while resolving. 664 If looking up of IPv6 addresses is not desired, the ipv6=off parameter can be 665 specified. 666 ''; 667 }; 668 }; 669 }; 670 description = '' 671 Configures name servers used to resolve names of upstream servers into addresses 672 ''; 673 default = {}; 674 }; 675 676 upstreams = mkOption { 677 type = types.attrsOf (types.submodule { 678 options = { 679 servers = mkOption { 680 type = types.attrsOf (types.submodule { 681 options = { 682 backup = mkOption { 683 type = types.bool; 684 default = false; 685 description = '' 686 Marks the server as a backup server. It will be passed 687 requests when the primary servers are unavailable. 688 ''; 689 }; 690 }; 691 }); 692 description = '' 693 Defines the address and other parameters of the upstream servers. 694 ''; 695 default = {}; 696 example = { "127.0.0.1:8000" = {}; }; 697 }; 698 extraConfig = mkOption { 699 type = types.lines; 700 default = ""; 701 description = '' 702 These lines go to the end of the upstream verbatim. 703 ''; 704 }; 705 }; 706 }); 707 description = '' 708 Defines a group of servers to use as proxy target. 709 ''; 710 default = {}; 711 example = literalExample '' 712 "backend_server" = { 713 servers = { "127.0.0.1:8000" = {}; }; 714 extraConfig = '''' 715 keepalive 16; 716 ''''; 717 }; 718 ''; 719 }; 720 721 virtualHosts = mkOption { 722 type = types.attrsOf (types.submodule (import ./vhost-options.nix { 723 inherit config lib; 724 })); 725 default = { 726 localhost = {}; 727 }; 728 example = literalExample '' 729 { 730 "hydra.example.com" = { 731 forceSSL = true; 732 enableACME = true; 733 locations."/" = { 734 proxyPass = "http://localhost:3000"; 735 }; 736 }; 737 }; 738 ''; 739 description = "Declarative vhost config"; 740 }; 741 }; 742 }; 743 744 imports = [ 745 (mkRemovedOptionModule [ "services" "nginx" "stateDir" ] '' 746 The Nginx log directory has been moved to /var/log/nginx, the cache directory 747 to /var/cache/nginx. The option services.nginx.stateDir has been removed. 748 '') 749 ]; 750 751 config = mkIf cfg.enable { 752 # TODO: test user supplied config file pases syntax test 753 754 warnings = 755 let 756 deprecatedSSL = name: config: optional config.enableSSL 757 '' 758 config.services.nginx.virtualHosts.<name>.enableSSL is deprecated, 759 use config.services.nginx.virtualHosts.<name>.onlySSL instead. 760 ''; 761 762 in flatten (mapAttrsToList deprecatedSSL virtualHosts); 763 764 assertions = 765 let 766 hostOrAliasIsNull = l: l.root == null || l.alias == null; 767 in [ 768 { 769 assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts); 770 message = "Only one of nginx root or alias can be specified on a location."; 771 } 772 773 { 774 assertion = all (conf: with conf; 775 !(addSSL && (onlySSL || enableSSL)) && 776 !(forceSSL && (onlySSL || enableSSL)) && 777 !(addSSL && forceSSL) 778 ) (attrValues virtualHosts); 779 message = '' 780 Options services.nginx.service.virtualHosts.<name>.addSSL, 781 services.nginx.virtualHosts.<name>.onlySSL and services.nginx.virtualHosts.<name>.forceSSL 782 are mutually exclusive. 783 ''; 784 } 785 786 { 787 assertion = all (conf: !(conf.enableACME && conf.useACMEHost != null)) (attrValues virtualHosts); 788 message = '' 789 Options services.nginx.service.virtualHosts.<name>.enableACME and 790 services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive. 791 ''; 792 } 793 ]; 794 795 systemd.services.nginx = { 796 description = "Nginx Web Server"; 797 wantedBy = [ "multi-user.target" ]; 798 wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames); 799 after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames; 800 # Nginx needs to be started in order to be able to request certificates 801 # (it's hosting the acme challenge after all) 802 # This fixes https://github.com/NixOS/nixpkgs/issues/81842 803 before = map (certName: "acme-${certName}.service") dependentCertNames; 804 stopIfChanged = false; 805 preStart = '' 806 ${cfg.preStart} 807 ${execCommand} -t 808 ''; 809 810 startLimitIntervalSec = 60; 811 serviceConfig = { 812 ExecStart = execCommand; 813 ExecReload = [ 814 "${execCommand} -t" 815 "${pkgs.coreutils}/bin/kill -HUP $MAINPID" 816 ]; 817 Restart = "always"; 818 RestartSec = "10s"; 819 # User and group 820 User = cfg.user; 821 Group = cfg.group; 822 # Runtime directory and mode 823 RuntimeDirectory = "nginx"; 824 RuntimeDirectoryMode = "0750"; 825 # Cache directory and mode 826 CacheDirectory = "nginx"; 827 CacheDirectoryMode = "0750"; 828 # Logs directory and mode 829 LogsDirectory = "nginx"; 830 LogsDirectoryMode = "0750"; 831 # Proc filesystem 832 ProcSubset = "pid"; 833 ProtectProc = "invisible"; 834 # New file permissions 835 UMask = "0027"; # 0640 / 0750 836 # Capabilities 837 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ]; 838 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ]; 839 # Security 840 NoNewPrivileges = true; 841 # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html) 842 ProtectSystem = "strict"; 843 ProtectHome = mkDefault true; 844 PrivateTmp = true; 845 PrivateDevices = true; 846 ProtectHostname = true; 847 ProtectClock = true; 848 ProtectKernelTunables = true; 849 ProtectKernelModules = true; 850 ProtectKernelLogs = true; 851 ProtectControlGroups = true; 852 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 853 RestrictNamespaces = true; 854 LockPersonality = true; 855 MemoryDenyWriteExecute = !(builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules); 856 RestrictRealtime = true; 857 RestrictSUIDSGID = true; 858 RemoveIPC = true; 859 PrivateMounts = true; 860 # System Call Filtering 861 SystemCallArchitectures = "native"; 862 SystemCallFilter = "~@cpu-emulation @debug @keyring @ipc @mount @obsolete @privileged @setuid"; 863 }; 864 }; 865 866 environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload { 867 source = configFile; 868 }; 869 870 # This service waits for all certificates to be available 871 # before reloading nginx configuration. 872 # sslTargets are added to wantedBy + before 873 # which allows the acme-finished-$cert.target to signify the successful updating 874 # of certs end-to-end. 875 systemd.services.nginx-config-reload = let 876 sslServices = map (certName: "acme-${certName}.service") dependentCertNames; 877 sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames; 878 in mkIf (cfg.enableReload || sslServices != []) { 879 wants = optionals (cfg.enableReload) [ "nginx.service" ]; 880 wantedBy = sslServices ++ [ "multi-user.target" ]; 881 # Before the finished targets, after the renew services. 882 # This service might be needed for HTTP-01 challenges, but we only want to confirm 883 # certs are updated _after_ config has been reloaded. 884 before = sslTargets; 885 after = sslServices; 886 restartTriggers = optionals (cfg.enableReload) [ configFile ]; 887 # Block reloading if not all certs exist yet. 888 # Happens when config changes add new vhosts/certs. 889 unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames); 890 serviceConfig = { 891 Type = "oneshot"; 892 TimeoutSec = 60; 893 ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service"; 894 ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service"; 895 }; 896 }; 897 898 security.acme.certs = let 899 acmePairs = map (vhostConfig: nameValuePair vhostConfig.serverName { 900 group = mkDefault cfg.group; 901 webroot = vhostConfig.acmeRoot; 902 extraDomainNames = vhostConfig.serverAliases; 903 # Filter for enableACME-only vhosts. Don't want to create dud certs 904 }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts); 905 in listToAttrs acmePairs; 906 907 users.users = optionalAttrs (cfg.user == "nginx") { 908 nginx = { 909 group = cfg.group; 910 isSystemUser = true; 911 uid = config.ids.uids.nginx; 912 }; 913 }; 914 915 users.groups = optionalAttrs (cfg.group == "nginx") { 916 nginx.gid = config.ids.gids.nginx; 917 }; 918 919 }; 920}