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