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