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