1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.nginx;
7 certs = 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 = "${certs.${certName}.directory}/chain.pem";
26 })
27 ) cfg.virtualHosts;
28 enableIPv6 = config.networking.enableIPv6;
29
30 defaultFastcgiParams = {
31 SCRIPT_FILENAME = "$document_root$fastcgi_script_name";
32 QUERY_STRING = "$query_string";
33 REQUEST_METHOD = "$request_method";
34 CONTENT_TYPE = "$content_type";
35 CONTENT_LENGTH = "$content_length";
36
37 SCRIPT_NAME = "$fastcgi_script_name";
38 REQUEST_URI = "$request_uri";
39 DOCUMENT_URI = "$document_uri";
40 DOCUMENT_ROOT = "$document_root";
41 SERVER_PROTOCOL = "$server_protocol";
42 REQUEST_SCHEME = "$scheme";
43 HTTPS = "$https if_not_empty";
44
45 GATEWAY_INTERFACE = "CGI/1.1";
46 SERVER_SOFTWARE = "nginx/$nginx_version";
47
48 REMOTE_ADDR = "$remote_addr";
49 REMOTE_PORT = "$remote_port";
50 SERVER_ADDR = "$server_addr";
51 SERVER_PORT = "$server_port";
52 SERVER_NAME = "$server_name";
53
54 REDIRECT_STATUS = "200";
55 };
56
57 recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" ''
58 proxy_set_header Host $host;
59 proxy_set_header X-Real-IP $remote_addr;
60 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
61 proxy_set_header X-Forwarded-Proto $scheme;
62 proxy_set_header X-Forwarded-Host $host;
63 proxy_set_header X-Forwarded-Server $host;
64 '';
65
66 upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
67 upstream ${name} {
68 ${toString (flip mapAttrsToList upstream.servers (name: server: ''
69 server ${name} ${optionalString server.backup "backup"};
70 ''))}
71 ${upstream.extraConfig}
72 }
73 ''));
74
75 commonHttpConfig = ''
76 # The mime type definitions included with nginx are very incomplete, so
77 # we use a list of mime types from the mailcap package, which is also
78 # used by most other Linux distributions by default.
79 include ${pkgs.mailcap}/etc/nginx/mime.types;
80 include ${cfg.package}/conf/fastcgi.conf;
81 include ${cfg.package}/conf/uwsgi_params;
82
83 default_type application/octet-stream;
84 '';
85
86 configFile = pkgs.writers.writeNginxConfig "nginx.conf" ''
87 pid /run/nginx/nginx.pid;
88 error_log ${cfg.logError};
89 daemon off;
90
91 ${cfg.config}
92
93 ${optionalString (cfg.eventsConfig != "" || cfg.config == "") ''
94 events {
95 ${cfg.eventsConfig}
96 }
97 ''}
98
99 ${optionalString (cfg.httpConfig == "" && cfg.config == "") ''
100 http {
101 ${commonHttpConfig}
102
103 ${optionalString (cfg.resolver.addresses != []) ''
104 resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"} ${optionalString (!cfg.resolver.ipv6) "ipv6=off"};
105 ''}
106 ${upstreamConfig}
107
108 ${optionalString (cfg.recommendedOptimisation) ''
109 # optimisation
110 sendfile on;
111 tcp_nopush on;
112 tcp_nodelay on;
113 keepalive_timeout 65;
114 types_hash_max_size 4096;
115 ''}
116
117 ssl_protocols ${cfg.sslProtocols};
118 ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
119 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
120
121 ${optionalString (cfg.recommendedTlsSettings) ''
122 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
123
124 ssl_session_timeout 1d;
125 ssl_session_cache shared:SSL:10m;
126 # Breaks forward secrecy: https://github.com/mozilla/server-side-tls/issues/135
127 ssl_session_tickets off;
128 # We don't enable insecure ciphers by default, so this allows
129 # clients to pick the most performant, per https://github.com/mozilla/server-side-tls/issues/260
130 ssl_prefer_server_ciphers off;
131
132 # OCSP stapling
133 ssl_stapling on;
134 ssl_stapling_verify on;
135 ''}
136
137 ${optionalString (cfg.recommendedGzipSettings) ''
138 gzip on;
139 gzip_proxied any;
140 gzip_comp_level 5;
141 gzip_types
142 application/atom+xml
143 application/javascript
144 application/json
145 application/xml
146 application/xml+rss
147 image/svg+xml
148 text/css
149 text/javascript
150 text/plain
151 text/xml;
152 gzip_vary on;
153 ''}
154
155 ${optionalString (cfg.recommendedProxySettings) ''
156 proxy_redirect off;
157 proxy_connect_timeout ${cfg.proxyTimeout};
158 proxy_send_timeout ${cfg.proxyTimeout};
159 proxy_read_timeout ${cfg.proxyTimeout};
160 proxy_http_version 1.1;
161 include ${recommendedProxyConfig};
162 ''}
163
164 ${optionalString (cfg.mapHashBucketSize != null) ''
165 map_hash_bucket_size ${toString cfg.mapHashBucketSize};
166 ''}
167
168 ${optionalString (cfg.mapHashMaxSize != null) ''
169 map_hash_max_size ${toString cfg.mapHashMaxSize};
170 ''}
171
172 # $connection_upgrade is used for websocket proxying
173 map $http_upgrade $connection_upgrade {
174 default upgrade;
175 ''' close;
176 }
177 client_max_body_size ${cfg.clientMaxBodySize};
178
179 server_tokens ${if cfg.serverTokens then "on" else "off"};
180
181 ${cfg.commonHttpConfig}
182
183 ${vhosts}
184
185 ${optionalString cfg.statusPage ''
186 server {
187 listen 80;
188 ${optionalString enableIPv6 "listen [::]:80;" }
189
190 server_name localhost;
191
192 location /nginx_status {
193 stub_status on;
194 access_log off;
195 allow 127.0.0.1;
196 ${optionalString enableIPv6 "allow ::1;"}
197 deny all;
198 }
199 }
200 ''}
201
202 ${cfg.appendHttpConfig}
203 }''}
204
205 ${optionalString (cfg.httpConfig != "") ''
206 http {
207 ${commonHttpConfig}
208 ${cfg.httpConfig}
209 }''}
210
211 ${optionalString (cfg.streamConfig != "") ''
212 stream {
213 ${cfg.streamConfig}
214 }
215 ''}
216
217 ${cfg.appendConfig}
218 '';
219
220 configPath = if cfg.enableReload
221 then "/etc/nginx/nginx.conf"
222 else configFile;
223
224 execCommand = "${cfg.package}/bin/nginx -c '${configPath}'";
225
226 vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost:
227 let
228 onlySSL = vhost.onlySSL || vhost.enableSSL;
229 hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL;
230
231 defaultListen =
232 if vhost.listen != [] then vhost.listen
233 else ((optionals hasSSL (
234 singleton { addr = "0.0.0.0"; port = 443; ssl = true; }
235 ++ optional enableIPv6 { addr = "[::]"; port = 443; ssl = true; }
236 )) ++ optionals (!onlySSL) (
237 singleton { addr = "0.0.0.0"; port = 80; ssl = false; }
238 ++ optional enableIPv6 { addr = "[::]"; port = 80; ssl = false; }
239 ));
240
241 hostListen =
242 if vhost.forceSSL
243 then filter (x: x.ssl) defaultListen
244 else defaultListen;
245
246 listenString = { addr, port, ssl, extraParameters ? [], ... }:
247 "listen ${addr}:${toString port} "
248 + optionalString ssl "ssl "
249 + optionalString (ssl && vhost.http2) "http2 "
250 + optionalString vhost.default "default_server "
251 + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
252 + ";"
253 + (if ssl && vhost.http3 then ''
254 # UDP listener for **QUIC+HTTP/3
255 listen ${addr}:${toString port} http3 reuseport;
256 # Advertise that HTTP/3 is available
257 add_header Alt-Svc 'h3=":443"';
258 # Sent when QUIC was used
259 add_header QUIC-Status $quic;
260 '' else "");
261
262 redirectListen = filter (x: !x.ssl) defaultListen;
263
264 acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
265 location /.well-known/acme-challenge {
266 ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
267 root ${vhost.acmeRoot};
268 auth_basic off;
269 }
270 ${optionalString (vhost.acmeFallbackHost != null) ''
271 location @acme-fallback {
272 auth_basic off;
273 proxy_pass http://${vhost.acmeFallbackHost};
274 }
275 ''}
276 '';
277
278 in ''
279 ${optionalString vhost.forceSSL ''
280 server {
281 ${concatMapStringsSep "\n" listenString redirectListen}
282
283 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
284 ${acmeLocation}
285 location / {
286 return 301 https://$host$request_uri;
287 }
288 }
289 ''}
290
291 server {
292 ${concatMapStringsSep "\n" listenString hostListen}
293 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
294 ${acmeLocation}
295 ${optionalString (vhost.root != null) "root ${vhost.root};"}
296 ${optionalString (vhost.globalRedirect != null) ''
297 return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri;
298 ''}
299 ${optionalString hasSSL ''
300 ssl_certificate ${vhost.sslCertificate};
301 ssl_certificate_key ${vhost.sslCertificateKey};
302 ''}
303 ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) ''
304 ssl_trusted_certificate ${vhost.sslTrustedCertificate};
305 ''}
306
307 ${mkBasicAuth vhostName vhost}
308
309 ${mkLocations vhost.locations}
310
311 ${vhost.extraConfig}
312 }
313 ''
314 ) virtualHosts);
315 mkLocations = locations: concatStringsSep "\n" (map (config: ''
316 location ${config.location} {
317 ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning)
318 "proxy_pass ${config.proxyPass};"
319 }
320 ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) ''
321 set $nix_proxy_target "${config.proxyPass}";
322 proxy_pass $nix_proxy_target;
323 ''}
324 ${optionalString config.proxyWebsockets ''
325 proxy_http_version 1.1;
326 proxy_set_header Upgrade $http_upgrade;
327 proxy_set_header Connection $connection_upgrade;
328 ''}
329 ${concatStringsSep "\n"
330 (mapAttrsToList (n: v: ''fastcgi_param ${n} "${v}";'')
331 (optionalAttrs (config.fastcgiParams != {})
332 (defaultFastcgiParams // config.fastcgiParams)))}
333 ${optionalString (config.index != null) "index ${config.index};"}
334 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
335 ${optionalString (config.root != null) "root ${config.root};"}
336 ${optionalString (config.alias != null) "alias ${config.alias};"}
337 ${optionalString (config.return != null) "return ${config.return};"}
338 ${config.extraConfig}
339 ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"}
340 ${mkBasicAuth "sublocation" config}
341 }
342 '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
343
344 mkBasicAuth = name: zone: optionalString (zone.basicAuthFile != null || zone.basicAuth != {}) (let
345 auth_file = if zone.basicAuthFile != null
346 then zone.basicAuthFile
347 else mkHtpasswd name zone.basicAuth;
348 in ''
349 auth_basic secured;
350 auth_basic_user_file ${auth_file};
351 '');
352 mkHtpasswd = name: authDef: pkgs.writeText "${name}.htpasswd" (
353 concatStringsSep "\n" (mapAttrsToList (user: password: ''
354 ${user}:{PLAIN}${password}
355 '') authDef)
356 );
357in
358
359{
360 options = {
361 services.nginx = {
362 enable = mkEnableOption "Nginx Web Server";
363
364 statusPage = mkOption {
365 default = false;
366 type = types.bool;
367 description = "
368 Enable status page reachable from localhost on http://127.0.0.1/nginx_status.
369 ";
370 };
371
372 recommendedTlsSettings = mkOption {
373 default = false;
374 type = types.bool;
375 description = "
376 Enable recommended TLS settings.
377 ";
378 };
379
380 recommendedOptimisation = mkOption {
381 default = false;
382 type = types.bool;
383 description = "
384 Enable recommended optimisation settings.
385 ";
386 };
387
388 recommendedGzipSettings = mkOption {
389 default = false;
390 type = types.bool;
391 description = "
392 Enable recommended gzip settings.
393 ";
394 };
395
396 recommendedProxySettings = mkOption {
397 default = false;
398 type = types.bool;
399 description = "
400 Enable recommended proxy settings.
401 ";
402 };
403
404 proxyTimeout = mkOption {
405 type = types.str;
406 default = "60s";
407 example = "20s";
408 description = "
409 Change the proxy related timeouts in recommendedProxySettings.
410 ";
411 };
412
413 package = mkOption {
414 default = pkgs.nginxStable;
415 defaultText = "pkgs.nginxStable";
416 type = types.package;
417 apply = p: p.override {
418 modules = p.modules ++ cfg.additionalModules;
419 };
420 description = "
421 Nginx package to use. This defaults to the stable version. Note
422 that the nginx team recommends to use the mainline version which
423 available in nixpkgs as <literal>nginxMainline</literal>.
424 ";
425 };
426
427 additionalModules = mkOption {
428 default = [];
429 type = types.listOf (types.attrsOf types.anything);
430 example = literalExample "[ pkgs.nginxModules.brotli ]";
431 description = ''
432 Additional <link xlink:href="https://www.nginx.com/resources/wiki/modules/">third-party nginx modules</link>
433 to install. Packaged modules are available in
434 <literal>pkgs.nginxModules</literal>.
435 '';
436 };
437
438 logError = mkOption {
439 default = "stderr";
440 type = types.str;
441 description = "
442 Configures logging.
443 The first parameter defines a file that will store the log. The
444 special value stderr selects the standard error file. Logging to
445 syslog can be configured by specifying the “syslog:” prefix.
446 The second parameter determines the level of logging, and can be
447 one of the following: debug, info, notice, warn, error, crit,
448 alert, or emerg. Log levels above are listed in the order of
449 increasing severity. Setting a certain log level will cause all
450 messages of the specified and more severe log levels to be logged.
451 If this parameter is omitted then error is used.
452 ";
453 };
454
455 preStart = mkOption {
456 type = types.lines;
457 default = "";
458 description = "
459 Shell commands executed before the service's nginx is started.
460 ";
461 };
462
463 config = mkOption {
464 type = types.str;
465 default = "";
466 description = ''
467 Verbatim <filename>nginx.conf</filename> configuration.
468 This is mutually exclusive to any other config option for
469 <filename>nginx.conf</filename> except for
470 <itemizedlist>
471 <listitem><para><xref linkend="opt-services.nginx.appendConfig" />
472 </para></listitem>
473 <listitem><para><xref linkend="opt-services.nginx.httpConfig" />
474 </para></listitem>
475 <listitem><para><xref linkend="opt-services.nginx.logError" />
476 </para></listitem>
477 </itemizedlist>
478
479 If additional verbatim config in addition to other options is needed,
480 <xref linkend="opt-services.nginx.appendConfig" /> should be used instead.
481 '';
482 };
483
484 appendConfig = mkOption {
485 type = types.lines;
486 default = "";
487 description = ''
488 Configuration lines appended to the generated Nginx
489 configuration file. Commonly used by different modules
490 providing http snippets. <option>appendConfig</option>
491 can be specified more than once and it's value will be
492 concatenated (contrary to <option>config</option> which
493 can be set only once).
494 '';
495 };
496
497 commonHttpConfig = mkOption {
498 type = types.lines;
499 default = "";
500 example = ''
501 resolver 127.0.0.1 valid=5s;
502
503 log_format myformat '$remote_addr - $remote_user [$time_local] '
504 '"$request" $status $body_bytes_sent '
505 '"$http_referer" "$http_user_agent"';
506 '';
507 description = ''
508 With nginx you must provide common http context definitions before
509 they are used, e.g. log_format, resolver, etc. inside of server
510 or location contexts. Use this attribute to set these definitions
511 at the appropriate location.
512 '';
513 };
514
515 httpConfig = mkOption {
516 type = types.lines;
517 default = "";
518 description = "
519 Configuration lines to be set inside the http block.
520 This is mutually exclusive with the structured configuration
521 via virtualHosts and the recommendedXyzSettings configuration
522 options. See appendHttpConfig for appending to the generated http block.
523 ";
524 };
525
526 streamConfig = mkOption {
527 type = types.lines;
528 default = "";
529 example = ''
530 server {
531 listen 127.0.0.1:53 udp reuseport;
532 proxy_timeout 20s;
533 proxy_pass 192.168.0.1:53535;
534 }
535 '';
536 description = "
537 Configuration lines to be set inside the stream block.
538 ";
539 };
540
541 eventsConfig = mkOption {
542 type = types.lines;
543 default = "";
544 description = ''
545 Configuration lines to be set inside the events block.
546 '';
547 };
548
549 appendHttpConfig = mkOption {
550 type = types.lines;
551 default = "";
552 description = "
553 Configuration lines to be appended to the generated http block.
554 This is mutually exclusive with using config and httpConfig for
555 specifying the whole http block verbatim.
556 ";
557 };
558
559 enableReload = mkOption {
560 default = false;
561 type = types.bool;
562 description = ''
563 Reload nginx when configuration file changes (instead of restart).
564 The configuration file is exposed at <filename>/etc/nginx/nginx.conf</filename>.
565 See also <literal>systemd.services.*.restartIfChanged</literal>.
566 '';
567 };
568
569 user = mkOption {
570 type = types.str;
571 default = "nginx";
572 description = "User account under which nginx runs.";
573 };
574
575 group = mkOption {
576 type = types.str;
577 default = "nginx";
578 description = "Group account under which nginx runs.";
579 };
580
581 serverTokens = mkOption {
582 type = types.bool;
583 default = false;
584 description = "Show nginx version in headers and error pages.";
585 };
586
587 clientMaxBodySize = mkOption {
588 type = types.str;
589 default = "10m";
590 description = "Set nginx global client_max_body_size.";
591 };
592
593 sslCiphers = mkOption {
594 type = types.nullOr types.str;
595 # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
596 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";
597 description = "Ciphers to choose from when negotiating TLS handshakes.";
598 };
599
600 sslProtocols = mkOption {
601 type = types.str;
602 default = "TLSv1.2 TLSv1.3";
603 example = "TLSv1 TLSv1.1 TLSv1.2 TLSv1.3";
604 description = "Allowed TLS protocol versions.";
605 };
606
607 sslDhparam = mkOption {
608 type = types.nullOr types.path;
609 default = null;
610 example = "/path/to/dhparams.pem";
611 description = "Path to DH parameters file.";
612 };
613
614 proxyResolveWhileRunning = mkOption {
615 type = types.bool;
616 default = false;
617 description = ''
618 Resolves domains of proxyPass targets at runtime
619 and not only at start, you have to set
620 services.nginx.resolver, too.
621 '';
622 };
623
624 mapHashBucketSize = mkOption {
625 type = types.nullOr (types.enum [ 32 64 128 ]);
626 default = null;
627 description = ''
628 Sets the bucket size for the map variables hash tables. Default
629 value depends on the processor’s cache line size.
630 '';
631 };
632
633 mapHashMaxSize = mkOption {
634 type = types.nullOr types.ints.positive;
635 default = null;
636 description = ''
637 Sets the maximum size of the map variables hash tables.
638 '';
639 };
640
641 resolver = mkOption {
642 type = types.submodule {
643 options = {
644 addresses = mkOption {
645 type = types.listOf types.str;
646 default = [];
647 example = literalExample ''[ "[::1]" "127.0.0.1:5353" ]'';
648 description = "List of resolvers to use";
649 };
650 valid = mkOption {
651 type = types.str;
652 default = "";
653 example = "30s";
654 description = ''
655 By default, nginx caches answers using the TTL value of a response.
656 An optional valid parameter allows overriding it
657 '';
658 };
659 ipv6 = mkOption {
660 type = types.bool;
661 default = true;
662 description = ''
663 By default, nginx will look up both IPv4 and IPv6 addresses while resolving.
664 If looking up of IPv6 addresses is not desired, the ipv6=off parameter can be
665 specified.
666 '';
667 };
668 };
669 };
670 description = ''
671 Configures name servers used to resolve names of upstream servers into addresses
672 '';
673 default = {};
674 };
675
676 upstreams = mkOption {
677 type = types.attrsOf (types.submodule {
678 options = {
679 servers = mkOption {
680 type = types.attrsOf (types.submodule {
681 options = {
682 backup = mkOption {
683 type = types.bool;
684 default = false;
685 description = ''
686 Marks the server as a backup server. It will be passed
687 requests when the primary servers are unavailable.
688 '';
689 };
690 };
691 });
692 description = ''
693 Defines the address and other parameters of the upstream servers.
694 '';
695 default = {};
696 example = { "127.0.0.1:8000" = {}; };
697 };
698 extraConfig = mkOption {
699 type = types.lines;
700 default = "";
701 description = ''
702 These lines go to the end of the upstream verbatim.
703 '';
704 };
705 };
706 });
707 description = ''
708 Defines a group of servers to use as proxy target.
709 '';
710 default = {};
711 example = literalExample ''
712 "backend_server" = {
713 servers = { "127.0.0.1:8000" = {}; };
714 extraConfig = ''''
715 keepalive 16;
716 '''';
717 };
718 '';
719 };
720
721 virtualHosts = mkOption {
722 type = types.attrsOf (types.submodule (import ./vhost-options.nix {
723 inherit config lib;
724 }));
725 default = {
726 localhost = {};
727 };
728 example = literalExample ''
729 {
730 "hydra.example.com" = {
731 forceSSL = true;
732 enableACME = true;
733 locations."/" = {
734 proxyPass = "http://localhost:3000";
735 };
736 };
737 };
738 '';
739 description = "Declarative vhost config";
740 };
741 };
742 };
743
744 imports = [
745 (mkRemovedOptionModule [ "services" "nginx" "stateDir" ] ''
746 The Nginx log directory has been moved to /var/log/nginx, the cache directory
747 to /var/cache/nginx. The option services.nginx.stateDir has been removed.
748 '')
749 ];
750
751 config = mkIf cfg.enable {
752 # TODO: test user supplied config file pases syntax test
753
754 warnings =
755 let
756 deprecatedSSL = name: config: optional config.enableSSL
757 ''
758 config.services.nginx.virtualHosts.<name>.enableSSL is deprecated,
759 use config.services.nginx.virtualHosts.<name>.onlySSL instead.
760 '';
761
762 in flatten (mapAttrsToList deprecatedSSL virtualHosts);
763
764 assertions =
765 let
766 hostOrAliasIsNull = l: l.root == null || l.alias == null;
767 in [
768 {
769 assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts);
770 message = "Only one of nginx root or alias can be specified on a location.";
771 }
772
773 {
774 assertion = all (conf: with conf;
775 !(addSSL && (onlySSL || enableSSL)) &&
776 !(forceSSL && (onlySSL || enableSSL)) &&
777 !(addSSL && forceSSL)
778 ) (attrValues virtualHosts);
779 message = ''
780 Options services.nginx.service.virtualHosts.<name>.addSSL,
781 services.nginx.virtualHosts.<name>.onlySSL and services.nginx.virtualHosts.<name>.forceSSL
782 are mutually exclusive.
783 '';
784 }
785
786 {
787 assertion = all (conf: !(conf.enableACME && conf.useACMEHost != null)) (attrValues virtualHosts);
788 message = ''
789 Options services.nginx.service.virtualHosts.<name>.enableACME and
790 services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
791 '';
792 }
793 ];
794
795 systemd.services.nginx = {
796 description = "Nginx Web Server";
797 wantedBy = [ "multi-user.target" ];
798 wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
799 after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
800 # Nginx needs to be started in order to be able to request certificates
801 # (it's hosting the acme challenge after all)
802 # This fixes https://github.com/NixOS/nixpkgs/issues/81842
803 before = map (certName: "acme-${certName}.service") dependentCertNames;
804 stopIfChanged = false;
805 preStart = ''
806 ${cfg.preStart}
807 ${execCommand} -t
808 '';
809
810 startLimitIntervalSec = 60;
811 serviceConfig = {
812 ExecStart = execCommand;
813 ExecReload = [
814 "${execCommand} -t"
815 "${pkgs.coreutils}/bin/kill -HUP $MAINPID"
816 ];
817 Restart = "always";
818 RestartSec = "10s";
819 # User and group
820 User = cfg.user;
821 Group = cfg.group;
822 # Runtime directory and mode
823 RuntimeDirectory = "nginx";
824 RuntimeDirectoryMode = "0750";
825 # Cache directory and mode
826 CacheDirectory = "nginx";
827 CacheDirectoryMode = "0750";
828 # Logs directory and mode
829 LogsDirectory = "nginx";
830 LogsDirectoryMode = "0750";
831 # Proc filesystem
832 ProcSubset = "pid";
833 ProtectProc = "invisible";
834 # New file permissions
835 UMask = "0027"; # 0640 / 0750
836 # Capabilities
837 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
838 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
839 # Security
840 NoNewPrivileges = true;
841 # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
842 ProtectSystem = "strict";
843 ProtectHome = mkDefault true;
844 PrivateTmp = true;
845 PrivateDevices = true;
846 ProtectHostname = true;
847 ProtectClock = true;
848 ProtectKernelTunables = true;
849 ProtectKernelModules = true;
850 ProtectKernelLogs = true;
851 ProtectControlGroups = true;
852 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
853 RestrictNamespaces = true;
854 LockPersonality = true;
855 MemoryDenyWriteExecute = !(builtins.any (mod: (mod.allowMemoryWriteExecute or false)) cfg.package.modules);
856 RestrictRealtime = true;
857 RestrictSUIDSGID = true;
858 RemoveIPC = true;
859 PrivateMounts = true;
860 # System Call Filtering
861 SystemCallArchitectures = "native";
862 SystemCallFilter = "~@cpu-emulation @debug @keyring @ipc @mount @obsolete @privileged @setuid";
863 };
864 };
865
866 environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload {
867 source = configFile;
868 };
869
870 # This service waits for all certificates to be available
871 # before reloading nginx configuration.
872 # sslTargets are added to wantedBy + before
873 # which allows the acme-finished-$cert.target to signify the successful updating
874 # of certs end-to-end.
875 systemd.services.nginx-config-reload = let
876 sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
877 sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
878 in mkIf (cfg.enableReload || sslServices != []) {
879 wants = optionals (cfg.enableReload) [ "nginx.service" ];
880 wantedBy = sslServices ++ [ "multi-user.target" ];
881 # Before the finished targets, after the renew services.
882 # This service might be needed for HTTP-01 challenges, but we only want to confirm
883 # certs are updated _after_ config has been reloaded.
884 before = sslTargets;
885 after = sslServices;
886 restartTriggers = optionals (cfg.enableReload) [ configFile ];
887 # Block reloading if not all certs exist yet.
888 # Happens when config changes add new vhosts/certs.
889 unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
890 serviceConfig = {
891 Type = "oneshot";
892 TimeoutSec = 60;
893 ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active nginx.service";
894 ExecStart = "/run/current-system/systemd/bin/systemctl reload nginx.service";
895 };
896 };
897
898 security.acme.certs = let
899 acmePairs = map (vhostConfig: nameValuePair vhostConfig.serverName {
900 group = mkDefault cfg.group;
901 webroot = vhostConfig.acmeRoot;
902 extraDomainNames = vhostConfig.serverAliases;
903 # Filter for enableACME-only vhosts. Don't want to create dud certs
904 }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
905 in listToAttrs acmePairs;
906
907 users.users = optionalAttrs (cfg.user == "nginx") {
908 nginx = {
909 group = cfg.group;
910 isSystemUser = true;
911 uid = config.ids.uids.nginx;
912 };
913 };
914
915 users.groups = optionalAttrs (cfg.group == "nginx") {
916 nginx.gid = config.ids.gids.nginx;
917 };
918
919 };
920}