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