at 23.11-pre 46 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.nginx; 7 inherit (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 = if vhostConfig.sslTrustedCertificate != null 26 then vhostConfig.sslTrustedCertificate 27 else "${certs.${certName}.directory}/chain.pem"; 28 }) 29 ) cfg.virtualHosts; 30 inherit (config.networking) enableIPv6; 31 32 # Mime.types values are taken from brotli sample configuration - https://github.com/google/ngx_brotli 33 # and Nginx Server Configs - https://github.com/h5bp/server-configs-nginx 34 # "text/html" is implicitly included in {brotli,gzip,zstd}_types 35 compressMimeTypes = [ 36 "application/atom+xml" 37 "application/geo+json" 38 "application/json" 39 "application/ld+json" 40 "application/manifest+json" 41 "application/rdf+xml" 42 "application/vnd.ms-fontobject" 43 "application/wasm" 44 "application/x-rss+xml" 45 "application/x-web-app-manifest+json" 46 "application/xhtml+xml" 47 "application/xliff+xml" 48 "application/xml" 49 "font/collection" 50 "font/otf" 51 "font/ttf" 52 "image/bmp" 53 "image/svg+xml" 54 "image/vnd.microsoft.icon" 55 "text/cache-manifest" 56 "text/calendar" 57 "text/css" 58 "text/csv" 59 "text/javascript" 60 "text/markdown" 61 "text/plain" 62 "text/vcard" 63 "text/vnd.rim.location.xloc" 64 "text/vtt" 65 "text/x-component" 66 "text/xml" 67 ]; 68 69 defaultFastcgiParams = { 70 SCRIPT_FILENAME = "$document_root$fastcgi_script_name"; 71 QUERY_STRING = "$query_string"; 72 REQUEST_METHOD = "$request_method"; 73 CONTENT_TYPE = "$content_type"; 74 CONTENT_LENGTH = "$content_length"; 75 76 SCRIPT_NAME = "$fastcgi_script_name"; 77 REQUEST_URI = "$request_uri"; 78 DOCUMENT_URI = "$document_uri"; 79 DOCUMENT_ROOT = "$document_root"; 80 SERVER_PROTOCOL = "$server_protocol"; 81 REQUEST_SCHEME = "$scheme"; 82 HTTPS = "$https if_not_empty"; 83 84 GATEWAY_INTERFACE = "CGI/1.1"; 85 SERVER_SOFTWARE = "nginx/$nginx_version"; 86 87 REMOTE_ADDR = "$remote_addr"; 88 REMOTE_PORT = "$remote_port"; 89 SERVER_ADDR = "$server_addr"; 90 SERVER_PORT = "$server_port"; 91 SERVER_NAME = "$server_name"; 92 93 REDIRECT_STATUS = "200"; 94 }; 95 96 recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" '' 97 proxy_set_header Host $host; 98 proxy_set_header X-Real-IP $remote_addr; 99 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 100 proxy_set_header X-Forwarded-Proto $scheme; 101 proxy_set_header X-Forwarded-Host $host; 102 proxy_set_header X-Forwarded-Server $host; 103 ''; 104 105 proxyCachePathConfig = concatStringsSep "\n" (mapAttrsToList (name: proxyCachePath: '' 106 proxy_cache_path ${concatStringsSep " " [ 107 "/var/cache/nginx/${name}" 108 "keys_zone=${proxyCachePath.keysZoneName}:${proxyCachePath.keysZoneSize}" 109 "levels=${proxyCachePath.levels}" 110 "use_temp_path=${if proxyCachePath.useTempPath then "on" else "off"}" 111 "inactive=${proxyCachePath.inactive}" 112 "max_size=${proxyCachePath.maxSize}" 113 ]}; 114 '') (filterAttrs (name: conf: conf.enable) cfg.proxyCachePath)); 115 116 toUpstreamParameter = key: value: 117 if builtins.isBool value 118 then lib.optionalString value key 119 else "${key}=${toString value}"; 120 121 upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: '' 122 upstream ${name} { 123 ${toString (flip mapAttrsToList upstream.servers (name: server: '' 124 server ${name} ${concatStringsSep " " (mapAttrsToList toUpstreamParameter server)}; 125 ''))} 126 ${upstream.extraConfig} 127 } 128 '')); 129 130 commonHttpConfig = '' 131 # Load mime types. 132 include ${cfg.defaultMimeTypes}; 133 # When recommendedOptimisation is disabled nginx fails to start because the mailmap mime.types database 134 # contains 1026 entries and the default is only 1024. Setting to a higher number to remove the need to 135 # overwrite it because nginx does not allow duplicated settings. 136 types_hash_max_size 4096; 137 138 include ${cfg.package}/conf/fastcgi.conf; 139 include ${cfg.package}/conf/uwsgi_params; 140 141 default_type application/octet-stream; 142 ''; 143 144 configFile = pkgs.writers.writeNginxConfig "nginx.conf" '' 145 pid /run/nginx/nginx.pid; 146 error_log ${cfg.logError}; 147 daemon off; 148 149 ${cfg.config} 150 151 ${optionalString (cfg.eventsConfig != "" || cfg.config == "") '' 152 events { 153 ${cfg.eventsConfig} 154 } 155 ''} 156 157 ${optionalString (cfg.httpConfig == "" && cfg.config == "") '' 158 http { 159 ${commonHttpConfig} 160 161 ${optionalString (cfg.resolver.addresses != []) '' 162 resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"} ${optionalString (!cfg.resolver.ipv6) "ipv6=off"}; 163 ''} 164 ${upstreamConfig} 165 166 ${optionalString cfg.recommendedOptimisation '' 167 # optimisation 168 sendfile on; 169 tcp_nopush on; 170 tcp_nodelay on; 171 keepalive_timeout 65; 172 ''} 173 174 ssl_protocols ${cfg.sslProtocols}; 175 ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"} 176 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"} 177 178 ${optionalString cfg.recommendedTlsSettings '' 179 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate 180 181 ssl_session_timeout 1d; 182 ssl_session_cache shared:SSL:10m; 183 # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135 184 ssl_session_tickets off; 185 # We don't enable insecure ciphers by default, so this allows 186 # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260 187 ssl_prefer_server_ciphers off; 188 189 # OCSP stapling 190 ssl_stapling on; 191 ssl_stapling_verify on; 192 ''} 193 194 ${optionalString cfg.recommendedBrotliSettings '' 195 brotli on; 196 brotli_static on; 197 brotli_comp_level 5; 198 brotli_window 512k; 199 brotli_min_length 256; 200 brotli_types ${lib.concatStringsSep " " compressMimeTypes}; 201 ''} 202 203 ${optionalString cfg.recommendedGzipSettings 204 # https://docs.nginx.com/nginx/admin-guide/web-server/compression/ 205 '' 206 gzip on; 207 gzip_static on; 208 gzip_vary on; 209 gzip_comp_level 5; 210 gzip_min_length 256; 211 gzip_proxied expired no-cache no-store private auth; 212 gzip_types ${lib.concatStringsSep " " compressMimeTypes}; 213 ''} 214 215 ${optionalString cfg.recommendedZstdSettings '' 216 zstd on; 217 zstd_comp_level 9; 218 zstd_min_length 256; 219 zstd_static on; 220 zstd_types ${lib.concatStringsSep " " compressMimeTypes}; 221 ''} 222 223 ${optionalString cfg.recommendedProxySettings '' 224 proxy_redirect off; 225 proxy_connect_timeout ${cfg.proxyTimeout}; 226 proxy_send_timeout ${cfg.proxyTimeout}; 227 proxy_read_timeout ${cfg.proxyTimeout}; 228 proxy_http_version 1.1; 229 # don't let clients close the keep-alive connection to upstream. See the nginx blog for details: 230 # https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives 231 proxy_set_header "Connection" ""; 232 include ${recommendedProxyConfig}; 233 ''} 234 235 ${optionalString (cfg.mapHashBucketSize != null) '' 236 map_hash_bucket_size ${toString cfg.mapHashBucketSize}; 237 ''} 238 239 ${optionalString (cfg.mapHashMaxSize != null) '' 240 map_hash_max_size ${toString cfg.mapHashMaxSize}; 241 ''} 242 243 ${optionalString (cfg.serverNamesHashBucketSize != null) '' 244 server_names_hash_bucket_size ${toString cfg.serverNamesHashBucketSize}; 245 ''} 246 247 ${optionalString (cfg.serverNamesHashMaxSize != null) '' 248 server_names_hash_max_size ${toString cfg.serverNamesHashMaxSize}; 249 ''} 250 251 # $connection_upgrade is used for websocket proxying 252 map $http_upgrade $connection_upgrade { 253 default upgrade; 254 ''' close; 255 } 256 client_max_body_size ${cfg.clientMaxBodySize}; 257 258 server_tokens ${if cfg.serverTokens then "on" else "off"}; 259 260 ${cfg.commonHttpConfig} 261 262 ${proxyCachePathConfig} 263 264 ${optionalString cfg.statusPage '' 265 server { 266 listen ${toString cfg.defaultHTTPListenPort}; 267 ${optionalString enableIPv6 "listen [::]:${toString cfg.defaultHTTPListenPort};" } 268 269 server_name localhost; 270 271 location /nginx_status { 272 stub_status on; 273 access_log off; 274 allow 127.0.0.1; 275 ${optionalString enableIPv6 "allow ::1;"} 276 deny all; 277 } 278 } 279 ''} 280 281 ${vhosts} 282 283 ${cfg.appendHttpConfig} 284 }''} 285 286 ${optionalString (cfg.httpConfig != "") '' 287 http { 288 ${commonHttpConfig} 289 ${cfg.httpConfig} 290 }''} 291 292 ${optionalString (cfg.streamConfig != "") '' 293 stream { 294 ${cfg.streamConfig} 295 } 296 ''} 297 298 ${cfg.appendConfig} 299 ''; 300 301 configPath = if cfg.enableReload 302 then "/etc/nginx/nginx.conf" 303 else configFile; 304 305 execCommand = "${cfg.package}/bin/nginx -c '${configPath}'"; 306 307 vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost: 308 let 309 onlySSL = vhost.onlySSL || vhost.enableSSL; 310 hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL; 311 312 defaultListen = 313 if vhost.listen != [] then vhost.listen 314 else 315 let addrs = if vhost.listenAddresses != [] then vhost.listenAddresses else cfg.defaultListenAddresses; 316 in optionals (hasSSL || vhost.rejectSSL) (map (addr: { inherit addr; port = cfg.defaultSSLListenPort; ssl = true; }) addrs) 317 ++ optionals (!onlySSL) (map (addr: { inherit addr; port = cfg.defaultHTTPListenPort; ssl = false; }) addrs); 318 319 hostListen = 320 if vhost.forceSSL 321 then filter (x: x.ssl) defaultListen 322 else defaultListen; 323 324 listenString = { addr, port, ssl, extraParameters ? [], ... }: 325 # UDP listener for QUIC transport protocol. 326 (optionalString (ssl && vhost.quic) (" 327 listen ${addr}:${toString port} quic " 328 + optionalString vhost.default "default_server " 329 + optionalString vhost.reuseport "reuseport " 330 + optionalString (extraParameters != []) (concatStringsSep " " ( 331 let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ]; 332 isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters); 333 in filter isCompatibleParameter extraParameters)) 334 + ";")) 335 + " 336 337 listen ${addr}:${toString port} " 338 + optionalString (ssl && vhost.http2) "http2 " 339 + optionalString ssl "ssl " 340 + optionalString vhost.default "default_server " 341 + optionalString vhost.reuseport "reuseport " 342 + optionalString (extraParameters != []) (concatStringsSep " " extraParameters) 343 + ";"; 344 345 redirectListen = filter (x: !x.ssl) defaultListen; 346 347 acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) '' 348 # Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx) 349 # We use ^~ here, so that we don't check any regexes (which could 350 # otherwise easily override this intended match accidentally). 351 location ^~ /.well-known/acme-challenge/ { 352 ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"} 353 ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"} 354 auth_basic off; 355 } 356 ${optionalString (vhost.acmeFallbackHost != null) '' 357 location @acme-fallback { 358 auth_basic off; 359 proxy_pass http://${vhost.acmeFallbackHost}; 360 } 361 ''} 362 ''; 363 364 in '' 365 ${optionalString vhost.forceSSL '' 366 server { 367 ${concatMapStringsSep "\n" listenString redirectListen} 368 369 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; 370 ${acmeLocation} 371 location / { 372 return 301 https://$host$request_uri; 373 } 374 } 375 ''} 376 377 server { 378 ${concatMapStringsSep "\n" listenString hostListen} 379 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases}; 380 ${optionalString (hasSSL && vhost.quic) '' 381 http3 ${if vhost.http3 then "on" else "off"}; 382 http3_hq ${if vhost.http3_hq then "on" else "off"}; 383 ''} 384 ${acmeLocation} 385 ${optionalString (vhost.root != null) "root ${vhost.root};"} 386 ${optionalString (vhost.globalRedirect != null) '' 387 location / { 388 return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri; 389 } 390 ''} 391 ${optionalString hasSSL '' 392 ssl_certificate ${vhost.sslCertificate}; 393 ssl_certificate_key ${vhost.sslCertificateKey}; 394 ''} 395 ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) '' 396 ssl_trusted_certificate ${vhost.sslTrustedCertificate}; 397 ''} 398 ${optionalString vhost.rejectSSL '' 399 ssl_reject_handshake on; 400 ''} 401 ${optionalString (hasSSL && vhost.kTLS) '' 402 ssl_conf_command Options KTLS; 403 ''} 404 405 ${optionalString (hasSSL && vhost.quic && vhost.http3) 406 # Advertise that HTTP/3 is available 407 '' 408 add_header Alt-Svc 'h3=":$server_port"; ma=86400'; 409 ''} 410 411 ${mkBasicAuth vhostName vhost} 412 413 ${mkLocations vhost.locations} 414 415 ${vhost.extraConfig} 416 } 417 '' 418 ) virtualHosts); 419 mkLocations = locations: concatStringsSep "\n" (map (config: '' 420 location ${config.location} { 421 ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning) 422 "proxy_pass ${config.proxyPass};" 423 } 424 ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) '' 425 set $nix_proxy_target "${config.proxyPass}"; 426 proxy_pass $nix_proxy_target; 427 ''} 428 ${optionalString config.proxyWebsockets '' 429 proxy_http_version 1.1; 430 proxy_set_header Upgrade $http_upgrade; 431 proxy_set_header Connection $connection_upgrade; 432 ''} 433 ${concatStringsSep "\n" 434 (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'') 435 (optionalAttrs (config.fastcgiParams != {}) 436 (defaultFastcgiParams // config.fastcgiParams)))} 437 ${optionalString (config.index != null) "index ${config.index};"} 438 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"} 439 ${optionalString (config.root != null) "root ${config.root};"} 440 ${optionalString (config.alias != null) "alias ${config.alias};"} 441 ${optionalString (config.return != null) "return ${config.return};"} 442 ${config.extraConfig} 443 ${optionalString (config.proxyPass != null && config.recommendedProxySettings) "include ${recommendedProxyConfig};"} 444 ${mkBasicAuth "sublocation" config} 445 } 446 '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))); 447 448 mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let 449 auth_file = if zone.basicAuthFile != null 450 then zone.basicAuthFile 451 else mkHtpasswd name zone.basicAuth; 452 in '' 453 auth_basic secured; 454 auth_basic_user_file ${auth_file}; 455 ''); 456 mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" ( 457 concatStringsSep "\n" (mapAttrsToList (user: password: '' 458 ${user}:{PLAIN}${password} 459 '') authDef) 460 ); 461 462 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; 463in 464 465{ 466 options = { 467 services.nginx = { 468 enable = mkEnableOption (lib.mdDoc "Nginx Web Server"); 469 470 statusPage = mkOption { 471 default = false; 472 type = types.bool; 473 description = lib.mdDoc '' 474 Enable status page reachable from localhost on http://127.0.0.1/nginx_status. 475 ''; 476 }; 477 478 recommendedTlsSettings = mkOption { 479 default = false; 480 type = types.bool; 481 description = lib.mdDoc '' 482 Enable recommended TLS settings. 483 ''; 484 }; 485 486 recommendedOptimisation = mkOption { 487 default = false; 488 type = types.bool; 489 description = lib.mdDoc '' 490 Enable recommended optimisation settings. 491 ''; 492 }; 493 494 recommendedBrotliSettings = mkOption { 495 default = false; 496 type = types.bool; 497 description = lib.mdDoc '' 498 Enable recommended brotli settings. 499 Learn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/). 500 501 This adds `pkgs.nginxModules.brotli` to `services.nginx.additionalModules`. 502 ''; 503 }; 504 505 recommendedGzipSettings = mkOption { 506 default = false; 507 type = types.bool; 508 description = lib.mdDoc '' 509 Enable recommended gzip settings. 510 Learn more about compression in Gzip format [here](https://docs.nginx.com/nginx/admin-guide/web-server/compression/). 511 ''; 512 }; 513 514 recommendedZstdSettings = mkOption { 515 default = false; 516 type = types.bool; 517 description = lib.mdDoc '' 518 Enable recommended zstd settings. 519 Learn more about compression in Zstd format [here](https://github.com/tokers/zstd-nginx-module). 520 521 This adds `pkgs.nginxModules.zstd` to `services.nginx.additionalModules`. 522 ''; 523 }; 524 525 recommendedProxySettings = mkOption { 526 default = false; 527 type = types.bool; 528 description = lib.mdDoc '' 529 Whether to enable recommended proxy settings if a vhost does not specify the option manually. 530 ''; 531 }; 532 533 proxyTimeout = mkOption { 534 type = types.str; 535 default = "60s"; 536 example = "20s"; 537 description = lib.mdDoc '' 538 Change the proxy related timeouts in recommendedProxySettings. 539 ''; 540 }; 541 542 defaultListenAddresses = mkOption { 543 type = types.listOf types.str; 544 default = [ "0.0.0.0" ] ++ optional enableIPv6 "[::0]"; 545 defaultText = literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"''; 546 example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]''; 547 description = lib.mdDoc '' 548 If vhosts do not specify listenAddresses, use these addresses by default. 549 ''; 550 }; 551 552 defaultHTTPListenPort = mkOption { 553 type = types.port; 554 default = 80; 555 example = 8080; 556 description = lib.mdDoc '' 557 If vhosts do not specify listen.port, use these ports for HTTP by default. 558 ''; 559 }; 560 561 defaultSSLListenPort = mkOption { 562 type = types.port; 563 default = 443; 564 example = 8443; 565 description = lib.mdDoc '' 566 If vhosts do not specify listen.port, use these ports for SSL by default. 567 ''; 568 }; 569 570 defaultMimeTypes = mkOption { 571 type = types.path; 572 default = "${pkgs.mailcap}/etc/nginx/mime.types"; 573 defaultText = literalExpression "$''{pkgs.mailcap}/etc/nginx/mime.types"; 574 example = literalExpression "$''{pkgs.nginx}/conf/mime.types"; 575 description = lib.mdDoc '' 576 Default MIME types for NGINX, as MIME types definitions from NGINX are very incomplete, 577 we use by default the ones bundled in the mailcap package, used by most of the other 578 Linux distributions. 579 ''; 580 }; 581 582 package = mkOption { 583 default = pkgs.nginxStable; 584 defaultText = literalExpression "pkgs.nginxStable"; 585 type = types.package; 586 apply = p: p.override { 587 modules = lib.unique (p.modules ++ cfg.additionalModules); 588 }; 589 description = lib.mdDoc '' 590 Nginx package to use. This defaults to the stable version. Note 591 that the nginx team recommends to use the mainline version which 592 available in nixpkgs as `nginxMainline`. 593 ''; 594 }; 595 596 additionalModules = mkOption { 597 default = []; 598 type = types.listOf (types.attrsOf types.anything); 599 example = literalExpression "[ pkgs.nginxModules.echo ]"; 600 description = lib.mdDoc '' 601 Additional [third-party nginx modules](https://www.nginx.com/resources/wiki/modules/) 602 to install. Packaged modules are available in `pkgs.nginxModules`. 603 ''; 604 }; 605 606 logError = mkOption { 607 default = "stderr"; 608 type = types.str; 609 description = lib.mdDoc '' 610 Configures logging. 611 The first parameter defines a file that will store the log. The 612 special value stderr selects the standard error file. Logging to 613 syslog can be configured by specifying the syslog: prefix. 614 The second parameter determines the level of logging, and can be 615 one of the following: debug, info, notice, warn, error, crit, 616 alert, or emerg. Log levels above are listed in the order of 617 increasing severity. Setting a certain log level will cause all 618 messages of the specified and more severe log levels to be logged. 619 If this parameter is omitted then error is used. 620 ''; 621 }; 622 623 preStart = mkOption { 624 type = types.lines; 625 default = ""; 626 description = lib.mdDoc '' 627 Shell commands executed before the service's nginx is started. 628 ''; 629 }; 630 631 config = mkOption { 632 type = types.str; 633 default = ""; 634 description = lib.mdDoc '' 635 Verbatim {file}`nginx.conf` configuration. 636 This is mutually exclusive to any other config option for 637 {file}`nginx.conf` except for 638 - [](#opt-services.nginx.appendConfig) 639 - [](#opt-services.nginx.httpConfig) 640 - [](#opt-services.nginx.logError) 641 642 If additional verbatim config in addition to other options is needed, 643 [](#opt-services.nginx.appendConfig) should be used instead. 644 ''; 645 }; 646 647 appendConfig = mkOption { 648 type = types.lines; 649 default = ""; 650 description = lib.mdDoc '' 651 Configuration lines appended to the generated Nginx 652 configuration file. Commonly used by different modules 653 providing http snippets. {option}`appendConfig` 654 can be specified more than once and it's value will be 655 concatenated (contrary to {option}`config` which 656 can be set only once). 657 ''; 658 }; 659 660 commonHttpConfig = mkOption { 661 type = types.lines; 662 default = ""; 663 example = '' 664 resolver 127.0.0.1 valid=5s; 665 666 log_format myformat '$remote_addr - $remote_user [$time_local] ' 667 '"$request" $status $body_bytes_sent ' 668 '"$http_referer" "$http_user_agent"'; 669 ''; 670 description = lib.mdDoc '' 671 With nginx you must provide common http context definitions before 672 they are used, e.g. log_format, resolver, etc. inside of server 673 or location contexts. Use this attribute to set these definitions 674 at the appropriate location. 675 ''; 676 }; 677 678 httpConfig = mkOption { 679 type = types.lines; 680 default = ""; 681 description = lib.mdDoc '' 682 Configuration lines to be set inside the http block. 683 This is mutually exclusive with the structured configuration 684 via virtualHosts and the recommendedXyzSettings configuration 685 options. See appendHttpConfig for appending to the generated http block. 686 ''; 687 }; 688 689 streamConfig = mkOption { 690 type = types.lines; 691 default = ""; 692 example = '' 693 server { 694 listen 127.0.0.1:53 udp reuseport; 695 proxy_timeout 20s; 696 proxy_pass 192.168.0.1:53535; 697 } 698 ''; 699 description = lib.mdDoc '' 700 Configuration lines to be set inside the stream block. 701 ''; 702 }; 703 704 eventsConfig = mkOption { 705 type = types.lines; 706 default = ""; 707 description = lib.mdDoc '' 708 Configuration lines to be set inside the events block. 709 ''; 710 }; 711 712 appendHttpConfig = mkOption { 713 type = types.lines; 714 default = ""; 715 description = lib.mdDoc '' 716 Configuration lines to be appended to the generated http block. 717 This is mutually exclusive with using config and httpConfig for 718 specifying the whole http block verbatim. 719 ''; 720 }; 721 722 enableReload = mkOption { 723 default = false; 724 type = types.bool; 725 description = lib.mdDoc '' 726 Reload nginx when configuration file changes (instead of restart). 727 The configuration file is exposed at {file}`/etc/nginx/nginx.conf`. 728 See also `systemd.services.*.restartIfChanged`. 729 ''; 730 }; 731 732 user = mkOption { 733 type = types.str; 734 default = "nginx"; 735 description = lib.mdDoc "User account under which nginx runs."; 736 }; 737 738 group = mkOption { 739 type = types.str; 740 default = "nginx"; 741 description = lib.mdDoc "Group account under which nginx runs."; 742 }; 743 744 serverTokens = mkOption { 745 type = types.bool; 746 default = false; 747 description = lib.mdDoc "Show nginx version in headers and error pages."; 748 }; 749 750 clientMaxBodySize = mkOption { 751 type = types.str; 752 default = "10m"; 753 description = lib.mdDoc "Set nginx global client_max_body_size."; 754 }; 755 756 sslCiphers = mkOption { 757 type = types.nullOr types.str; 758 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate 759 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"; 760 description = lib.mdDoc "Ciphers to choose from when negotiating TLS handshakes."; 761 }; 762 763 sslProtocols = mkOption { 764 type = types.str; 765 default = "TLSv1.2 TLSv1.3"; 766 example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3"; 767 description = lib.mdDoc "Allowed TLS protocol versions."; 768 }; 769 770 sslDhparam = mkOption { 771 type = types.nullOr types.path; 772 default = null; 773 example = "/path/to/dhparams.pem"; 774 description = lib.mdDoc "Path to DH parameters file."; 775 }; 776 777 proxyResolveWhileRunning = mkOption { 778 type = types.bool; 779 default = false; 780 description = lib.mdDoc '' 781 Resolves domains of proxyPass targets at runtime 782 and not only at start, you have to set 783 services.nginx.resolver, too. 784 ''; 785 }; 786 787 mapHashBucketSize = mkOption { 788 type = types.nullOr (types.enum [ 32 64 128 ]); 789 default = null; 790 description = lib.mdDoc '' 791 Sets the bucket size for the map variables hash tables. Default 792 value depends on the processors cache line size. 793 ''; 794 }; 795 796 mapHashMaxSize = mkOption { 797 type = types.nullOr types.ints.positive; 798 default = null; 799 description = lib.mdDoc '' 800 Sets the maximum size of the map variables hash tables. 801 ''; 802 }; 803 804 serverNamesHashBucketSize = mkOption { 805 type = types.nullOr types.ints.positive; 806 default = null; 807 description = lib.mdDoc '' 808 Sets the bucket size for the server names hash tables. Default 809 value depends on the processors cache line size. 810 ''; 811 }; 812 813 serverNamesHashMaxSize = mkOption { 814 type = types.nullOr types.ints.positive; 815 default = null; 816 description = lib.mdDoc '' 817 Sets the maximum size of the server names hash tables. 818 ''; 819 }; 820 821 proxyCachePath = mkOption { 822 type = types.attrsOf (types.submodule ({ ... }: { 823 options = { 824 enable = mkEnableOption (lib.mdDoc "this proxy cache path entry"); 825 826 keysZoneName = mkOption { 827 type = types.str; 828 default = "cache"; 829 example = "my_cache"; 830 description = lib.mdDoc "Set name to shared memory zone."; 831 }; 832 833 keysZoneSize = mkOption { 834 type = types.str; 835 default = "10m"; 836 example = "32m"; 837 description = lib.mdDoc "Set size to shared memory zone."; 838 }; 839 840 levels = mkOption { 841 type = types.str; 842 default = "1:2"; 843 example = "1:2:2"; 844 description = lib.mdDoc '' 845 The levels parameter defines structure of subdirectories in cache: from 846 1 to 3, each level accepts values 1 or 2. Сan be used any combination of 847 1 and 2 in these formats: x, x:x and x:x:x. 848 ''; 849 }; 850 851 useTempPath = mkOption { 852 type = types.bool; 853 default = false; 854 example = true; 855 description = lib.mdDoc '' 856 Nginx first writes files that are destined for the cache to a temporary 857 storage area, and the use_temp_path=off directive instructs Nginx to 858 write them to the same directories where they will be cached. Recommended 859 that you set this parameter to off to avoid unnecessary copying of data 860 between file systems. 861 ''; 862 }; 863 864 inactive = mkOption { 865 type = types.str; 866 default = "10m"; 867 example = "1d"; 868 description = lib.mdDoc '' 869 Cached data that has not been accessed for the time specified by 870 the inactive parameter is removed from the cache, regardless of 871 its freshness. 872 ''; 873 }; 874 875 maxSize = mkOption { 876 type = types.str; 877 default = "1g"; 878 example = "2048m"; 879 description = lib.mdDoc "Set maximum cache size"; 880 }; 881 }; 882 })); 883 default = {}; 884 description = lib.mdDoc '' 885 Configure a proxy cache path entry. 886 See <http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path> for documentation. 887 ''; 888 }; 889 890 resolver = mkOption { 891 type = types.submodule { 892 options = { 893 addresses = mkOption { 894 type = types.listOf types.str; 895 default = []; 896 example = literalExpression ''[ "[::1]" "127.0.0.1:5353" ]''; 897 description = lib.mdDoc "List of resolvers to use"; 898 }; 899 valid = mkOption { 900 type = types.str; 901 default = ""; 902 example = "30s"; 903 description = lib.mdDoc '' 904 By default, nginx caches answers using the TTL value of a response. 905 An optional valid parameter allows overriding it 906 ''; 907 }; 908 ipv6 = mkOption { 909 type = types.bool; 910 default = true; 911 description = lib.mdDoc '' 912 By default, nginx will look up both IPv4 and IPv6 addresses while resolving. 913 If looking up of IPv6 addresses is not desired, the ipv6=off parameter can be 914 specified. 915 ''; 916 }; 917 }; 918 }; 919 description = lib.mdDoc '' 920 Configures name servers used to resolve names of upstream servers into addresses 921 ''; 922 default = {}; 923 }; 924 925 upstreams = mkOption { 926 type = types.attrsOf (types.submodule { 927 options = { 928 servers = mkOption { 929 type = types.attrsOf (types.submodule { 930 freeformType = types.attrsOf (types.oneOf [ types.bool types.int types.str ]); 931 options = { 932 backup = mkOption { 933 type = types.bool; 934 default = false; 935 description = lib.mdDoc '' 936 Marks the server as a backup server. It will be passed 937 requests when the primary servers are unavailable. 938 ''; 939 }; 940 }; 941 }); 942 description = lib.mdDoc '' 943 Defines the address and other parameters of the upstream servers. 944 See [the documentation](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#server) 945 for the available parameters. 946 ''; 947 default = {}; 948 example = lib.literalMD "see [](#opt-services.nginx.upstreams)"; 949 }; 950 extraConfig = mkOption { 951 type = types.lines; 952 default = ""; 953 description = lib.mdDoc '' 954 These lines go to the end of the upstream verbatim. 955 ''; 956 }; 957 }; 958 }); 959 description = lib.mdDoc '' 960 Defines a group of servers to use as proxy target. 961 ''; 962 default = {}; 963 example = { 964 "backend" = { 965 servers = { 966 "backend1.example.com:8080" = { weight = 5; }; 967 "backend2.example.com" = { max_fails = 3; fail_timeout = "30s"; }; 968 "backend3.example.com" = {}; 969 "backup1.example.com" = { backup = true; }; 970 "backup2.example.com" = { backup = true; }; 971 }; 972 extraConfig = '' 973 keepalive 16; 974 ''; 975 }; 976 "memcached" = { 977 servers."unix:/run//memcached/memcached.sock" = {}; 978 }; 979 }; 980 }; 981 982 virtualHosts = mkOption { 983 type = types.attrsOf (types.submodule (import ./vhost-options.nix { 984 inherit config lib; 985 })); 986 default = { 987 localhost = {}; 988 }; 989 example = literalExpression '' 990 { 991 "hydra.example.com" = { 992 forceSSL = true; 993 enableACME = true; 994 locations."/" = { 995 proxyPass = "http://localhost:3000"; 996 }; 997 }; 998 }; 999 ''; 1000 description = lib.mdDoc "Declarative vhost config"; 1001 }; 1002 }; 1003 }; 1004 1005 imports = [ 1006 (mkRemovedOptionModule [ "services" "nginx" "stateDir" ] '' 1007 The Nginx log directory has been moved to /var/log/nginx, the cache directory 1008 to /var/cache/nginx. The option services.nginx.stateDir has been removed. 1009 '') 1010 (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "inactive" ] [ "services" "nginx" "proxyCachePath" "" "inactive" ]) 1011 (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "useTempPath" ] [ "services" "nginx" "proxyCachePath" "" "useTempPath" ]) 1012 (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "levels" ] [ "services" "nginx" "proxyCachePath" "" "levels" ]) 1013 (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "keysZoneSize" ] [ "services" "nginx" "proxyCachePath" "" "keysZoneSize" ]) 1014 (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "keysZoneName" ] [ "services" "nginx" "proxyCachePath" "" "keysZoneName" ]) 1015 (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "enable" ] [ "services" "nginx" "proxyCachePath" "" "enable" ]) 1016 ]; 1017 1018 config = mkIf cfg.enable { 1019 warnings = 1020 let 1021 deprecatedSSL = name: config: optional config.enableSSL 1022 '' 1023 config.services.nginx.virtualHosts.<name>.enableSSL is deprecated, 1024 use config.services.nginx.virtualHosts.<name>.onlySSL instead. 1025 ''; 1026 1027 in flatten (mapAttrsToList deprecatedSSL virtualHosts); 1028 1029 assertions = 1030 let 1031 hostOrAliasIsNull = l: l.root == null || l.alias == null; 1032 in [ 1033 { 1034 assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts); 1035 message = "Only one of nginx root or alias can be specified on a location."; 1036 } 1037 1038 { 1039 assertion = all (host: with host; 1040 count id [ addSSL (onlySSL || enableSSL) forceSSL rejectSSL ] <= 1 1041 ) (attrValues virtualHosts); 1042 message = '' 1043 Options services.nginx.service.virtualHosts.<name>.addSSL, 1044 services.nginx.virtualHosts.<name>.onlySSL, 1045 services.nginx.virtualHosts.<name>.forceSSL and 1046 services.nginx.virtualHosts.<name>.rejectSSL are mutually exclusive. 1047 ''; 1048 } 1049 1050 { 1051 assertion = any (host: host.rejectSSL) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.19.4"; 1052 message = '' 1053 services.nginx.virtualHosts.<name>.rejectSSL requires nginx version 1054 1.19.4 or above; see the documentation for services.nginx.package. 1055 ''; 1056 } 1057 1058 { 1059 assertion = any (host: host.kTLS) (attrValues virtualHosts) -> versionAtLeast cfg.package.version "1.21.4"; 1060 message = '' 1061 services.nginx.virtualHosts.<name>.kTLS requires nginx version 1062 1.21.4 or above; see the documentation for services.nginx.package. 1063 ''; 1064 } 1065 1066 { 1067 assertion = all (host: !(host.enableACME && host.useACMEHost != null)) (attrValues virtualHosts); 1068 message = '' 1069 Options services.nginx.service.virtualHosts.<name>.enableACME and 1070 services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive. 1071 ''; 1072 } 1073 1074 { 1075 assertion = cfg.package.pname != "nginxQuic" -> all (host: !host.quic) (attrValues virtualHosts); 1076 message = '' 1077 services.nginx.service.virtualHosts.<name>.quic requires using nginxQuic package, 1078 which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`. 1079 ''; 1080 } 1081 ] ++ map (name: mkCertOwnershipAssertion { 1082 inherit (cfg) group user; 1083 cert = config.security.acme.certs.${name}; 1084 groups = config.users.groups; 1085 }) dependentCertNames; 1086 1087 services.nginx.additionalModules = optional cfg.recommendedBrotliSettings pkgs.nginxModules.brotli 1088 ++ lib.optional cfg.recommendedZstdSettings pkgs.nginxModules.zstd; 1089 1090 systemd.services.nginx = { 1091 description = "Nginx Web Server"; 1092 wantedBy = [ "multi-user.target" ]; 1093 wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames); 1094 after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames; 1095 # Nginx needs to be started in order to be able to request certificates 1096 # (it's hosting the acme challenge after all) 1097 # This fixes https://github.com/NixOS/nixpkgs/issues/81842 1098 before = map (certName: "acme-${certName}.service") dependentCertNames; 1099 stopIfChanged = false; 1100 preStart = '' 1101 ${cfg.preStart} 1102 ${execCommand} -t 1103 ''; 1104 1105 startLimitIntervalSec = 60; 1106 serviceConfig = { 1107 ExecStart = execCommand; 1108 ExecReload = [ 1109 "${execCommand} -t" 1110 "${pkgs.coreutils}/bin/kill -HUP $MAINPID" 1111 ]; 1112 Restart = "always"; 1113 RestartSec = "10s"; 1114 # User and group 1115 User = cfg.user; 1116 Group = cfg.group; 1117 # Runtime directory and mode 1118 RuntimeDirectory = "nginx"; 1119 RuntimeDirectoryMode = "0750"; 1120 # Cache directory and mode 1121 CacheDirectory = "nginx"; 1122 CacheDirectoryMode = "0750"; 1123 # Logs directory and mode 1124 LogsDirectory = "nginx"; 1125 LogsDirectoryMode = "0750"; 1126 # Proc filesystem 1127 ProcSubset = "pid"; 1128 ProtectProc = "invisible"; 1129 # New file permissions 1130 UMask = "0027"; # 0640 / 0750 1131 # Capabilities 1132 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ]; 1133 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ]; 1134 # Security 1135 NoNewPrivileges = true; 1136 # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html) 1137 ProtectSystem = "strict"; 1138 ProtectHome = mkDefault true; 1139 PrivateTmp = true; 1140 PrivateDevices = true; 1141 ProtectHostname = true; 1142 ProtectClock = true; 1143 ProtectKernelTunables = true; 1144 ProtectKernelModules = true; 1145 ProtectKernelLogs = true; 1146 ProtectControlGroups = true; 1147 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 1148 RestrictNamespaces = true; 1149 LockPersonality = true; 1150 MemoryDenyWriteExecute = !((builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules) || (cfg.package == pkgs.openresty)); 1151 RestrictRealtime = true; 1152 RestrictSUIDSGID = true; 1153 RemoveIPC = true; 1154 PrivateMounts = true; 1155 # System Call Filtering 1156 SystemCallArchitectures = "native"; 1157 SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" ] 1158 ++ optionals ((cfg.package != pkgs.tengine) && (cfg.package != pkgs.openresty) && (!lib.any (mod: (mod.disableIPC or false)) cfg.package.modules)) [ "~@ipc" ]; 1159 }; 1160 }; 1161 1162 environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload { 1163 source = configFile; 1164 }; 1165 1166 # This service waits for all certificates to be available 1167 # before reloading nginx configuration. 1168 # sslTargets are added to wantedBy + before 1169 # which allows the acme-finished-$cert.target to signify the successful updating 1170 # of certs end-to-end. 1171 systemd.services.nginx-config-reload = let 1172 sslServices = map (certName: "acme-${certName}.service") dependentCertNames; 1173 sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames; 1174 in mkIf (cfg.enableReload || sslServices != []) { 1175 wants = optionals cfg.enableReload [ "nginx.service" ]; 1176 wantedBy = sslServices ++ [ "multi-user.target" ]; 1177 # Before the finished targets, after the renew services. 1178 # This service might be needed for HTTP-01 challenges, but we only want to confirm 1179 # certs are updated _after_ config has been reloaded. 1180 before = sslTargets; 1181 after = sslServices; 1182 restartTriggers = optionals cfg.enableReload [ configFile ]; 1183 # Block reloading if not all certs exist yet. 1184 # Happens when config changes add new vhosts/certs. 1185 unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames); 1186 serviceConfig = { 1187 Type = "oneshot"; 1188 TimeoutSec = 60; 1189 ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service"; 1190 ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service"; 1191 }; 1192 }; 1193 1194 security.acme.certs = let 1195 acmePairs = map (vhostConfig: let 1196 hasRoot = vhostConfig.acmeRoot != null; 1197 in nameValuePair vhostConfig.serverName { 1198 group = mkDefault cfg.group; 1199 # if acmeRoot is null inherit config.security.acme 1200 # Since config.security.acme.certs.<cert>.webroot's own default value 1201 # should take precedence set priority higher than mkOptionDefault 1202 webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot; 1203 # Also nudge dnsProvider to null in case it is inherited 1204 dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null; 1205 extraDomainNames = vhostConfig.serverAliases; 1206 # Filter for enableACME-only vhosts. Don't want to create dud certs 1207 }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts); 1208 in listToAttrs acmePairs; 1209 1210 users.users = optionalAttrs (cfg.user == "nginx") { 1211 nginx = { 1212 group = cfg.group; 1213 isSystemUser = true; 1214 uid = config.ids.uids.nginx; 1215 }; 1216 }; 1217 1218 users.groups = optionalAttrs (cfg.group == "nginx") { 1219 nginx.gid = config.ids.gids.nginx; 1220 }; 1221 1222 services.logrotate.settings.nginx = mapAttrs (_: mkDefault) { 1223 files = "/var/log/nginx/*.log"; 1224 frequency = "weekly"; 1225 su = "${cfg.user} ${cfg.group}"; 1226 rotate = 26; 1227 compress = true; 1228 delaycompress = true; 1229 postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`"; 1230 }; 1231 }; 1232}