1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.nginx;
12 inherit (config.security.acme) certs;
13 vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
14 acmeEnabledVhosts = filter (
15 vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null
16 ) vhostsConfigs;
17 vhostCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
18 dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server
19 independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server
20 virtualHosts = mapAttrs (
21 vhostName: vhostConfig:
22 let
23 serverName = if vhostConfig.serverName != null then vhostConfig.serverName else vhostName;
24 certName = if vhostConfig.useACMEHost != null then vhostConfig.useACMEHost else serverName;
25 in
26 vhostConfig
27 // {
28 inherit serverName certName;
29 }
30 // (optionalAttrs (vhostConfig.enableACME || vhostConfig.useACMEHost != null) {
31 sslCertificate = "${certs.${certName}.directory}/fullchain.pem";
32 sslCertificateKey = "${certs.${certName}.directory}/key.pem";
33 sslTrustedCertificate =
34 if vhostConfig.sslTrustedCertificate != null then
35 vhostConfig.sslTrustedCertificate
36 else
37 "${certs.${certName}.directory}/chain.pem";
38 })
39 ) cfg.virtualHosts;
40 inherit (config.networking) enableIPv6;
41
42 # Mime.types values are taken from brotli sample configuration - https://github.com/google/ngx_brotli
43 # and Nginx Server Configs - https://github.com/h5bp/server-configs-nginx
44 # "text/html" is implicitly included in {brotli,gzip,zstd}_types
45 compressMimeTypes = [
46 "application/atom+xml"
47 "application/geo+json"
48 "application/javascript" # Deprecated by IETF RFC 9239, but still widely used
49 "application/json"
50 "application/ld+json"
51 "application/manifest+json"
52 "application/rdf+xml"
53 "application/vnd.ms-fontobject"
54 "application/wasm"
55 "application/x-rss+xml"
56 "application/x-web-app-manifest+json"
57 "application/xhtml+xml"
58 "application/xliff+xml"
59 "application/xml"
60 "font/collection"
61 "font/otf"
62 "font/ttf"
63 "image/bmp"
64 "image/svg+xml"
65 "image/vnd.microsoft.icon"
66 "text/cache-manifest"
67 "text/calendar"
68 "text/css"
69 "text/csv"
70 "text/javascript"
71 "text/markdown"
72 "text/plain"
73 "text/vcard"
74 "text/vnd.rim.location.xloc"
75 "text/vtt"
76 "text/x-component"
77 "text/xml"
78 ];
79
80 defaultFastcgiParams = {
81 SCRIPT_FILENAME = "$document_root$fastcgi_script_name";
82 QUERY_STRING = "$query_string";
83 REQUEST_METHOD = "$request_method";
84 CONTENT_TYPE = "$content_type";
85 CONTENT_LENGTH = "$content_length";
86
87 SCRIPT_NAME = "$fastcgi_script_name";
88 REQUEST_URI = "$request_uri";
89 DOCUMENT_URI = "$document_uri";
90 DOCUMENT_ROOT = "$document_root";
91 SERVER_PROTOCOL = "$server_protocol";
92 REQUEST_SCHEME = "$scheme";
93 HTTPS = "$https if_not_empty";
94
95 GATEWAY_INTERFACE = "CGI/1.1";
96 SERVER_SOFTWARE = "nginx/$nginx_version";
97
98 REMOTE_ADDR = "$remote_addr";
99 REMOTE_PORT = "$remote_port";
100 SERVER_ADDR = "$server_addr";
101 SERVER_PORT = "$server_port";
102 SERVER_NAME = "$server_name";
103
104 REDIRECT_STATUS = "200";
105 };
106
107 recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy_set_header-headers.conf" ''
108 proxy_set_header Host $host;
109 proxy_set_header X-Real-IP $remote_addr;
110 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
111 proxy_set_header X-Forwarded-Proto $scheme;
112 proxy_set_header X-Forwarded-Host $host;
113 proxy_set_header X-Forwarded-Server $host;
114 '';
115
116 proxyCachePathConfig = concatStringsSep "\n" (
117 mapAttrsToList (name: proxyCachePath: ''
118 proxy_cache_path ${
119 concatStringsSep " " [
120 "/var/cache/nginx/${name}"
121 "keys_zone=${proxyCachePath.keysZoneName}:${proxyCachePath.keysZoneSize}"
122 "levels=${proxyCachePath.levels}"
123 "use_temp_path=${if proxyCachePath.useTempPath then "on" else "off"}"
124 "inactive=${proxyCachePath.inactive}"
125 "max_size=${proxyCachePath.maxSize}"
126 ]
127 };
128 '') (filterAttrs (name: conf: conf.enable) cfg.proxyCachePath)
129 );
130
131 toUpstreamParameter =
132 key: value:
133 if builtins.isBool value then lib.optionalString value key else "${key}=${toString value}";
134
135 upstreamConfig = toString (
136 flip mapAttrsToList cfg.upstreams (
137 name: upstream: ''
138 upstream ${name} {
139 ${toString (
140 flip mapAttrsToList upstream.servers (
141 name: server: ''
142 server ${name} ${concatStringsSep " " (mapAttrsToList toUpstreamParameter server)};
143 ''
144 )
145 )}
146 ${upstream.extraConfig}
147 }
148 ''
149 )
150 );
151
152 commonHttpConfig = ''
153 # Load mime types and configure maximum size of the types hash tables.
154 include ${cfg.defaultMimeTypes};
155 types_hash_max_size ${toString cfg.typesHashMaxSize};
156
157 include ${cfg.package}/conf/fastcgi.conf;
158 include ${cfg.package}/conf/uwsgi_params;
159
160 default_type application/octet-stream;
161 '';
162
163 configFile =
164 (if cfg.validateConfigFile then pkgs.writers.writeNginxConfig else pkgs.writeText) "nginx.conf"
165 ''
166 pid /run/nginx/nginx.pid;
167 error_log ${cfg.logError};
168 daemon off;
169
170 ${optionalString cfg.enableQuicBPF ''
171 quic_bpf on;
172 ''}
173
174 ${cfg.config}
175
176 ${optionalString (cfg.eventsConfig != "" || cfg.config == "") ''
177 events {
178 ${cfg.eventsConfig}
179 }
180 ''}
181
182 ${optionalString (cfg.httpConfig == "" && cfg.config == "") ''
183 http {
184 ${commonHttpConfig}
185
186 ${optionalString (cfg.resolver.addresses != [ ]) ''
187 resolver ${toString cfg.resolver.addresses} ${
188 optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"
189 } ${optionalString (!cfg.resolver.ipv4) "ipv4=off"} ${
190 optionalString (!cfg.resolver.ipv6) "ipv6=off"
191 };
192 ''}
193 ${upstreamConfig}
194
195 ${optionalString cfg.recommendedOptimisation ''
196 # optimisation
197 sendfile on;
198 tcp_nopush on;
199 tcp_nodelay on;
200 keepalive_timeout 65;
201 ''}
202
203 ssl_protocols ${cfg.sslProtocols};
204 ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
205 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
206
207 ${optionalString cfg.recommendedTlsSettings ''
208 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
209
210 ssl_session_timeout 1d;
211 ssl_session_cache shared:SSL:10m;
212 # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135
213 ssl_session_tickets off;
214 # We don't enable insecure ciphers by default, so this allows
215 # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260
216 ssl_prefer_server_ciphers off;
217
218 # OCSP stapling
219 ssl_stapling on;
220 ssl_stapling_verify on;
221 ''}
222
223 ${optionalString cfg.recommendedBrotliSettings ''
224 brotli on;
225 brotli_static on;
226 brotli_comp_level 5;
227 brotli_window 512k;
228 brotli_min_length 256;
229 brotli_types ${lib.concatStringsSep " " compressMimeTypes};
230 ''}
231
232 ${optionalString cfg.recommendedGzipSettings
233 # https://docs.nginx.com/nginx/admin-guide/web-server/compression/
234 ''
235 gzip on;
236 gzip_static on;
237 gzip_vary on;
238 gzip_comp_level 5;
239 gzip_min_length 256;
240 gzip_proxied expired no-cache no-store private auth;
241 gzip_types ${lib.concatStringsSep " " compressMimeTypes};
242 ''
243 }
244
245 ${optionalString cfg.recommendedZstdSettings ''
246 zstd on;
247 zstd_comp_level 9;
248 zstd_min_length 256;
249 zstd_static on;
250 zstd_types ${lib.concatStringsSep " " compressMimeTypes};
251 ''}
252
253 ${optionalString cfg.recommendedProxySettings ''
254 proxy_redirect off;
255 proxy_connect_timeout ${cfg.proxyTimeout};
256 proxy_send_timeout ${cfg.proxyTimeout};
257 proxy_read_timeout ${cfg.proxyTimeout};
258 proxy_http_version 1.1;
259 # don't let clients close the keep-alive connection to upstream. See the nginx blog for details:
260 # https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives
261 proxy_set_header "Connection" "";
262 include ${recommendedProxyConfig};
263 ''}
264
265 ${optionalString cfg.recommendedUwsgiSettings ''
266 uwsgi_connect_timeout ${cfg.uwsgiTimeout};
267 uwsgi_send_timeout ${cfg.uwsgiTimeout};
268 uwsgi_read_timeout ${cfg.uwsgiTimeout};
269 uwsgi_param HTTP_CONNECTION "";
270 include ${cfg.package}/conf/uwsgi_params;
271 ''}
272
273 ${optionalString (cfg.mapHashBucketSize != null) ''
274 map_hash_bucket_size ${toString cfg.mapHashBucketSize};
275 ''}
276
277 ${optionalString (cfg.mapHashMaxSize != null) ''
278 map_hash_max_size ${toString cfg.mapHashMaxSize};
279 ''}
280
281 ${optionalString (cfg.serverNamesHashBucketSize != null) ''
282 server_names_hash_bucket_size ${toString cfg.serverNamesHashBucketSize};
283 ''}
284
285 ${optionalString (cfg.serverNamesHashMaxSize != null) ''
286 server_names_hash_max_size ${toString cfg.serverNamesHashMaxSize};
287 ''}
288
289 # $connection_upgrade is used for websocket proxying
290 map $http_upgrade $connection_upgrade {
291 default upgrade;
292 ''' close;
293 }
294 client_max_body_size ${cfg.clientMaxBodySize};
295
296 server_tokens ${if cfg.serverTokens then "on" else "off"};
297
298 ${cfg.commonHttpConfig}
299
300 ${proxyCachePathConfig}
301
302 ${vhosts}
303
304 ${cfg.appendHttpConfig}
305 }''}
306
307 ${optionalString (cfg.httpConfig != "") ''
308 http {
309 ${commonHttpConfig}
310 ${cfg.httpConfig}
311 }''}
312
313 ${optionalString (cfg.streamConfig != "") ''
314 stream {
315 ${cfg.streamConfig}
316 }
317 ''}
318
319 ${cfg.appendConfig}
320 '';
321
322 configPath = if cfg.enableReload then "/etc/nginx/nginx.conf" else configFile;
323
324 execCommand = "${cfg.package}/bin/nginx -c '${configPath}'";
325
326 vhosts = concatStringsSep "\n" (
327 mapAttrsToList (
328 vhostName: vhost:
329 let
330 onlySSL = vhost.onlySSL || vhost.enableSSL;
331 hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL;
332
333 # First evaluation of defaultListen based on a set of listen lines.
334 mkDefaultListenVhost =
335 listenLines:
336 # If this vhost has SSL or is a SSL rejection host.
337 # We enable a TLS variant for lines without explicit ssl or ssl = true.
338 optionals (hasSSL || vhost.rejectSSL) (
339 map (
340 listen:
341 {
342 port = if (hasPrefix "unix:" listen.addr) then null else cfg.defaultSSLListenPort;
343 ssl = true;
344 }
345 // listen
346 ) (filter (listen: !(listen ? ssl) || listen.ssl) listenLines)
347 )
348 # If this vhost is supposed to serve HTTP
349 # We provide listen lines for those without explicit ssl or ssl = false.
350 ++ optionals (!onlySSL) (
351 map (
352 listen:
353 {
354 port = if (hasPrefix "unix:" listen.addr) then null else cfg.defaultHTTPListenPort;
355 ssl = false;
356 }
357 // listen
358 ) (filter (listen: !(listen ? ssl) || !listen.ssl) listenLines)
359 );
360
361 defaultListen =
362 if vhost.listen != [ ] then
363 vhost.listen
364 else if cfg.defaultListen != [ ] then
365 mkDefaultListenVhost
366 # Cleanup nulls which will mess up with //.
367 # TODO: is there a better way to achieve this? i.e. mergeButIgnoreNullPlease?
368 (map (listenLine: filterAttrs (_: v: (v != null)) listenLine) cfg.defaultListen)
369 else
370 let
371 addrs = if vhost.listenAddresses != [ ] then vhost.listenAddresses else cfg.defaultListenAddresses;
372 in
373 mkDefaultListenVhost (map (addr: { inherit addr; }) addrs);
374
375 hostListen = if vhost.forceSSL then filter (x: x.ssl) defaultListen else defaultListen;
376
377 listenString =
378 {
379 addr,
380 port,
381 ssl,
382 proxyProtocol ? false,
383 extraParameters ? [ ],
384 ...
385 }:
386 # UDP listener for QUIC transport protocol.
387 (optionalString (ssl && vhost.quic) (
388 "
389 listen ${addr}${optionalString (port != null) ":${toString port}"} quic "
390 + optionalString vhost.default "default_server "
391 + optionalString vhost.reuseport "reuseport "
392 + optionalString (extraParameters != [ ]) (
393 concatStringsSep " " (
394 let
395 inCompatibleParameters = [
396 "accept_filter"
397 "backlog"
398 "deferred"
399 "fastopen"
400 "http2"
401 "proxy_protocol"
402 "so_keepalive"
403 "ssl"
404 ];
405 isCompatibleParameter = param: !(any (p: lib.hasPrefix p param) inCompatibleParameters);
406 in
407 filter isCompatibleParameter extraParameters
408 )
409 )
410 + ";"
411 ))
412 + "
413 listen ${addr}${optionalString (port != null) ":${toString port}"} "
414 + optionalString (ssl && vhost.http2 && oldHTTP2) "http2 "
415 + optionalString ssl "ssl "
416 + optionalString vhost.default "default_server "
417 + optionalString vhost.reuseport "reuseport "
418 + optionalString proxyProtocol "proxy_protocol "
419 + optionalString (extraParameters != [ ]) (concatStringsSep " " extraParameters)
420 + ";";
421
422 redirectListen = filter (x: !x.ssl) defaultListen;
423
424 # The acme-challenge location doesn't need to be added if we are not using any automated
425 # certificate provisioning and can also be omitted when we use a certificate obtained via a DNS-01 challenge
426 acmeName = if vhost.useACMEHost != null then vhost.useACMEHost else vhost.serverName;
427 acmeLocation =
428 optionalString
429 (
430 (vhost.enableACME || vhost.useACMEHost != null)
431 && config.security.acme.certs.${acmeName}.dnsProvider == null
432 )
433 # Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
434 # We use ^~ here, so that we don't check any regexes (which could
435 # otherwise easily override this intended match accidentally).
436 ''
437 location ^~ /.well-known/acme-challenge/ {
438 ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
439 ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"}
440 auth_basic off;
441 auth_request off;
442 }
443 ${optionalString (vhost.acmeFallbackHost != null) ''
444 location @acme-fallback {
445 auth_basic off;
446 auth_request off;
447 proxy_pass http://${vhost.acmeFallbackHost};
448 }
449 ''}
450 '';
451
452 in
453 ''
454 ${optionalString vhost.forceSSL ''
455 server {
456 ${concatMapStringsSep "\n" listenString redirectListen}
457
458 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
459
460 location / {
461 return ${toString vhost.redirectCode} https://$host$request_uri;
462 }
463 ${acmeLocation}
464 }
465 ''}
466
467 server {
468 ${concatMapStringsSep "\n" listenString hostListen}
469 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
470 ${optionalString (hasSSL && vhost.http2 && !oldHTTP2) ''
471 http2 on;
472 ''}
473 ${optionalString (hasSSL && vhost.quic) ''
474 http3 ${if vhost.http3 then "on" else "off"};
475 http3_hq ${if vhost.http3_hq then "on" else "off"};
476 ''}
477 ${optionalString hasSSL ''
478 ssl_certificate ${vhost.sslCertificate};
479 ssl_certificate_key ${vhost.sslCertificateKey};
480 ''}
481 ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) ''
482 ssl_trusted_certificate ${vhost.sslTrustedCertificate};
483 ''}
484 ${optionalString vhost.rejectSSL ''
485 ssl_reject_handshake on;
486 ''}
487 ${optionalString (hasSSL && vhost.kTLS) ''
488 ssl_conf_command Options KTLS;
489 ''}
490
491 ${mkBasicAuth vhostName vhost}
492
493 ${optionalString (vhost.root != null) "root ${vhost.root};"}
494
495 ${optionalString (vhost.globalRedirect != null) ''
496 location / {
497 return ${toString vhost.redirectCode} http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri;
498 }
499 ''}
500 ${acmeLocation}
501 ${mkLocations vhost.locations}
502
503 ${vhost.extraConfig}
504 }
505 ''
506 ) virtualHosts
507 );
508 mkLocations =
509 locations:
510 concatStringsSep "\n" (
511 map (config: ''
512 location ${config.location} {
513 ${optionalString (
514 config.proxyPass != null && !cfg.proxyResolveWhileRunning
515 ) "proxy_pass ${config.proxyPass};"}
516 ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) ''
517 set $nix_proxy_target "${config.proxyPass}";
518 proxy_pass $nix_proxy_target;
519 ''}
520 ${optionalString config.proxyWebsockets ''
521 proxy_http_version 1.1;
522 proxy_set_header Upgrade $http_upgrade;
523 proxy_set_header Connection $connection_upgrade;
524 ''}
525 ${optionalString (
526 config.uwsgiPass != null && !cfg.uwsgiResolveWhileRunning
527 ) "uwsgi_pass ${config.uwsgiPass};"}
528 ${optionalString (config.uwsgiPass != null && cfg.uwsgiResolveWhileRunning) ''
529 set $nix_proxy_target "${config.uwsgiPass}";
530 uwsgi_pass $nix_proxy_target;
531 ''}
532 ${concatStringsSep "\n" (
533 mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'') (
534 optionalAttrs (config.fastcgiParams != { }) (defaultFastcgiParams // config.fastcgiParams)
535 )
536 )}
537 ${optionalString (config.index != null) "index ${config.index};"}
538 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
539 ${optionalString (config.root != null) "root ${config.root};"}
540 ${optionalString (config.alias != null) "alias ${config.alias};"}
541 ${optionalString (config.return != null) "return ${toString config.return};"}
542 ${config.extraConfig}
543 ${optionalString (
544 config.proxyPass != null && config.recommendedProxySettings
545 ) "include ${recommendedProxyConfig};"}
546 ${optionalString (
547 config.uwsgiPass != null && config.recommendedUwsgiSettings
548 ) "include ${cfg.package}/conf/uwsgi_params;"}
549 ${mkBasicAuth "sublocation" config}
550 }
551 '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))
552 );
553
554 mkBasicAuth =
555 name: zone:
556 optionalString (zone.basicAuthFile != null || zone.basicAuth != { }) (
557 let
558 auth_file =
559 if zone.basicAuthFile != null then zone.basicAuthFile else mkHtpasswd name zone.basicAuth;
560 in
561 ''
562 auth_basic secured;
563 auth_basic_user_file ${auth_file};
564 ''
565 );
566 mkHtpasswd =
567 name: authDef:
568 pkgs.writeText "${name}.htpasswd" (
569 concatStringsSep "\n" (
570 mapAttrsToList (user: password: ''
571 ${user}:{PLAIN}${password}
572 '') authDef
573 )
574 );
575
576 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
577
578 oldHTTP2 = (
579 versionOlder cfg.package.version "1.25.1"
580 && !(cfg.package.pname == "angie" || cfg.package.pname == "angieQuic")
581 );
582in
583
584{
585 options = {
586 services.nginx = {
587 enable = mkEnableOption "Nginx Web Server";
588
589 statusPage = mkOption {
590 default = false;
591 type = types.bool;
592 description = ''
593 Enable status page reachable from localhost on http://127.0.0.1/nginx_status.
594 '';
595 };
596
597 recommendedTlsSettings = mkOption {
598 default = false;
599 type = types.bool;
600 description = ''
601 Enable recommended TLS settings.
602 '';
603 };
604
605 recommendedOptimisation = mkOption {
606 default = false;
607 type = types.bool;
608 description = ''
609 Enable recommended optimisation settings.
610 '';
611 };
612
613 recommendedBrotliSettings = mkOption {
614 default = false;
615 type = types.bool;
616 description = ''
617 Enable recommended brotli settings.
618 Learn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/).
619
620 This adds `pkgs.nginxModules.brotli` to `services.nginx.additionalModules`.
621 '';
622 };
623
624 recommendedGzipSettings = mkOption {
625 default = false;
626 type = types.bool;
627 description = ''
628 Enable recommended gzip settings.
629 Learn more about compression in Gzip format [here](https://docs.nginx.com/nginx/admin-guide/web-server/compression/).
630 '';
631 };
632
633 recommendedZstdSettings = mkOption {
634 default = false;
635 type = types.bool;
636 description = ''
637 Enable recommended zstd settings.
638 Learn more about compression in Zstd format [here](https://github.com/tokers/zstd-nginx-module).
639
640 This adds `pkgs.nginxModules.zstd` to `services.nginx.additionalModules`.
641 '';
642 };
643
644 recommendedProxySettings = mkOption {
645 default = false;
646 type = types.bool;
647 description = ''
648 Whether to enable recommended proxy settings if a vhost does not specify the option manually.
649 '';
650 };
651
652 proxyTimeout = mkOption {
653 type = types.str;
654 default = "60s";
655 example = "20s";
656 description = ''
657 Change the proxy related timeouts in recommendedProxySettings.
658 '';
659 };
660
661 recommendedUwsgiSettings = mkOption {
662 default = false;
663 type = types.bool;
664 description = ''
665 Whether to enable recommended uwsgi settings if a vhost does not specify the option manually.
666 '';
667 };
668
669 uwsgiTimeout = mkOption {
670 type = types.str;
671 default = "60s";
672 example = "20s";
673 description = ''
674 Change the uwsgi related timeouts in recommendedUwsgiSettings.
675 '';
676 };
677
678 defaultListen = mkOption {
679 type =
680 with types;
681 listOf (submodule {
682 options = {
683 addr = mkOption {
684 type = str;
685 description = "IP address.";
686 };
687 port = mkOption {
688 type = nullOr port;
689 description = "Port number.";
690 default = null;
691 };
692 ssl = mkOption {
693 type = nullOr bool;
694 default = null;
695 description = "Enable SSL.";
696 };
697 proxyProtocol = mkOption {
698 type = bool;
699 description = "Enable PROXY protocol.";
700 default = false;
701 };
702 extraParameters = mkOption {
703 type = listOf str;
704 description = "Extra parameters of this listen directive.";
705 default = [ ];
706 example = [
707 "backlog=1024"
708 "deferred"
709 ];
710 };
711 };
712 });
713 default = [ ];
714 example = literalExpression ''
715 [
716 { addr = "10.0.0.12"; proxyProtocol = true; ssl = true; }
717 { addr = "0.0.0.0"; }
718 { addr = "[::0]"; }
719 ]
720 '';
721 description = ''
722 If vhosts do not specify listen, use these addresses by default.
723 This option takes precedence over {option}`defaultListenAddresses` and
724 other listen-related defaults options.
725 '';
726 };
727
728 defaultListenAddresses = mkOption {
729 type = types.listOf types.str;
730 default = [ "0.0.0.0" ] ++ optional enableIPv6 "[::0]";
731 defaultText = literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
732 example = literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
733 description = ''
734 If vhosts do not specify listenAddresses, use these addresses by default.
735 This is akin to writing `defaultListen = [ { addr = "0.0.0.0" } ]`.
736 '';
737 };
738
739 defaultHTTPListenPort = mkOption {
740 type = types.port;
741 default = 80;
742 example = 8080;
743 description = ''
744 If vhosts do not specify listen.port, use these ports for HTTP by default.
745 '';
746 };
747
748 defaultSSLListenPort = mkOption {
749 type = types.port;
750 default = 443;
751 example = 8443;
752 description = ''
753 If vhosts do not specify listen.port, use these ports for SSL by default.
754 '';
755 };
756
757 defaultMimeTypes = mkOption {
758 type = types.path;
759 default = "${pkgs.mailcap}/etc/nginx/mime.types";
760 defaultText = literalExpression "$''{pkgs.mailcap}/etc/nginx/mime.types";
761 example = literalExpression "$''{pkgs.nginx}/conf/mime.types";
762 description = ''
763 Default MIME types for NGINX, as MIME types definitions from NGINX are very incomplete,
764 we use by default the ones bundled in the mailcap package, used by most of the other
765 Linux distributions.
766 '';
767 };
768
769 package = mkOption {
770 default = pkgs.nginxStable;
771 defaultText = literalExpression "pkgs.nginxStable";
772 type = types.package;
773 apply =
774 p:
775 p.override {
776 modules = lib.unique (p.modules ++ cfg.additionalModules);
777 };
778 description = ''
779 Nginx package to use. This defaults to the stable version. Note
780 that the nginx team recommends to use the mainline version which
781 available in nixpkgs as `nginxMainline`.
782 Supported Nginx forks include `angie`, `openresty` and `tengine`.
783 For HTTP/3 support use `nginxQuic` or `angieQuic`.
784 '';
785 };
786
787 additionalModules = mkOption {
788 default = [ ];
789 type = types.listOf (types.attrsOf types.anything);
790 example = literalExpression "[ pkgs.nginxModules.echo ]";
791 description = ''
792 Additional [third-party nginx modules](https://www.nginx.com/resources/wiki/modules/)
793 to install. Packaged modules are available in `pkgs.nginxModules`.
794 '';
795 };
796
797 logError = mkOption {
798 default = "stderr";
799 type = types.str;
800 description = ''
801 Configures logging.
802 The first parameter defines a file that will store the log. The
803 special value stderr selects the standard error file. Logging to
804 syslog can be configured by specifying the “syslog:” prefix.
805 The second parameter determines the level of logging, and can be
806 one of the following: debug, info, notice, warn, error, crit,
807 alert, or emerg. Log levels above are listed in the order of
808 increasing severity. Setting a certain log level will cause all
809 messages of the specified and more severe log levels to be logged.
810 If this parameter is omitted then error is used.
811 '';
812 };
813
814 preStart = mkOption {
815 type = types.lines;
816 default = "";
817 description = ''
818 Shell commands executed before the service's nginx is started.
819 '';
820 };
821
822 config = mkOption {
823 type = types.str;
824 default = "";
825 description = ''
826 Verbatim {file}`nginx.conf` configuration.
827 This is mutually exclusive to any other config option for
828 {file}`nginx.conf` except for
829 - [](#opt-services.nginx.appendConfig)
830 - [](#opt-services.nginx.httpConfig)
831 - [](#opt-services.nginx.logError)
832
833 If additional verbatim config in addition to other options is needed,
834 [](#opt-services.nginx.appendConfig) should be used instead.
835 '';
836 };
837
838 appendConfig = mkOption {
839 type = types.lines;
840 default = "";
841 description = ''
842 Configuration lines appended to the generated Nginx
843 configuration file. Commonly used by different modules
844 providing http snippets. {option}`appendConfig`
845 can be specified more than once and its value will be
846 concatenated (contrary to {option}`config` which
847 can be set only once).
848 '';
849 };
850
851 commonHttpConfig = mkOption {
852 type = types.lines;
853 default = "";
854 example = ''
855 resolver 127.0.0.1 valid=5s;
856
857 log_format myformat '$remote_addr - $remote_user [$time_local] '
858 '"$request" $status $body_bytes_sent '
859 '"$http_referer" "$http_user_agent"';
860 '';
861 description = ''
862 With nginx you must provide common http context definitions before
863 they are used, e.g. log_format, resolver, etc. inside of server
864 or location contexts. Use this attribute to set these definitions
865 at the appropriate location.
866 '';
867 };
868
869 httpConfig = mkOption {
870 type = types.lines;
871 default = "";
872 description = ''
873 Configuration lines to be set inside the http block.
874 This is mutually exclusive with the structured configuration
875 via virtualHosts and the recommendedXyzSettings configuration
876 options. See appendHttpConfig for appending to the generated http block.
877 '';
878 };
879
880 streamConfig = mkOption {
881 type = types.lines;
882 default = "";
883 example = ''
884 server {
885 listen 127.0.0.1:53 udp reuseport;
886 proxy_timeout 20s;
887 proxy_pass 192.168.0.1:53535;
888 }
889 '';
890 description = ''
891 Configuration lines to be set inside the stream block.
892 '';
893 };
894
895 eventsConfig = mkOption {
896 type = types.lines;
897 default = "";
898 description = ''
899 Configuration lines to be set inside the events block.
900 '';
901 };
902
903 appendHttpConfig = mkOption {
904 type = types.lines;
905 default = "";
906 description = ''
907 Configuration lines to be appended to the generated http block.
908 This is mutually exclusive with using config and httpConfig for
909 specifying the whole http block verbatim.
910 '';
911 };
912
913 enableReload = mkOption {
914 default = false;
915 type = types.bool;
916 description = ''
917 Reload nginx when configuration file changes (instead of restart).
918 The configuration file is exposed at {file}`/etc/nginx/nginx.conf`.
919 See also `systemd.services.*.restartIfChanged`.
920 '';
921 };
922
923 enableQuicBPF = mkOption {
924 default = false;
925 type = types.bool;
926 description = ''
927 Enables routing of QUIC packets using eBPF. When enabled, this allows
928 to support QUIC connection migration. The directive is only supported
929 on Linux 5.7+.
930 Note that enabling this option will make nginx run with extended
931 capabilities that are usually limited to processes running as root
932 namely `CAP_SYS_ADMIN` and `CAP_NET_ADMIN`.
933 '';
934 };
935
936 user = mkOption {
937 type = types.str;
938 default = "nginx";
939 description = "User account under which nginx runs.";
940 };
941
942 group = mkOption {
943 type = types.str;
944 default = "nginx";
945 description = "Group account under which nginx runs.";
946 };
947
948 serverTokens = mkOption {
949 type = types.bool;
950 default = false;
951 description = "Show nginx version in headers and error pages.";
952 };
953
954 clientMaxBodySize = mkOption {
955 type = types.str;
956 default = "10m";
957 description = "Set nginx global client_max_body_size.";
958 };
959
960 sslCiphers = mkOption {
961 type = types.nullOr types.str;
962 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
963 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:DHE-RSA-CHACHA20-POLY1305";
964 description = "Ciphers to choose from when negotiating TLS handshakes.";
965 };
966
967 sslProtocols = mkOption {
968 type = types.str;
969 default = "TLSv1.2 TLSv1.3";
970 example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3";
971 description = "Allowed TLS protocol versions.";
972 };
973
974 sslDhparam = mkOption {
975 type = types.nullOr types.path;
976 default = null;
977 example = "/path/to/dhparams.pem";
978 description = "Path to DH parameters file.";
979 };
980
981 proxyResolveWhileRunning = mkOption {
982 type = types.bool;
983 default = false;
984 description = ''
985 Resolves domains of proxyPass targets at runtime and not only at startup.
986 This can be used as a workaround if nginx fails to start because of not-yet-working DNS.
987
988 :::{.warn}
989 `services.nginx.resolver` must be set for this option to work.
990 :::
991 '';
992 };
993
994 uwsgiResolveWhileRunning = mkOption {
995 type = types.bool;
996 default = false;
997 description = ''
998 Resolves domains of uwsgi targets at runtime
999 and not only at start, you have to set
1000 services.nginx.resolver, too.
1001 '';
1002 };
1003
1004 mapHashBucketSize = mkOption {
1005 type = types.nullOr (
1006 types.enum [
1007 32
1008 64
1009 128
1010 ]
1011 );
1012 default = null;
1013 description = ''
1014 Sets the bucket size for the map variables hash tables. Default
1015 value depends on the processor’s cache line size.
1016 '';
1017 };
1018
1019 mapHashMaxSize = mkOption {
1020 type = types.nullOr types.ints.positive;
1021 default = null;
1022 description = ''
1023 Sets the maximum size of the map variables hash tables.
1024 '';
1025 };
1026
1027 serverNamesHashBucketSize = mkOption {
1028 type = types.nullOr types.ints.positive;
1029 default = null;
1030 description = ''
1031 Sets the bucket size for the server names hash tables. Default
1032 value depends on the processor’s cache line size.
1033 '';
1034 };
1035
1036 serverNamesHashMaxSize = mkOption {
1037 type = types.nullOr types.ints.positive;
1038 default = null;
1039 description = ''
1040 Sets the maximum size of the server names hash tables.
1041 '';
1042 };
1043
1044 typesHashMaxSize = mkOption {
1045 type = types.ints.positive;
1046 default = if cfg.defaultMimeTypes == "${pkgs.mailcap}/etc/nginx/mime.types" then 2688 else 1024;
1047 defaultText = literalExpression ''if config.services.nginx.defaultMimeTypes == "''${pkgs.mailcap}/etc/nginx/mime.types" then 2688 else 1024'';
1048 description = ''
1049 Sets the maximum size of the types hash tables (`types_hash_max_size`).
1050 It is recommended that the minimum size possible size is used.
1051 If {option}`recommendedOptimisation` is disabled, nginx would otherwise
1052 fail to start since the mailmap `mime.types` database has more entries
1053 than the nginx default value 1024.
1054 '';
1055 };
1056
1057 proxyCachePath = mkOption {
1058 type = types.attrsOf (
1059 types.submodule (
1060 { ... }:
1061 {
1062 options = {
1063 enable = mkEnableOption "this proxy cache path entry";
1064
1065 keysZoneName = mkOption {
1066 type = types.str;
1067 default = "cache";
1068 example = "my_cache";
1069 description = "Set name to shared memory zone.";
1070 };
1071
1072 keysZoneSize = mkOption {
1073 type = types.str;
1074 default = "10m";
1075 example = "32m";
1076 description = "Set size to shared memory zone.";
1077 };
1078
1079 levels = mkOption {
1080 type = types.str;
1081 default = "1:2";
1082 example = "1:2:2";
1083 description = ''
1084 The levels parameter defines structure of subdirectories in cache: from
1085 1 to 3, each level accepts values 1 or 2. Сan be used any combination of
1086 1 and 2 in these formats: x, x:x and x:x:x.
1087 '';
1088 };
1089
1090 useTempPath = mkOption {
1091 type = types.bool;
1092 default = false;
1093 example = true;
1094 description = ''
1095 Nginx first writes files that are destined for the cache to a temporary
1096 storage area, and the use_temp_path=off directive instructs Nginx to
1097 write them to the same directories where they will be cached. Recommended
1098 that you set this parameter to off to avoid unnecessary copying of data
1099 between file systems.
1100 '';
1101 };
1102
1103 inactive = mkOption {
1104 type = types.str;
1105 default = "10m";
1106 example = "1d";
1107 description = ''
1108 Cached data that has not been accessed for the time specified by
1109 the inactive parameter is removed from the cache, regardless of
1110 its freshness.
1111 '';
1112 };
1113
1114 maxSize = mkOption {
1115 type = types.str;
1116 default = "1g";
1117 example = "2048m";
1118 description = "Set maximum cache size";
1119 };
1120 };
1121 }
1122 )
1123 );
1124 default = { };
1125 description = ''
1126 Configure a proxy cache path entry.
1127 See <https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path> for documentation.
1128 '';
1129 };
1130
1131 resolver = mkOption {
1132 type = types.submodule {
1133 options = {
1134 addresses = mkOption {
1135 type = types.listOf types.str;
1136 default = [ ];
1137 example = literalExpression ''[ "[::1]" "127.0.0.1:5353" ]'';
1138 description = "List of resolvers to use";
1139 };
1140 valid = mkOption {
1141 type = types.str;
1142 default = "";
1143 example = "30s";
1144 description = ''
1145 By default, nginx caches answers using the TTL value of a response.
1146 An optional valid parameter allows overriding it
1147 '';
1148 };
1149 ipv4 = mkOption {
1150 type = types.bool;
1151 default = true;
1152 description = ''
1153 By default, nginx will look up both IPv4 and IPv6 addresses while resolving.
1154 If looking up of IPv4 addresses is not desired, the ipv4=off parameter can be
1155 specified.
1156 '';
1157 };
1158 ipv6 = mkOption {
1159 type = types.bool;
1160 default = config.networking.enableIPv6;
1161 defaultText = lib.literalExpression "config.networking.enableIPv6";
1162 description = ''
1163 By default, nginx will look up both IPv4 and IPv6 addresses while resolving.
1164 If looking up of IPv6 addresses is not desired, the ipv6=off parameter can be
1165 specified.
1166 '';
1167 };
1168 };
1169 };
1170 description = ''
1171 Configures name servers used to resolve names of upstream servers into addresses
1172 '';
1173 default = { };
1174 };
1175
1176 upstreams = mkOption {
1177 type = types.attrsOf (
1178 types.submodule {
1179 options = {
1180 servers = mkOption {
1181 type = types.attrsOf (
1182 types.submodule {
1183 freeformType = types.attrsOf (
1184 types.oneOf [
1185 types.bool
1186 types.int
1187 types.str
1188 ]
1189 );
1190 options = {
1191 backup = mkOption {
1192 type = types.bool;
1193 default = false;
1194 description = ''
1195 Marks the server as a backup server. It will be passed
1196 requests when the primary servers are unavailable.
1197 '';
1198 };
1199 };
1200 }
1201 );
1202 description = ''
1203 Defines the address and other parameters of the upstream servers.
1204 See [the documentation](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#server)
1205 for the available parameters.
1206 '';
1207 default = { };
1208 example = lib.literalMD "see [](#opt-services.nginx.upstreams)";
1209 };
1210 extraConfig = mkOption {
1211 type = types.lines;
1212 default = "";
1213 description = ''
1214 These lines go to the end of the upstream verbatim.
1215 '';
1216 };
1217 };
1218 }
1219 );
1220 description = ''
1221 Defines a group of servers to use as proxy target.
1222 '';
1223 default = { };
1224 example = {
1225 "backend" = {
1226 servers = {
1227 "backend1.example.com:8080" = {
1228 weight = 5;
1229 };
1230 "backend2.example.com" = {
1231 max_fails = 3;
1232 fail_timeout = "30s";
1233 };
1234 "backend3.example.com" = { };
1235 "backup1.example.com" = {
1236 backup = true;
1237 };
1238 "backup2.example.com" = {
1239 backup = true;
1240 };
1241 };
1242 extraConfig = ''
1243 keepalive 16;
1244 '';
1245 };
1246 "memcached" = {
1247 servers."unix:/run/memcached/memcached.sock" = { };
1248 };
1249 };
1250 };
1251
1252 virtualHosts = mkOption {
1253 type = types.attrsOf (
1254 types.submodule (
1255 import ./vhost-options.nix {
1256 inherit config lib;
1257 }
1258 )
1259 );
1260 default = {
1261 localhost = { };
1262 };
1263 example = literalExpression ''
1264 {
1265 "hydra.example.com" = {
1266 forceSSL = true;
1267 enableACME = true;
1268 locations."/" = {
1269 proxyPass = "http://localhost:3000";
1270 };
1271 };
1272 };
1273 '';
1274 description = "Declarative vhost config";
1275 };
1276 validateConfigFile = lib.mkEnableOption "validating configuration with pkgs.writeNginxConfig" // {
1277 default = true;
1278 };
1279 };
1280 };
1281
1282 imports = [
1283 (mkRemovedOptionModule [ "services" "nginx" "stateDir" ] ''
1284 The Nginx log directory has been moved to /var/log/nginx, the cache directory
1285 to /var/cache/nginx. The option services.nginx.stateDir has been removed.
1286 '')
1287 (mkRenamedOptionModule
1288 [ "services" "nginx" "proxyCache" "inactive" ]
1289 [ "services" "nginx" "proxyCachePath" "" "inactive" ]
1290 )
1291 (mkRenamedOptionModule
1292 [ "services" "nginx" "proxyCache" "useTempPath" ]
1293 [ "services" "nginx" "proxyCachePath" "" "useTempPath" ]
1294 )
1295 (mkRenamedOptionModule
1296 [ "services" "nginx" "proxyCache" "levels" ]
1297 [ "services" "nginx" "proxyCachePath" "" "levels" ]
1298 )
1299 (mkRenamedOptionModule
1300 [ "services" "nginx" "proxyCache" "keysZoneSize" ]
1301 [ "services" "nginx" "proxyCachePath" "" "keysZoneSize" ]
1302 )
1303 (mkRenamedOptionModule
1304 [ "services" "nginx" "proxyCache" "keysZoneName" ]
1305 [ "services" "nginx" "proxyCachePath" "" "keysZoneName" ]
1306 )
1307 (mkRenamedOptionModule
1308 [ "services" "nginx" "proxyCache" "enable" ]
1309 [ "services" "nginx" "proxyCachePath" "" "enable" ]
1310 )
1311 ];
1312
1313 config = mkIf cfg.enable {
1314 warnings =
1315 let
1316 deprecatedSSL =
1317 name: config:
1318 optional config.enableSSL ''
1319 config.services.nginx.virtualHosts.<name>.enableSSL is deprecated,
1320 use config.services.nginx.virtualHosts.<name>.onlySSL instead.
1321 '';
1322
1323 in
1324 flatten (mapAttrsToList deprecatedSSL virtualHosts);
1325
1326 assertions =
1327 let
1328 hostOrAliasIsNull = l: l.root == null || l.alias == null;
1329 in
1330 [
1331 {
1332 assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts);
1333 message = "Only one of nginx root or alias can be specified on a location.";
1334 }
1335
1336 {
1337 assertion = all (
1338 host:
1339 with host;
1340 count id [
1341 addSSL
1342 (onlySSL || enableSSL)
1343 forceSSL
1344 rejectSSL
1345 ] <= 1
1346 ) (attrValues virtualHosts);
1347 message = ''
1348 Options services.nginx.service.virtualHosts.<name>.addSSL,
1349 services.nginx.virtualHosts.<name>.onlySSL,
1350 services.nginx.virtualHosts.<name>.forceSSL and
1351 services.nginx.virtualHosts.<name>.rejectSSL are mutually exclusive.
1352 '';
1353 }
1354
1355 {
1356 assertion = all (host: !(host.enableACME && host.useACMEHost != null)) (attrValues virtualHosts);
1357 message = ''
1358 Options services.nginx.service.virtualHosts.<name>.enableACME and
1359 services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
1360 '';
1361 }
1362
1363 {
1364 assertion = all (
1365 host:
1366 all (location: !(location.proxyPass != null && location.uwsgiPass != null)) (
1367 attrValues host.locations
1368 )
1369 ) (attrValues virtualHosts);
1370 message = ''
1371 Options services.nginx.service.virtualHosts.<name>.proxyPass and
1372 services.nginx.virtualHosts.<name>.uwsgiPass are mutually exclusive.
1373 '';
1374 }
1375
1376 {
1377 assertion =
1378 cfg.package.pname != "nginxQuic" && cfg.package.pname != "angieQuic" -> !(cfg.enableQuicBPF);
1379 message = ''
1380 services.nginx.enableQuicBPF requires using nginxQuic package,
1381 which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;` or
1382 `services.nginx.package = pkgs.angieQuic;`.
1383 '';
1384 }
1385
1386 {
1387 assertion =
1388 cfg.package.pname != "nginxQuic" && cfg.package.pname != "angieQuic"
1389 -> all (host: !host.quic) (attrValues virtualHosts);
1390 message = ''
1391 services.nginx.service.virtualHosts.<name>.quic requires using nginxQuic or angie packages,
1392 which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;` or
1393 `services.nginx.package = pkgs.angieQuic;`.
1394 '';
1395 }
1396
1397 {
1398 # The idea is to understand whether there is a virtual host with a listen configuration
1399 # that requires ACME configuration but has no HTTP listener which will make deterministically fail
1400 # this operation.
1401 # Options' priorities are the following at the moment:
1402 # listen (vhost) > defaultListen (server) > listenAddresses (vhost) > defaultListenAddresses (server)
1403 assertion =
1404 let
1405 hasAtLeastHttpListener =
1406 listenOptions:
1407 any (
1408 listenLine: if listenLine ? proxyProtocol then !listenLine.proxyProtocol else true
1409 ) listenOptions;
1410 hasAtLeastDefaultHttpListener =
1411 if cfg.defaultListen != [ ] then
1412 hasAtLeastHttpListener cfg.defaultListen
1413 else
1414 (cfg.defaultListenAddresses != [ ]);
1415 in
1416 all (
1417 host:
1418 let
1419 hasAtLeastVhostHttpListener =
1420 if host.listen != [ ] then hasAtLeastHttpListener host.listen else (host.listenAddresses != [ ]);
1421 vhostAuthority = host.listen != [ ] || (cfg.defaultListen == [ ] && host.listenAddresses != [ ]);
1422 in
1423 # Either vhost has precedence and we need a vhost specific http listener
1424 # Either vhost set nothing and inherit from server settings
1425 host.enableACME
1426 -> (
1427 (vhostAuthority && hasAtLeastVhostHttpListener)
1428 || (!vhostAuthority && hasAtLeastDefaultHttpListener)
1429 )
1430 ) (attrValues virtualHosts);
1431 message = ''
1432 services.nginx.virtualHosts.<name>.enableACME requires a HTTP listener
1433 to answer to ACME requests.
1434 '';
1435 }
1436
1437 {
1438 assertion = cfg.resolver.ipv4 || cfg.resolver.ipv6;
1439 message = ''
1440 At least one of services.nginx.resolver.ipv4 and services.nginx.resolver.ipv6 must be true.
1441 '';
1442 }
1443 ]
1444 ++ map (
1445 name:
1446 mkCertOwnershipAssertion {
1447 cert = config.security.acme.certs.${name};
1448 groups = config.users.groups;
1449 services =
1450 [ config.systemd.services.nginx ]
1451 ++ lib.optional (
1452 cfg.enableReload || vhostCertNames != [ ]
1453 ) config.systemd.services.nginx-config-reload;
1454 }
1455 ) vhostCertNames;
1456
1457 services.nginx.additionalModules =
1458 optional cfg.recommendedBrotliSettings pkgs.nginxModules.brotli
1459 ++ lib.optional cfg.recommendedZstdSettings pkgs.nginxModules.zstd;
1460
1461 services.nginx.virtualHosts.localhost = mkIf cfg.statusPage {
1462 serverAliases = [ "127.0.0.1" ] ++ lib.optional config.networking.enableIPv6 "[::1]";
1463 listenAddresses = lib.mkDefault (
1464 [
1465 "0.0.0.0"
1466 ]
1467 ++ lib.optional enableIPv6 "[::]"
1468 );
1469 locations."/nginx_status" = {
1470 extraConfig = ''
1471 stub_status on;
1472 access_log off;
1473 allow 127.0.0.1;
1474 ${optionalString enableIPv6 "allow ::1;"}
1475 deny all;
1476 '';
1477 };
1478 };
1479
1480 systemd.services.nginx = {
1481 description = "Nginx Web Server";
1482 wantedBy = [ "multi-user.target" ];
1483 wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) vhostCertNames);
1484 after =
1485 [ "network.target" ]
1486 ++ map (certName: "acme-selfsigned-${certName}.service") vhostCertNames
1487 ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa
1488 # Nginx needs to be started in order to be able to request certificates
1489 # (it's hosting the acme challenge after all)
1490 # This fixes https://github.com/NixOS/nixpkgs/issues/81842
1491 before = map (certName: "acme-${certName}.service") dependentCertNames;
1492 stopIfChanged = false;
1493 preStart = ''
1494 ${cfg.preStart}
1495 ${execCommand} -t
1496 '';
1497
1498 startLimitIntervalSec = 60;
1499 serviceConfig = {
1500 ExecStart = execCommand;
1501 ExecReload = [
1502 "${execCommand} -t"
1503 "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
1504 ];
1505 Restart = "always";
1506 RestartSec = "10s";
1507 # User and group
1508 User = cfg.user;
1509 Group = cfg.group;
1510 # Runtime directory and mode
1511 RuntimeDirectory = "nginx";
1512 RuntimeDirectoryMode = "0750";
1513 # Cache directory and mode
1514 CacheDirectory = "nginx";
1515 CacheDirectoryMode = "0750";
1516 # Logs directory and mode
1517 LogsDirectory = "nginx";
1518 LogsDirectoryMode = "0750";
1519 # Proc filesystem
1520 ProcSubset = "pid";
1521 ProtectProc = "invisible";
1522 # New file permissions
1523 UMask = "0027"; # 0640 / 0750
1524 # Capabilities
1525 AmbientCapabilities =
1526 [
1527 "CAP_NET_BIND_SERVICE"
1528 "CAP_SYS_RESOURCE"
1529 ]
1530 ++ optionals cfg.enableQuicBPF [
1531 "CAP_SYS_ADMIN"
1532 "CAP_NET_ADMIN"
1533 ];
1534 CapabilityBoundingSet =
1535 [
1536 "CAP_NET_BIND_SERVICE"
1537 "CAP_SYS_RESOURCE"
1538 ]
1539 ++ optionals cfg.enableQuicBPF [
1540 "CAP_SYS_ADMIN"
1541 "CAP_NET_ADMIN"
1542 ];
1543 # Security
1544 NoNewPrivileges = true;
1545 # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
1546 ProtectSystem = "strict";
1547 ProtectHome = mkDefault true;
1548 PrivateTmp = true;
1549 PrivateDevices = true;
1550 ProtectHostname = true;
1551 ProtectClock = true;
1552 ProtectKernelTunables = true;
1553 ProtectKernelModules = true;
1554 ProtectKernelLogs = true;
1555 ProtectControlGroups = true;
1556 RestrictAddressFamilies = [
1557 "AF_UNIX"
1558 "AF_INET"
1559 "AF_INET6"
1560 ];
1561 RestrictNamespaces = true;
1562 LockPersonality = true;
1563 MemoryDenyWriteExecute =
1564 !(
1565 (builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules)
1566 || (cfg.package == pkgs.openresty)
1567 );
1568 RestrictRealtime = true;
1569 RestrictSUIDSGID = true;
1570 RemoveIPC = true;
1571 PrivateMounts = true;
1572 # System Call Filtering
1573 SystemCallArchitectures = "native";
1574 SystemCallFilter = [
1575 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"
1576 ] ++ optional cfg.enableQuicBPF [ "bpf" ];
1577 };
1578 };
1579
1580 environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload {
1581 source = configFile;
1582 };
1583
1584 # This service waits for all certificates to be available
1585 # before reloading nginx configuration.
1586 # sslTargets are added to wantedBy + before
1587 # which allows the acme-finished-$cert.target to signify the successful updating
1588 # of certs end-to-end.
1589 systemd.services.nginx-config-reload =
1590 let
1591 sslServices = map (certName: "acme-${certName}.service") vhostCertNames;
1592 sslTargets = map (certName: "acme-finished-${certName}.target") vhostCertNames;
1593 in
1594 mkIf (cfg.enableReload || vhostCertNames != [ ]) {
1595 wants = optionals cfg.enableReload [ "nginx.service" ];
1596 wantedBy = sslServices ++ [ "multi-user.target" ];
1597 # Before the finished targets, after the renew services.
1598 # This service might be needed for HTTP-01 challenges, but we only want to confirm
1599 # certs are updated _after_ config has been reloaded.
1600 before = sslTargets;
1601 after = sslServices;
1602 restartTriggers = optionals cfg.enableReload [ configFile ];
1603 # Block reloading if not all certs exist yet.
1604 # Happens when config changes add new vhosts/certs.
1605 unitConfig = {
1606 ConditionPathExists = optionals (sslServices != [ ]) (
1607 map (certName: certs.${certName}.directory + "/fullchain.pem") vhostCertNames
1608 );
1609 # Disable rate limiting for this, because it may be triggered quickly a bunch of times
1610 # if a lot of certificates are renewed in quick succession. The reload itself is cheap,
1611 # so even doing a lot of them in a short burst is fine.
1612 # FIXME: there's probably a better way to do this.
1613 StartLimitIntervalSec = 0;
1614 };
1615 serviceConfig = {
1616 Type = "oneshot";
1617 TimeoutSec = 60;
1618 ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service";
1619 ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service";
1620 };
1621 };
1622
1623 security.acme.certs =
1624 let
1625 acmePairs = map (
1626 vhostConfig:
1627 let
1628 hasRoot = vhostConfig.acmeRoot != null;
1629 in
1630 nameValuePair vhostConfig.serverName {
1631 group = mkDefault cfg.group;
1632 # if acmeRoot is null inherit config.security.acme
1633 # Since config.security.acme.certs.<cert>.webroot's own default value
1634 # should take precedence set priority higher than mkOptionDefault
1635 webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot;
1636 # Also nudge dnsProvider to null in case it is inherited
1637 dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
1638 extraDomainNames = vhostConfig.serverAliases;
1639 # Filter for enableACME-only vhosts. Don't want to create dud certs
1640 }
1641 ) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
1642 in
1643 listToAttrs acmePairs;
1644
1645 users.users = optionalAttrs (cfg.user == "nginx") {
1646 nginx = {
1647 group = cfg.group;
1648 isSystemUser = true;
1649 uid = config.ids.uids.nginx;
1650 };
1651 };
1652
1653 users.groups = optionalAttrs (cfg.group == "nginx") {
1654 nginx.gid = config.ids.gids.nginx;
1655 };
1656
1657 boot.kernelModules = optional (versionAtLeast config.boot.kernelPackages.kernel.version "4.17") "tls";
1658
1659 # do not delete the default temp directories created upon nginx startup
1660 systemd.tmpfiles.rules = [
1661 "X /tmp/systemd-private-%b-nginx.service-*/tmp/nginx_*"
1662 ];
1663
1664 services.logrotate.settings.nginx = mapAttrs (_: mkDefault) {
1665 files = [ "/var/log/nginx/*.log" ];
1666 frequency = "weekly";
1667 su = "${cfg.user} ${cfg.group}";
1668 rotate = 26;
1669 compress = true;
1670 delaycompress = true;
1671 postrotate = "[ ! -f /var/run/nginx/nginx.pid ] || kill -USR1 `cat /var/run/nginx/nginx.pid`";
1672 };
1673 };
1674}