1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.nginx;
7 virtualHosts = mapAttrs (vhostName: vhostConfig:
8 vhostConfig // (optionalAttrs vhostConfig.enableACME {
9 sslCertificate = "/var/lib/acme/${vhostName}/fullchain.pem";
10 sslCertificateKey = "/var/lib/acme/${vhostName}/key.pem";
11 })
12 ) cfg.virtualHosts;
13
14 configFile = pkgs.writeText "nginx.conf" ''
15 user ${cfg.user} ${cfg.group};
16 error_log stderr;
17 daemon off;
18
19 ${cfg.config}
20
21 ${optionalString (cfg.httpConfig == "" && cfg.config == "") ''
22 events {}
23
24 http {
25 include ${cfg.package}/conf/mime.types;
26 include ${cfg.package}/conf/fastcgi.conf;
27
28 ${optionalString (cfg.recommendedOptimisation) ''
29 # optimisation
30 sendfile on;
31 tcp_nopush on;
32 tcp_nodelay on;
33 keepalive_timeout 65;
34 types_hash_max_size 2048;
35 ''}
36
37 ssl_protocols ${cfg.sslProtocols};
38 ssl_ciphers ${cfg.sslCiphers};
39 ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
40
41 ${optionalString (cfg.recommendedTlsSettings) ''
42 ssl_session_cache shared:SSL:42m;
43 ssl_session_timeout 23m;
44 ssl_ecdh_curve secp384r1;
45 ssl_prefer_server_ciphers on;
46 ssl_stapling on;
47 ssl_stapling_verify on;
48 ''}
49
50 ${optionalString (cfg.recommendedGzipSettings) ''
51 gzip on;
52 gzip_disable "msie6";
53 gzip_proxied any;
54 gzip_comp_level 9;
55 gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
56 ''}
57
58 ${optionalString (cfg.recommendedProxySettings) ''
59 proxy_set_header Host $host;
60 proxy_set_header X-Real-IP $remote_addr;
61 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
62 proxy_set_header X-Forwarded-Proto $scheme;
63 proxy_set_header X-Forwarded-Host $host;
64 proxy_set_header X-Forwarded-Server $host;
65 proxy_set_header Accept-Encoding "";
66
67 proxy_redirect off;
68 proxy_connect_timeout 90;
69 proxy_send_timeout 90;
70 proxy_read_timeout 90;
71 proxy_http_version 1.0;
72 ''}
73
74 client_max_body_size ${cfg.clientMaxBodySize};
75
76 server_tokens ${if cfg.serverTokens then "on" else "off"};
77
78 ${vhosts}
79
80 ${optionalString cfg.statusPage ''
81 server {
82 listen 80;
83 listen [::]:80;
84
85 server_name localhost;
86
87 location /nginx_status {
88 stub_status on;
89 access_log off;
90 allow 127.0.0.1;
91 allow ::1;
92 deny all;
93 }
94 }
95 ''}
96
97 ${cfg.appendHttpConfig}
98 }''}
99
100 ${optionalString (cfg.httpConfig != "") ''
101 events {}
102 http {
103 include ${cfg.package}/conf/mime.types;
104 include ${cfg.package}/conf/fastcgi.conf;
105 ${cfg.httpConfig}
106 }''}
107
108 ${cfg.appendConfig}
109 '';
110
111 vhosts = concatStringsSep "\n" (mapAttrsToList (serverName: vhost:
112 let
113 ssl = vhost.enableSSL || vhost.forceSSL;
114 port = if vhost.port != null then vhost.port else (if ssl then 443 else 80);
115 listenString = toString port + optionalString ssl " ssl http2"
116 + optionalString vhost.default " default";
117 acmeLocation = optionalString vhost.enableACME ''
118 location /.well-known/acme-challenge {
119 try_files $uri @acme-fallback;
120 root ${vhost.acmeRoot};
121 auth_basic off;
122 }
123 location @acme-fallback {
124 auth_basic off;
125 proxy_pass http://${vhost.acmeFallbackHost};
126 }
127 '';
128 in ''
129 ${optionalString vhost.forceSSL ''
130 server {
131 listen 80 ${optionalString vhost.default "default"};
132 listen [::]:80 ${optionalString vhost.default "default"};
133
134 server_name ${serverName} ${concatStringsSep " " vhost.serverAliases};
135 ${acmeLocation}
136 location / {
137 return 301 https://$host${optionalString (port != 443) ":${port}"}$request_uri;
138 }
139 }
140 ''}
141
142 server {
143 listen ${listenString};
144 listen [::]:${listenString};
145
146 server_name ${serverName} ${concatStringsSep " " vhost.serverAliases};
147 ${acmeLocation}
148 ${optionalString (vhost.root != null) "root ${vhost.root};"}
149 ${optionalString (vhost.globalRedirect != null) ''
150 return 301 http${optionalString ssl "s"}://${vhost.globalRedirect}$request_uri;
151 ''}
152 ${optionalString ssl ''
153 ssl_certificate ${vhost.sslCertificate};
154 ssl_certificate_key ${vhost.sslCertificateKey};
155 ''}
156
157 ${optionalString (vhost.basicAuth != {}) (mkBasicAuth serverName vhost.basicAuth)}
158
159 ${mkLocations vhost.locations}
160
161 ${vhost.extraConfig}
162 }
163 ''
164 ) virtualHosts);
165 mkLocations = locations: concatStringsSep "\n" (mapAttrsToList (location: config: ''
166 location ${location} {
167 ${optionalString (config.proxyPass != null) "proxy_pass ${config.proxyPass};"}
168 ${optionalString (config.index != null) "index ${config.index};"}
169 ${optionalString (config.tryFiles != null) "try_files ${config.tryFiles};"}
170 ${optionalString (config.root != null) "root ${config.root};"}
171 ${config.extraConfig}
172 }
173 '') locations);
174 mkBasicAuth = serverName: authDef: let
175 htpasswdFile = pkgs.writeText "${serverName}.htpasswd" (
176 concatStringsSep "\n" (mapAttrsToList (user: password: ''
177 ${user}:{PLAIN}${password}
178 '') authDef)
179 );
180 in ''
181 auth_basic secured;
182 auth_basic_user_file ${htpasswdFile};
183 '';
184in
185
186{
187 options = {
188 services.nginx = {
189 enable = mkEnableOption "Nginx Web Server";
190
191 statusPage = mkOption {
192 default = false;
193 type = types.bool;
194 description = "
195 Enable status page reachable from localhost on http://127.0.0.1/nginx_status.
196 ";
197 };
198
199 recommendedTlsSettings = mkOption {
200 default = false;
201 type = types.bool;
202 description = "
203 Enable recommended TLS settings.
204 ";
205 };
206
207 recommendedOptimisation = mkOption {
208 default = false;
209 type = types.bool;
210 description = "
211 Enable recommended optimisation settings.
212 ";
213 };
214
215 recommendedGzipSettings = mkOption {
216 default = false;
217 type = types.bool;
218 description = "
219 Enable recommended gzip settings.
220 ";
221 };
222
223 recommendedProxySettings = mkOption {
224 default = false;
225 type = types.bool;
226 description = "
227 Enable recommended proxy settings.
228 ";
229 };
230
231 package = mkOption {
232 default = pkgs.nginx;
233 defaultText = "pkgs.nginx";
234 type = types.package;
235 description = "
236 Nginx package to use.
237 ";
238 };
239
240 config = mkOption {
241 default = "";
242 description = "
243 Verbatim nginx.conf configuration.
244 This is mutually exclusive with the structured configuration
245 via virtualHosts and the recommendedXyzSettings configuration
246 options. See appendConfig for appending to the generated http block.
247 ";
248 };
249
250 appendConfig = mkOption {
251 type = types.lines;
252 default = "";
253 description = ''
254 Configuration lines appended to the generated Nginx
255 configuration file. Commonly used by different modules
256 providing http snippets. <option>appendConfig</option>
257 can be specified more than once and it's value will be
258 concatenated (contrary to <option>config</option> which
259 can be set only once).
260 '';
261 };
262
263 httpConfig = mkOption {
264 type = types.lines;
265 default = "";
266 description = "
267 Configuration lines to be set inside the http block.
268 This is mutually exclusive with the structured configuration
269 via virtualHosts and the recommendedXyzSettings configuration
270 options. See appendHttpConfig for appending to the generated http block.
271 ";
272 };
273
274 appendHttpConfig = mkOption {
275 type = types.lines;
276 default = "";
277 description = "
278 Configuration lines to be appended to the generated http block.
279 This is mutually exclusive with using config and httpConfig for
280 specifying the whole http block verbatim.
281 ";
282 };
283
284 stateDir = mkOption {
285 default = "/var/spool/nginx";
286 description = "
287 Directory holding all state for nginx to run.
288 ";
289 };
290
291 user = mkOption {
292 type = types.str;
293 default = "nginx";
294 description = "User account under which nginx runs.";
295 };
296
297 group = mkOption {
298 type = types.str;
299 default = "nginx";
300 description = "Group account under which nginx runs.";
301 };
302
303 serverTokens = mkOption {
304 type = types.bool;
305 default = false;
306 description = "Show nginx version in headers and error pages.";
307 };
308
309 clientMaxBodySize = mkOption {
310 type = types.string;
311 default = "10m";
312 description = "Set nginx global client_max_body_size.";
313 };
314
315 sslCiphers = mkOption {
316 type = types.str;
317 default = "EECDH+aRSA+AESGCM:EDH+aRSA:EECDH+aRSA:+AES256:+AES128:+SHA1:!CAMELLIA:!SEED:!3DES:!DES:!RC4:!eNULL";
318 description = "Ciphers to choose from when negotiating tls handshakes.";
319 };
320
321 sslProtocols = mkOption {
322 type = types.str;
323 default = "TLSv1.2";
324 example = "TLSv1 TLSv1.1 TLSv1.2";
325 description = "Allowed TLS protocol versions.";
326 };
327
328 sslDhparam = mkOption {
329 type = types.nullOr types.path;
330 default = null;
331 example = "/path/to/dhparams.pem";
332 description = "Path to DH parameters file.";
333 };
334
335 virtualHosts = mkOption {
336 type = types.attrsOf (types.submodule (import ./vhost-options.nix {
337 inherit lib;
338 }));
339 default = {
340 localhost = {};
341 };
342 example = literalExample ''
343 {
344 "hydra.example.com" = {
345 forceSSL = true;
346 enableACME = true;
347 locations."/" = {
348 proxyPass = "http://localhost:3000";
349 };
350 };
351 };
352 '';
353 description = "Declarative vhost config";
354 };
355 };
356 };
357
358 config = mkIf cfg.enable {
359 # TODO: test user supplied config file pases syntax test
360
361 systemd.services.nginx = {
362 description = "Nginx Web Server";
363 after = [ "network.target" ];
364 wantedBy = [ "multi-user.target" ];
365 preStart =
366 ''
367 mkdir -p ${cfg.stateDir}/logs
368 chmod 700 ${cfg.stateDir}
369 chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
370 '';
371 serviceConfig = {
372 ExecStart = "${cfg.package}/bin/nginx -c ${configFile} -p ${cfg.stateDir}";
373 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
374 Restart = "always";
375 RestartSec = "10s";
376 StartLimitInterval = "1min";
377 };
378 };
379
380 security.acme.certs = filterAttrs (n: v: v != {}) (
381 mapAttrs (vhostName: vhostConfig:
382 optionalAttrs vhostConfig.enableACME {
383 webroot = vhostConfig.acmeRoot;
384 extraDomains = genAttrs vhostConfig.serverAliases (alias: null);
385 }
386 ) virtualHosts
387 );
388
389 users.extraUsers = optionalAttrs (cfg.user == "nginx") (singleton
390 { name = "nginx";
391 group = cfg.group;
392 uid = config.ids.uids.nginx;
393 });
394
395 users.extraGroups = optionalAttrs (cfg.group == "nginx") (singleton
396 { name = "nginx";
397 gid = config.ids.gids.nginx;
398 });
399 };
400}