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