at 17.09-beta 19 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 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}