at 18.09-beta 21 kB view raw
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}