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