1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.nginx;
7 virtualHosts = mapAttrs (vhostName: vhostConfig:
8 let
9 serverName = if vhostConfig.serverName != null
10 then vhostConfig.serverName
11 else vhostName;
12 acmeDirectory = config.security.acme.directory;
13 in
14 vhostConfig // {
15 inherit serverName;
16 } // (optionalAttrs vhostConfig.enableACME {
17 sslCertificate = "${acmeDirectory}/${serverName}/fullchain.pem";
18 sslCertificateKey = "${acmeDirectory}/${serverName}/key.pem";
19 sslTrustedCertificate = "${acmeDirectory}/${serverName}/full.pem";
20 }) // (optionalAttrs (vhostConfig.useACMEHost != null) {
21 sslCertificate = "${acmeDirectory}/${vhostConfig.useACMEHost}/fullchain.pem";
22 sslCertificateKey = "${acmeDirectory}/${vhostConfig.useACMEHost}/key.pem";
23 sslTrustedCertificate = "${acmeDirectory}/${vhostConfig.useACMEHost}/full.pem";
24 })
25 ) cfg.virtualHosts;
26 enableIPv6 = config.networking.enableIPv6;
27
28 recommendedProxyConfig = pkgs.writeText "nginx-recommended-proxy-headers.conf" ''
29 proxy_set_header Host $host;
30 proxy_set_header X-Real-IP $remote_addr;
31 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
32 proxy_set_header X-Forwarded-Proto $scheme;
33 proxy_set_header X-Forwarded-Host $host;
34 proxy_set_header X-Forwarded-Server $host;
35 proxy_set_header Accept-Encoding "";
36 '';
37
38 upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
39 upstream ${name} {
40 ${toString (flip mapAttrsToList upstream.servers (name: server: ''
41 server ${name} ${optionalString server.backup "backup"};
42 ''))}
43 ${upstream.extraConfig}
44 }
45 ''));
46
47 configFile = pkgs.writeText "nginx.conf" ''
48 user ${cfg.user} ${cfg.group};
49 error_log stderr;
50 daemon off;
51
52 ${cfg.config}
53
54 ${optionalString (cfg.eventsConfig != "" || cfg.config == "") ''
55 events {
56 ${cfg.eventsConfig}
57 }
58 ''}
59
60 ${optionalString (cfg.httpConfig == "" && cfg.config == "") ''
61 http {
62 include ${cfg.package}/conf/mime.types;
63 include ${cfg.package}/conf/fastcgi.conf;
64 include ${cfg.package}/conf/uwsgi_params;
65
66 ${optionalString (cfg.resolver.addresses != []) ''
67 resolver ${toString cfg.resolver.addresses} ${optionalString (cfg.resolver.valid != "") "valid=${cfg.resolver.valid}"};
68 ''}
69 ${upstreamConfig}
70
71 ${optionalString (cfg.recommendedOptimisation) ''
72 # optimisation
73 sendfile on;
74 tcp_nopush on;
75 tcp_nodelay on;
76 keepalive_timeout 65;
77 types_hash_max_size 2048;
78 ''}
79
80 ssl_protocols ${cfg.sslProtocols};
81 ssl_ciphers ${cfg.sslCiphers};
82 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
83
84 ${optionalString (cfg.recommendedTlsSettings) ''
85 ssl_session_cache shared:SSL:42m;
86 ssl_session_timeout 23m;
87 ssl_ecdh_curve secp384r1;
88 ssl_prefer_server_ciphers on;
89 ssl_stapling on;
90 ssl_stapling_verify on;
91 ''}
92
93 ${optionalString (cfg.recommendedGzipSettings) ''
94 gzip on;
95 gzip_disable "msie6";
96 gzip_proxied any;
97 gzip_comp_level 5;
98 gzip_types
99 application/atom+xml
100 application/javascript
101 application/json
102 application/xml
103 application/xml+rss
104 image/svg+xml
105 text/css
106 text/javascript
107 text/plain
108 text/xml;
109 gzip_vary on;
110 ''}
111
112 ${optionalString (cfg.recommendedProxySettings) ''
113 proxy_redirect off;
114 proxy_connect_timeout 90;
115 proxy_send_timeout 90;
116 proxy_read_timeout 90;
117 proxy_http_version 1.0;
118 include ${recommendedProxyConfig};
119 ''}
120
121 # $connection_upgrade is used for websocket proxying
122 map $http_upgrade $connection_upgrade {
123 default upgrade;
124 ''' close;
125 }
126 client_max_body_size ${cfg.clientMaxBodySize};
127
128 server_tokens ${if cfg.serverTokens then "on" else "off"};
129
130 ${cfg.commonHttpConfig}
131
132 ${vhosts}
133
134 ${optionalString cfg.statusPage ''
135 server {
136 listen 80;
137 ${optionalString enableIPv6 "listen [::]:80;" }
138
139 server_name localhost;
140
141 location /nginx_status {
142 stub_status on;
143 access_log off;
144 allow 127.0.0.1;
145 ${optionalString enableIPv6 "allow ::1;"}
146 deny all;
147 }
148 }
149 ''}
150
151 ${cfg.appendHttpConfig}
152 }''}
153
154 ${optionalString (cfg.httpConfig != "") ''
155 http {
156 include ${cfg.package}/conf/mime.types;
157 include ${cfg.package}/conf/fastcgi.conf;
158 include ${cfg.package}/conf/uwsgi_params;
159 ${cfg.httpConfig}
160 }''}
161
162 ${cfg.appendConfig}
163 '';
164
165 vhosts = concatStringsSep "\n" (mapAttrsToList (vhostName: vhost:
166 let
167 onlySSL = vhost.onlySSL || vhost.enableSSL;
168 hasSSL = onlySSL || vhost.addSSL || vhost.forceSSL;
169
170 defaultListen =
171 if vhost.listen != [] then vhost.listen
172 else ((optionals hasSSL (
173 singleton { addr = "0.0.0.0"; port = 443; ssl = true; }
174 ++ optional enableIPv6 { addr = "[::]"; port = 443; ssl = true; }
175 )) ++ optionals (!onlySSL) (
176 singleton { addr = "0.0.0.0"; port = 80; ssl = false; }
177 ++ optional enableIPv6 { addr = "[::]"; port = 80; ssl = false; }
178 ));
179
180 hostListen =
181 if vhost.forceSSL
182 then filter (x: x.ssl) defaultListen
183 else defaultListen;
184
185 listenString = { addr, port, ssl, ... }:
186 "listen ${addr}:${toString port} "
187 + optionalString ssl "ssl "
188 + optionalString (ssl && vhost.http2) "http2 "
189 + optionalString vhost.default "default_server "
190 + ";";
191
192 redirectListen = filter (x: !x.ssl) defaultListen;
193
194 acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
195 location /.well-known/acme-challenge {
196 ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
197 root ${vhost.acmeRoot};
198 auth_basic off;
199 }
200 ${optionalString (vhost.acmeFallbackHost != null) ''
201 location @acme-fallback {
202 auth_basic off;
203 proxy_pass http://${vhost.acmeFallbackHost};
204 }
205 ''}
206 '';
207
208 in ''
209 ${optionalString vhost.forceSSL ''
210 server {
211 ${concatMapStringsSep "\n" listenString redirectListen}
212
213 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
214 ${acmeLocation}
215 location / {
216 return 301 https://$host$request_uri;
217 }
218 }
219 ''}
220
221 server {
222 ${concatMapStringsSep "\n" listenString hostListen}
223 server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
224 ${acmeLocation}
225 ${optionalString (vhost.root != null) "root ${vhost.root};"}
226 ${optionalString (vhost.globalRedirect != null) ''
227 return 301 http${optionalString hasSSL "s"}://${vhost.globalRedirect}$request_uri;
228 ''}
229 ${optionalString hasSSL ''
230 ssl_certificate ${vhost.sslCertificate};
231 ssl_certificate_key ${vhost.sslCertificateKey};
232 ''}
233 ${optionalString (hasSSL && vhost.sslTrustedCertificate != null) ''
234 ssl_trusted_certificate ${vhost.sslTrustedCertificate};
235 ''}
236
237 ${optionalString (vhost.basicAuthFile != null || vhost.basicAuth != {}) ''
238 auth_basic secured;
239 auth_basic_user_file ${if vhost.basicAuthFile != null then vhost.basicAuthFile else mkHtpasswd vhostName vhost.basicAuth};
240 ''}
241
242 ${mkLocations vhost.locations}
243
244 ${vhost.extraConfig}
245 }
246 ''
247 ) virtualHosts);
248 mkLocations = locations: concatStringsSep "\n" (mapAttrsToList (location: config: ''
249 location ${location} {
250 ${optionalString (config.proxyPass != null && !cfg.proxyResolveWhileRunning)
251 "proxy_pass ${config.proxyPass};"
252 }
253 ${optionalString (config.proxyPass != null && cfg.proxyResolveWhileRunning) ''
254 set $nix_proxy_target "${config.proxyPass}";
255 proxy_pass $nix_proxy_target;
256 ''}
257 ${optionalString config.proxyWebsockets ''
258 proxy_http_version 1.1;
259 proxy_set_header Upgrade $http_upgrade;
260 proxy_set_header Connection $connection_upgrade;
261 ''}
262 ${optionalString (config.index != null) "index ${config.index};"}
263 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
264 ${optionalString (config.root != null) "root ${config.root};"}
265 ${optionalString (config.alias != null) "alias ${config.alias};"}
266 ${config.extraConfig}
267 ${optionalString (config.proxyPass != null && cfg.recommendedProxySettings) "include ${recommendedProxyConfig};"}
268 }
269 '') locations);
270 mkHtpasswd = vhostName: authDef: pkgs.writeText "${vhostName}.htpasswd" (
271 concatStringsSep "\n" (mapAttrsToList (user: password: ''
272 ${user}:{PLAIN}${password}
273 '') authDef)
274 );
275in
276
277{
278 options = {
279 services.nginx = {
280 enable = mkEnableOption "Nginx Web Server";
281
282 statusPage = mkOption {
283 default = false;
284 type = types.bool;
285 description = "
286 Enable status page reachable from localhost on http://127.0.0.1/nginx_status.
287 ";
288 };
289
290 recommendedTlsSettings = mkOption {
291 default = false;
292 type = types.bool;
293 description = "
294 Enable recommended TLS settings.
295 ";
296 };
297
298 recommendedOptimisation = mkOption {
299 default = false;
300 type = types.bool;
301 description = "
302 Enable recommended optimisation settings.
303 ";
304 };
305
306 recommendedGzipSettings = mkOption {
307 default = false;
308 type = types.bool;
309 description = "
310 Enable recommended gzip settings.
311 ";
312 };
313
314 recommendedProxySettings = mkOption {
315 default = false;
316 type = types.bool;
317 description = "
318 Enable recommended proxy settings.
319 ";
320 };
321
322 package = mkOption {
323 default = pkgs.nginxStable;
324 defaultText = "pkgs.nginxStable";
325 type = types.package;
326 description = "
327 Nginx package to use. This defaults to the stable version. Note
328 that the nginx team recommends to use the mainline version which
329 available in nixpkgs as <literal>nginxMainline</literal>.
330 ";
331 };
332
333 config = mkOption {
334 default = "";
335 description = "
336 Verbatim nginx.conf configuration.
337 This is mutually exclusive with the structured configuration
338 via virtualHosts and the recommendedXyzSettings configuration
339 options. See appendConfig for appending to the generated http block.
340 ";
341 };
342
343 appendConfig = mkOption {
344 type = types.lines;
345 default = "";
346 description = ''
347 Configuration lines appended to the generated Nginx
348 configuration file. Commonly used by different modules
349 providing http snippets. <option>appendConfig</option>
350 can be specified more than once and it's value will be
351 concatenated (contrary to <option>config</option> which
352 can be set only once).
353 '';
354 };
355
356 commonHttpConfig = mkOption {
357 type = types.lines;
358 default = "";
359 example = ''
360 resolver 127.0.0.1 valid=5s;
361
362 log_format myformat '$remote_addr - $remote_user [$time_local] '
363 '"$request" $status $body_bytes_sent '
364 '"$http_referer" "$http_user_agent"';
365 '';
366 description = ''
367 With nginx you must provide common http context definitions before
368 they are used, e.g. log_format, resolver, etc. inside of server
369 or location contexts. Use this attribute to set these definitions
370 at the appropriate location.
371 '';
372 };
373
374 httpConfig = mkOption {
375 type = types.lines;
376 default = "";
377 description = "
378 Configuration lines to be set inside the http block.
379 This is mutually exclusive with the structured configuration
380 via virtualHosts and the recommendedXyzSettings configuration
381 options. See appendHttpConfig for appending to the generated http block.
382 ";
383 };
384
385 eventsConfig = mkOption {
386 type = types.lines;
387 default = "";
388 description = ''
389 Configuration lines to be set inside the events block.
390 '';
391 };
392
393 appendHttpConfig = mkOption {
394 type = types.lines;
395 default = "";
396 description = "
397 Configuration lines to be appended to the generated http block.
398 This is mutually exclusive with using config and httpConfig for
399 specifying the whole http block verbatim.
400 ";
401 };
402
403 stateDir = mkOption {
404 default = "/var/spool/nginx";
405 description = "
406 Directory holding all state for nginx to run.
407 ";
408 };
409
410 user = mkOption {
411 type = types.str;
412 default = "nginx";
413 description = "User account under which nginx runs.";
414 };
415
416 group = mkOption {
417 type = types.str;
418 default = "nginx";
419 description = "Group account under which nginx runs.";
420 };
421
422 serverTokens = mkOption {
423 type = types.bool;
424 default = false;
425 description = "Show nginx version in headers and error pages.";
426 };
427
428 clientMaxBodySize = mkOption {
429 type = types.string;
430 default = "10m";
431 description = "Set nginx global client_max_body_size.";
432 };
433
434 sslCiphers = mkOption {
435 type = types.str;
436 default = "EECDH+aRSA+AESGCM:EDH+aRSA:EECDH+aRSA:+AES256:+AES128:+SHA1:!CAMELLIA:!SEED:!3DES:!DES:!RC4:!eNULL";
437 description = "Ciphers to choose from when negotiating tls handshakes.";
438 };
439
440 sslProtocols = mkOption {
441 type = types.str;
442 default = "TLSv1.2";
443 example = "TLSv1 TLSv1.1 TLSv1.2";
444 description = "Allowed TLS protocol versions.";
445 };
446
447 sslDhparam = mkOption {
448 type = types.nullOr types.path;
449 default = null;
450 example = "/path/to/dhparams.pem";
451 description = "Path to DH parameters file.";
452 };
453
454 proxyResolveWhileRunning = mkOption {
455 type = types.bool;
456 default = false;
457 description = ''
458 Resolves domains of proxyPass targets at runtime
459 and not only at start, you have to set
460 services.nginx.resolver, too.
461 '';
462 };
463
464 resolver = mkOption {
465 type = types.submodule {
466 options = {
467 addresses = mkOption {
468 type = types.listOf types.str;
469 default = [];
470 example = literalExample ''[ "[::1]" "127.0.0.1:5353" ]'';
471 description = "List of resolvers to use";
472 };
473 valid = mkOption {
474 type = types.str;
475 default = "";
476 example = "30s";
477 description = ''
478 By default, nginx caches answers using the TTL value of a response.
479 An optional valid parameter allows overriding it
480 '';
481 };
482 };
483 };
484 description = ''
485 Configures name servers used to resolve names of upstream servers into addresses
486 '';
487 default = {};
488 };
489
490 upstreams = mkOption {
491 type = types.attrsOf (types.submodule {
492 options = {
493 servers = mkOption {
494 type = types.attrsOf (types.submodule {
495 options = {
496 backup = mkOption {
497 type = types.bool;
498 default = false;
499 description = ''
500 Marks the server as a backup server. It will be passed
501 requests when the primary servers are unavailable.
502 '';
503 };
504 };
505 });
506 description = ''
507 Defines the address and other parameters of the upstream servers.
508 '';
509 default = {};
510 };
511 extraConfig = mkOption {
512 type = types.lines;
513 default = "";
514 description = ''
515 These lines go to the end of the upstream verbatim.
516 '';
517 };
518 };
519 });
520 description = ''
521 Defines a group of servers to use as proxy target.
522 '';
523 default = {};
524 };
525
526 virtualHosts = mkOption {
527 type = types.attrsOf (types.submodule (import ./vhost-options.nix {
528 inherit config lib;
529 }));
530 default = {
531 localhost = {};
532 };
533 example = literalExample ''
534 {
535 "hydra.example.com" = {
536 forceSSL = true;
537 enableACME = true;
538 locations."/" = {
539 proxyPass = "http://localhost:3000";
540 };
541 };
542 };
543 '';
544 description = "Declarative vhost config";
545 };
546 };
547 };
548
549 config = mkIf cfg.enable {
550 # TODO: test user supplied config file pases syntax test
551
552 warnings =
553 let
554 deprecatedSSL = name: config: optional config.enableSSL
555 ''
556 config.services.nginx.virtualHosts.<name>.enableSSL is deprecated,
557 use config.services.nginx.virtualHosts.<name>.onlySSL instead.
558 '';
559
560 in flatten (mapAttrsToList deprecatedSSL virtualHosts);
561
562 assertions =
563 let
564 hostOrAliasIsNull = l: l.root == null || l.alias == null;
565 in [
566 {
567 assertion = all (host: all hostOrAliasIsNull (attrValues host.locations)) (attrValues virtualHosts);
568 message = "Only one of nginx root or alias can be specified on a location.";
569 }
570
571 {
572 assertion = all (conf: with conf;
573 !(addSSL && (onlySSL || enableSSL)) &&
574 !(forceSSL && (onlySSL || enableSSL)) &&
575 !(addSSL && forceSSL)
576 ) (attrValues virtualHosts);
577 message = ''
578 Options services.nginx.service.virtualHosts.<name>.addSSL,
579 services.nginx.virtualHosts.<name>.onlySSL and services.nginx.virtualHosts.<name>.forceSSL
580 are mutually exclusive.
581 '';
582 }
583
584 {
585 assertion = all (conf: !(conf.enableACME && conf.useACMEHost != null)) (attrValues virtualHosts);
586 message = ''
587 Options services.nginx.service.virtualHosts.<name>.enableACME and
588 services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
589 '';
590 }
591 ];
592
593 systemd.services.nginx = {
594 description = "Nginx Web Server";
595 after = [ "network.target" ];
596 wantedBy = [ "multi-user.target" ];
597 stopIfChanged = false;
598 preStart =
599 ''
600 mkdir -p ${cfg.stateDir}/logs
601 chmod 700 ${cfg.stateDir}
602 chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
603 ${cfg.package}/bin/nginx -c ${configFile} -p ${cfg.stateDir} -t
604 '';
605 serviceConfig = {
606 ExecStart = "${cfg.package}/bin/nginx -c ${configFile} -p ${cfg.stateDir}";
607 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
608 Restart = "always";
609 RestartSec = "10s";
610 StartLimitInterval = "1min";
611 };
612 };
613
614 security.acme.certs = filterAttrs (n: v: v != {}) (
615 let
616 vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
617 acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME && vhostConfig.useACMEHost == null) vhostsConfigs;
618 acmePairs = map (vhostConfig: { name = vhostConfig.serverName; value = {
619 user = cfg.user;
620 group = lib.mkDefault cfg.group;
621 webroot = vhostConfig.acmeRoot;
622 extraDomains = genAttrs vhostConfig.serverAliases (alias: null);
623 postRun = ''
624 systemctl reload nginx
625 '';
626 }; }) acmeEnabledVhosts;
627 in
628 listToAttrs acmePairs
629 );
630
631 users.users = optionalAttrs (cfg.user == "nginx") (singleton
632 { name = "nginx";
633 group = cfg.group;
634 uid = config.ids.uids.nginx;
635 });
636
637 users.groups = optionalAttrs (cfg.group == "nginx") (singleton
638 { name = "nginx";
639 gid = config.ids.gids.nginx;
640 });
641 };
642}