1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.services.httpd; 8 9 certs = config.security.acme.certs; 10 11 runtimeDir = "/run/httpd"; 12 13 pkg = cfg.package.out; 14 15 apachectl = pkgs.runCommand "apachectl" { meta.priority = -1; } '' 16 mkdir -p $out/bin 17 cp ${pkg}/bin/apachectl $out/bin/apachectl 18 sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f /etc/httpd/httpd.conf|' 19 ''; 20 21 php = cfg.phpPackage.override { apxs2Support = true; apacheHttpd = pkg; }; 22 23 phpModuleName = let 24 majorVersion = lib.versions.major (lib.getVersion php); 25 in (if majorVersion == "8" then "php" else "php${majorVersion}"); 26 27 mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; }; 28 29 vhosts = attrValues cfg.virtualHosts; 30 31 # certName is used later on to determine systemd service names. 32 acmeEnabledVhosts = map (hostOpts: hostOpts // { 33 certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName; 34 }) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts); 35 36 dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts); 37 38 mkListenInfo = hostOpts: 39 if hostOpts.listen != [] then 40 hostOpts.listen 41 else 42 optionals (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) (map (addr: { ip = addr; port = 443; ssl = true; }) hostOpts.listenAddresses) ++ 43 optionals (!hostOpts.onlySSL) (map (addr: { ip = addr; port = 80; ssl = false; }) hostOpts.listenAddresses) 44 ; 45 46 listenInfo = unique (concatMap mkListenInfo vhosts); 47 48 enableHttp2 = any (vhost: vhost.http2) vhosts; 49 enableSSL = any (listen: listen.ssl) listenInfo; 50 enableUserDir = any (vhost: vhost.enableUserDir) vhosts; 51 52 # NOTE: generally speaking order of modules is very important 53 modules = 54 [ # required apache modules our httpd service cannot run without 55 "authn_core" "authz_core" 56 "log_config" 57 "mime" "autoindex" "negotiation" "dir" 58 "alias" "rewrite" 59 "unixd" "slotmem_shm" "socache_shmcb" 60 "mpm_${cfg.mpm}" 61 ] 62 ++ (if cfg.mpm == "prefork" then [ "cgi" ] else [ "cgid" ]) 63 ++ optional enableHttp2 "http2" 64 ++ optional enableSSL "ssl" 65 ++ optional enableUserDir "userdir" 66 ++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } 67 ++ optional cfg.enablePHP { name = phpModuleName; path = "${php}/modules/lib${phpModuleName}.so"; } 68 ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } 69 ++ cfg.extraModules; 70 71 loggingConf = (if cfg.logFormat != "none" then '' 72 ErrorLog ${cfg.logDir}/error.log 73 74 LogLevel notice 75 76 LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined 77 LogFormat "%h %l %u %t \"%r\" %>s %b" common 78 LogFormat "%{Referer}i -> %U" referer 79 LogFormat "%{User-agent}i" agent 80 81 CustomLog ${cfg.logDir}/access.log ${cfg.logFormat} 82 '' else '' 83 ErrorLog /dev/null 84 ''); 85 86 87 browserHacks = '' 88 <IfModule mod_setenvif.c> 89 BrowserMatch "Mozilla/2" nokeepalive 90 BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 91 BrowserMatch "RealPlayer 4\.0" force-response-1.0 92 BrowserMatch "Java/1\.0" force-response-1.0 93 BrowserMatch "JDK/1\.0" force-response-1.0 94 BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully 95 BrowserMatch "^WebDrive" redirect-carefully 96 BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully 97 BrowserMatch "^gnome-vfs" redirect-carefully 98 </IfModule> 99 ''; 100 101 102 sslConf = '' 103 <IfModule mod_ssl.c> 104 SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000) 105 106 Mutex posixsem 107 108 SSLRandomSeed startup builtin 109 SSLRandomSeed connect builtin 110 111 SSLProtocol ${cfg.sslProtocols} 112 SSLCipherSuite ${cfg.sslCiphers} 113 SSLHonorCipherOrder on 114 </IfModule> 115 ''; 116 117 118 mimeConf = '' 119 TypesConfig ${pkg}/conf/mime.types 120 121 AddType application/x-x509-ca-cert .crt 122 AddType application/x-pkcs7-crl .crl 123 AddType application/x-httpd-php .php .phtml 124 125 <IfModule mod_mime_magic.c> 126 MIMEMagicFile ${pkg}/conf/magic 127 </IfModule> 128 ''; 129 130 luaSetPaths = let 131 # support both lua and lua.withPackages derivations 132 luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion; 133 in 134 '' 135 <IfModule mod_lua.c> 136 LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so 137 LuaPackagePath ${cfg.package.lua5}/share/lua/${luaversion}/?.lua 138 </IfModule> 139 ''; 140 141 mkVHostConf = hostOpts: 142 let 143 adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr; 144 listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts); 145 listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts); 146 147 useACME = hostOpts.enableACME || hostOpts.useACMEHost != null; 148 sslCertDir = 149 if hostOpts.enableACME then certs.${hostOpts.hostName}.directory 150 else if hostOpts.useACMEHost != null then certs.${hostOpts.useACMEHost}.directory 151 else abort "This case should never happen."; 152 153 sslServerCert = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerCert; 154 sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey; 155 sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain; 156 157 acmeChallenge = optionalString (useACME && hostOpts.acmeRoot != null) '' 158 Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/" 159 <Directory "${hostOpts.acmeRoot}"> 160 AllowOverride None 161 Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec 162 Require method GET POST OPTIONS 163 Require all granted 164 </Directory> 165 ''; 166 in 167 optionalString (listen != []) '' 168 <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}> 169 ServerName ${hostOpts.hostName} 170 ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} 171 ${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"} 172 <IfModule mod_ssl.c> 173 SSLEngine off 174 </IfModule> 175 ${acmeChallenge} 176 ${if hostOpts.forceSSL then '' 177 <IfModule mod_rewrite.c> 178 RewriteEngine on 179 RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC] 180 RewriteCond %{HTTPS} off 181 RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} 182 </IfModule> 183 '' else mkVHostCommonConf hostOpts} 184 </VirtualHost> 185 '' + 186 optionalString (listenSSL != []) '' 187 <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listenSSL}> 188 ServerName ${hostOpts.hostName} 189 ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} 190 ${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"} 191 SSLEngine on 192 SSLCertificateFile ${sslServerCert} 193 SSLCertificateKeyFile ${sslServerKey} 194 ${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"} 195 ${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"} 196 ${acmeChallenge} 197 ${mkVHostCommonConf hostOpts} 198 </VirtualHost> 199 '' 200 ; 201 202 mkVHostCommonConf = hostOpts: 203 let 204 documentRoot = if hostOpts.documentRoot != null 205 then hostOpts.documentRoot 206 else pkgs.emptyDirectory 207 ; 208 209 mkLocations = locations: concatStringsSep "\n" (map (config: '' 210 <Location ${config.location}> 211 ${optionalString (config.proxyPass != null) '' 212 <IfModule mod_proxy.c> 213 ProxyPass ${config.proxyPass} 214 ProxyPassReverse ${config.proxyPass} 215 </IfModule> 216 ''} 217 ${optionalString (config.index != null) '' 218 <IfModule mod_dir.c> 219 DirectoryIndex ${config.index} 220 </IfModule> 221 ''} 222 ${optionalString (config.alias != null) '' 223 <IfModule mod_alias.c> 224 Alias "${config.alias}" 225 </IfModule> 226 ''} 227 ${config.extraConfig} 228 </Location> 229 '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))); 230 in 231 '' 232 ${optionalString cfg.logPerVirtualHost '' 233 ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log 234 CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat} 235 ''} 236 237 ${optionalString (hostOpts.robotsEntries != "") '' 238 Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries} 239 ''} 240 241 DocumentRoot "${documentRoot}" 242 243 <Directory "${documentRoot}"> 244 Options Indexes FollowSymLinks 245 AllowOverride None 246 Require all granted 247 </Directory> 248 249 ${optionalString hostOpts.enableUserDir '' 250 UserDir public_html 251 UserDir disabled root 252 <Directory "/home/*/public_html"> 253 AllowOverride FileInfo AuthConfig Limit Indexes 254 Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec 255 <Limit GET POST OPTIONS> 256 Require all granted 257 </Limit> 258 <LimitExcept GET POST OPTIONS> 259 Require all denied 260 </LimitExcept> 261 </Directory> 262 ''} 263 264 ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") '' 265 RedirectPermanent / ${hostOpts.globalRedirect} 266 ''} 267 268 ${ 269 let makeDirConf = elem: '' 270 Alias ${elem.urlPath} ${elem.dir}/ 271 <Directory ${elem.dir}> 272 Options +Indexes 273 Require all granted 274 AllowOverride All 275 </Directory> 276 ''; 277 in concatMapStrings makeDirConf hostOpts.servedDirs 278 } 279 280 ${mkLocations hostOpts.locations} 281 ${hostOpts.extraConfig} 282 '' 283 ; 284 285 286 confFile = pkgs.writeText "httpd.conf" '' 287 288 ServerRoot ${pkg} 289 ServerName ${config.networking.hostName} 290 DefaultRuntimeDir ${runtimeDir}/runtime 291 292 PidFile ${runtimeDir}/httpd.pid 293 294 ${optionalString (cfg.mpm != "prefork") '' 295 # mod_cgid requires this. 296 ScriptSock ${runtimeDir}/cgisock 297 ''} 298 299 <IfModule prefork.c> 300 MaxClients ${toString cfg.maxClients} 301 MaxRequestsPerChild ${toString cfg.maxRequestsPerChild} 302 </IfModule> 303 304 ${let 305 toStr = listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}"; 306 uniqueListen = uniqList {inputList = map toStr listenInfo;}; 307 in concatStringsSep "\n" uniqueListen 308 } 309 310 User ${cfg.user} 311 Group ${cfg.group} 312 313 ${let 314 mkModule = module: 315 if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; } 316 else if isAttrs module then { inherit (module) name path; } 317 else throw "Expecting either a string or attribute set including a name and path."; 318 in 319 concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules)) 320 } 321 322 AddHandler type-map var 323 324 <Files ~ "^\.ht"> 325 Require all denied 326 </Files> 327 328 ${mimeConf} 329 ${loggingConf} 330 ${browserHacks} 331 332 Include ${pkg}/conf/extra/httpd-default.conf 333 Include ${pkg}/conf/extra/httpd-autoindex.conf 334 Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf 335 Include ${pkg}/conf/extra/httpd-languages.conf 336 337 TraceEnable off 338 339 ${sslConf} 340 341 ${optionalString cfg.package.luaSupport luaSetPaths} 342 343 # Fascist default - deny access to everything. 344 <Directory /> 345 Options FollowSymLinks 346 AllowOverride None 347 Require all denied 348 </Directory> 349 350 # But do allow access to files in the store so that we don't have 351 # to generate <Directory> clauses for every generated file that we 352 # want to serve. 353 <Directory /nix/store> 354 Require all granted 355 </Directory> 356 357 ${cfg.extraConfig} 358 359 ${concatMapStringsSep "\n" mkVHostConf vhosts} 360 ''; 361 362 # Generate the PHP configuration file. Should probably be factored 363 # out into a separate module. 364 phpIni = pkgs.runCommand "php.ini" 365 { options = cfg.phpOptions; 366 preferLocalBuild = true; 367 } 368 '' 369 cat ${php}/etc/php.ini > $out 370 cat ${php.phpIni} > $out 371 echo "$options" >> $out 372 ''; 373 374 mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; 375in 376 377 378{ 379 380 imports = [ 381 (mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.") 382 (mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.") 383 (mkRenamedOptionModule [ "services" "httpd" "multiProcessingModule" ] [ "services" "httpd" "mpm" ]) 384 385 # virtualHosts options 386 (mkRemovedOptionModule [ "services" "httpd" "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 387 (mkRemovedOptionModule [ "services" "httpd" "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 388 (mkRemovedOptionModule [ "services" "httpd" "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 389 (mkRemovedOptionModule [ "services" "httpd" "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 390 (mkRemovedOptionModule [ "services" "httpd" "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 391 (mkRemovedOptionModule [ "services" "httpd" "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 392 (mkRemovedOptionModule [ "services" "httpd" "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 393 (mkRemovedOptionModule [ "services" "httpd" "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 394 (mkRemovedOptionModule [ "services" "httpd" "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 395 (mkRemovedOptionModule [ "services" "httpd" "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 396 (mkRemovedOptionModule [ "services" "httpd" "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 397 (mkRemovedOptionModule [ "services" "httpd" "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 398 (mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.") 399 ]; 400 401 # interface 402 403 options = { 404 405 services.httpd = { 406 407 enable = mkEnableOption (lib.mdDoc "the Apache HTTP Server"); 408 409 package = mkOption { 410 type = types.package; 411 default = pkgs.apacheHttpd; 412 defaultText = literalExpression "pkgs.apacheHttpd"; 413 description = lib.mdDoc '' 414 Overridable attribute of the Apache HTTP Server package to use. 415 ''; 416 }; 417 418 configFile = mkOption { 419 type = types.path; 420 default = confFile; 421 defaultText = literalExpression "confFile"; 422 example = literalExpression ''pkgs.writeText "httpd.conf" "# my custom config file ..."''; 423 description = lib.mdDoc '' 424 Override the configuration file used by Apache. By default, 425 NixOS generates one automatically. 426 ''; 427 }; 428 429 extraConfig = mkOption { 430 type = types.lines; 431 default = ""; 432 description = lib.mdDoc '' 433 Configuration lines appended to the generated Apache 434 configuration file. Note that this mechanism will not work 435 when {option}`configFile` is overridden. 436 ''; 437 }; 438 439 extraModules = mkOption { 440 type = types.listOf types.unspecified; 441 default = []; 442 example = literalExpression '' 443 [ 444 "proxy_connect" 445 { name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.so"; } 446 ] 447 ''; 448 description = lib.mdDoc '' 449 Additional Apache modules to be used. These can be 450 specified as a string in the case of modules distributed 451 with Apache, or as an attribute set specifying the 452 {var}`name` and {var}`path` of the 453 module. 454 ''; 455 }; 456 457 adminAddr = mkOption { 458 type = types.nullOr types.str; 459 example = "admin@example.org"; 460 default = null; 461 description = lib.mdDoc "E-mail address of the server administrator."; 462 }; 463 464 logFormat = mkOption { 465 type = types.str; 466 default = "common"; 467 example = "combined"; 468 description = lib.mdDoc '' 469 Log format for log files. Possible values are: combined, common, referer, agent, none. 470 See <https://httpd.apache.org/docs/2.4/logs.html> for more details. 471 ''; 472 }; 473 474 logPerVirtualHost = mkOption { 475 type = types.bool; 476 default = true; 477 description = lib.mdDoc '' 478 If enabled, each virtual host gets its own 479 {file}`access.log` and 480 {file}`error.log`, namely suffixed by the 481 {option}`hostName` of the virtual host. 482 ''; 483 }; 484 485 user = mkOption { 486 type = types.str; 487 default = "wwwrun"; 488 description = lib.mdDoc '' 489 User account under which httpd children processes run. 490 491 If you require the main httpd process to run as 492 `root` add the following configuration: 493 ``` 494 systemd.services.httpd.serviceConfig.User = lib.mkForce "root"; 495 ``` 496 ''; 497 }; 498 499 group = mkOption { 500 type = types.str; 501 default = "wwwrun"; 502 description = lib.mdDoc '' 503 Group under which httpd children processes run. 504 ''; 505 }; 506 507 logDir = mkOption { 508 type = types.path; 509 default = "/var/log/httpd"; 510 description = lib.mdDoc '' 511 Directory for Apache's log files. It is created automatically. 512 ''; 513 }; 514 515 virtualHosts = mkOption { 516 type = with types; attrsOf (submodule (import ./vhost-options.nix)); 517 default = { 518 localhost = { 519 documentRoot = "${pkg}/htdocs"; 520 }; 521 }; 522 defaultText = literalExpression '' 523 { 524 localhost = { 525 documentRoot = "''${package.out}/htdocs"; 526 }; 527 } 528 ''; 529 example = literalExpression '' 530 { 531 "foo.example.com" = { 532 forceSSL = true; 533 documentRoot = "/var/www/foo.example.com" 534 }; 535 "bar.example.com" = { 536 addSSL = true; 537 documentRoot = "/var/www/bar.example.com"; 538 }; 539 } 540 ''; 541 description = lib.mdDoc '' 542 Specification of the virtual hosts served by Apache. Each 543 element should be an attribute set specifying the 544 configuration of the virtual host. 545 ''; 546 }; 547 548 enableMellon = mkOption { 549 type = types.bool; 550 default = false; 551 description = lib.mdDoc "Whether to enable the mod_auth_mellon module."; 552 }; 553 554 enablePHP = mkOption { 555 type = types.bool; 556 default = false; 557 description = lib.mdDoc "Whether to enable the PHP module."; 558 }; 559 560 phpPackage = mkOption { 561 type = types.package; 562 default = pkgs.php; 563 defaultText = literalExpression "pkgs.php"; 564 description = lib.mdDoc '' 565 Overridable attribute of the PHP package to use. 566 ''; 567 }; 568 569 enablePerl = mkOption { 570 type = types.bool; 571 default = false; 572 description = lib.mdDoc "Whether to enable the Perl module (mod_perl)."; 573 }; 574 575 phpOptions = mkOption { 576 type = types.lines; 577 default = ""; 578 example = 579 '' 580 date.timezone = "CET" 581 ''; 582 description = lib.mdDoc '' 583 Options appended to the PHP configuration file {file}`php.ini`. 584 ''; 585 }; 586 587 mpm = mkOption { 588 type = types.enum [ "event" "prefork" "worker" ]; 589 default = "event"; 590 example = "worker"; 591 description = 592 lib.mdDoc '' 593 Multi-processing module to be used by Apache. Available 594 modules are `prefork` (handles each 595 request in a separate child process), `worker` 596 (hybrid approach that starts a number of child processes 597 each running a number of threads) and `event` 598 (the default; a recent variant of `worker` 599 that handles persistent connections more efficiently). 600 ''; 601 }; 602 603 maxClients = mkOption { 604 type = types.int; 605 default = 150; 606 example = 8; 607 description = lib.mdDoc "Maximum number of httpd processes (prefork)"; 608 }; 609 610 maxRequestsPerChild = mkOption { 611 type = types.int; 612 default = 0; 613 example = 500; 614 description = lib.mdDoc '' 615 Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited. 616 ''; 617 }; 618 619 sslCiphers = mkOption { 620 type = types.str; 621 default = "HIGH:!aNULL:!MD5:!EXP"; 622 description = lib.mdDoc "Cipher Suite available for negotiation in SSL proxy handshake."; 623 }; 624 625 sslProtocols = mkOption { 626 type = types.str; 627 default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1"; 628 example = "All -SSLv2 -SSLv3"; 629 description = lib.mdDoc "Allowed SSL/TLS protocol versions."; 630 }; 631 }; 632 633 }; 634 635 # implementation 636 637 config = mkIf cfg.enable { 638 639 assertions = [ 640 { 641 assertion = all (hostOpts: !hostOpts.enableSSL) vhosts; 642 message = '' 643 The option `services.httpd.virtualHosts.<name>.enableSSL` no longer has any effect; please remove it. 644 Select one of `services.httpd.virtualHosts.<name>.addSSL`, `services.httpd.virtualHosts.<name>.forceSSL`, 645 or `services.httpd.virtualHosts.<name>.onlySSL`. 646 ''; 647 } 648 { 649 assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts; 650 message = '' 651 Options `services.httpd.virtualHosts.<name>.addSSL`, 652 `services.httpd.virtualHosts.<name>.onlySSL` and `services.httpd.virtualHosts.<name>.forceSSL` 653 are mutually exclusive. 654 ''; 655 } 656 { 657 assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts; 658 message = '' 659 Options `services.httpd.virtualHosts.<name>.enableACME` and 660 `services.httpd.virtualHosts.<name>.useACMEHost` are mutually exclusive. 661 ''; 662 } 663 { 664 assertion = cfg.enablePHP -> php.ztsSupport; 665 message = '' 666 The php package provided by `services.httpd.phpPackage` is not built with zts support. Please 667 ensure the php has zts support by settings `services.httpd.phpPackage = php.override { ztsSupport = true; }` 668 ''; 669 } 670 ] ++ map (name: mkCertOwnershipAssertion { 671 inherit (cfg) group user; 672 cert = config.security.acme.certs.${name}; 673 groups = config.users.groups; 674 }) dependentCertNames; 675 676 warnings = 677 mapAttrsToList (name: hostOpts: '' 678 Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS. 679 '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts); 680 681 users.users = optionalAttrs (cfg.user == "wwwrun") { 682 wwwrun = { 683 group = cfg.group; 684 description = "Apache httpd user"; 685 uid = config.ids.uids.wwwrun; 686 }; 687 }; 688 689 users.groups = optionalAttrs (cfg.group == "wwwrun") { 690 wwwrun.gid = config.ids.gids.wwwrun; 691 }; 692 693 security.acme.certs = let 694 acmePairs = map (hostOpts: let 695 hasRoot = hostOpts.acmeRoot != null; 696 in nameValuePair hostOpts.hostName { 697 group = mkDefault cfg.group; 698 # if acmeRoot is null inherit config.security.acme 699 # Since config.security.acme.certs.<cert>.webroot's own default value 700 # should take precedence set priority higher than mkOptionDefault 701 webroot = mkOverride (if hasRoot then 1000 else 2000) hostOpts.acmeRoot; 702 # Also nudge dnsProvider to null in case it is inherited 703 dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null; 704 extraDomainNames = hostOpts.serverAliases; 705 # Use the vhost-specific email address if provided, otherwise let 706 # security.acme.email or security.acme.certs.<cert>.email be used. 707 email = mkOverride 2000 (if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr); 708 # Filter for enableACME-only vhosts. Don't want to create dud certs 709 }) (filter (hostOpts: hostOpts.useACMEHost == null) acmeEnabledVhosts); 710 in listToAttrs acmePairs; 711 712 # httpd requires a stable path to the configuration file for reloads 713 environment.etc."httpd/httpd.conf".source = cfg.configFile; 714 environment.systemPackages = [ 715 apachectl 716 pkg 717 ]; 718 719 services.logrotate = optionalAttrs (cfg.logFormat != "none") { 720 enable = mkDefault true; 721 settings.httpd = { 722 files = "${cfg.logDir}/*.log"; 723 su = "${cfg.user} ${cfg.group}"; 724 frequency = "daily"; 725 rotate = 28; 726 sharedscripts = true; 727 compress = true; 728 delaycompress = true; 729 postrotate = "systemctl reload httpd.service > /dev/null 2>/dev/null || true"; 730 }; 731 }; 732 733 services.httpd.phpOptions = 734 '' 735 ; Don't advertise PHP 736 expose_php = off 737 '' + optionalString (config.time.timeZone != null) '' 738 739 ; Apparently PHP doesn't use $TZ. 740 date.timezone = "${config.time.timeZone}" 741 ''; 742 743 services.httpd.extraModules = mkBefore [ 744 # HTTP authentication mechanisms: basic and digest. 745 "auth_basic" "auth_digest" 746 747 # Authentication: is the user who he claims to be? 748 "authn_file" "authn_dbm" "authn_anon" 749 750 # Authorization: is the user allowed access? 751 "authz_user" "authz_groupfile" "authz_host" 752 753 # Other modules. 754 "ext_filter" "include" "env" "mime_magic" 755 "cern_meta" "expires" "headers" "usertrack" "setenvif" 756 "dav" "status" "asis" "info" "dav_fs" 757 "vhost_alias" "imagemap" "actions" "speling" 758 "proxy" "proxy_http" 759 "cache" "cache_disk" 760 761 # For compatibility with old configurations, the new module mod_access_compat is provided. 762 "access_compat" 763 ]; 764 765 systemd.tmpfiles.rules = 766 let 767 svc = config.systemd.services.httpd.serviceConfig; 768 in 769 [ 770 "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}" 771 "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}" 772 ]; 773 774 systemd.services.httpd = { 775 description = "Apache HTTPD"; 776 wantedBy = [ "multi-user.target" ]; 777 wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames); 778 after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames; 779 before = map (certName: "acme-${certName}.service") dependentCertNames; 780 restartTriggers = [ cfg.configFile ]; 781 782 path = [ pkg pkgs.coreutils pkgs.gnugrep ]; 783 784 environment = 785 optionalAttrs cfg.enablePHP { PHPRC = phpIni; } 786 // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; }; 787 788 preStart = 789 '' 790 # Get rid of old semaphores. These tend to accumulate across 791 # server restarts, eventually preventing it from restarting 792 # successfully. 793 for i in $(${pkgs.util-linux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do 794 ${pkgs.util-linux}/bin/ipcrm -s $i 795 done 796 ''; 797 798 serviceConfig = { 799 ExecStart = "@${pkg}/bin/httpd httpd -f /etc/httpd/httpd.conf"; 800 ExecStop = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful-stop"; 801 ExecReload = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful"; 802 User = cfg.user; 803 Group = cfg.group; 804 Type = "forking"; 805 PIDFile = "${runtimeDir}/httpd.pid"; 806 Restart = "always"; 807 RestartSec = "5s"; 808 RuntimeDirectory = "httpd httpd/runtime"; 809 RuntimeDirectoryMode = "0750"; 810 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 811 }; 812 }; 813 814 # postRun hooks on cert renew can't be used to restart Apache since renewal 815 # runs as the unprivileged acme user. sslTargets are added to wantedBy + before 816 # which allows the acme-finished-$cert.target to signify the successful updating 817 # of certs end-to-end. 818 systemd.services.httpd-config-reload = let 819 sslServices = map (certName: "acme-${certName}.service") dependentCertNames; 820 sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames; 821 in mkIf (sslServices != []) { 822 wantedBy = sslServices ++ [ "multi-user.target" ]; 823 # Before the finished targets, after the renew services. 824 # This service might be needed for HTTP-01 challenges, but we only want to confirm 825 # certs are updated _after_ config has been reloaded. 826 before = sslTargets; 827 after = sslServices; 828 restartTriggers = [ cfg.configFile ]; 829 # Block reloading if not all certs exist yet. 830 # Happens when config changes add new vhosts/certs. 831 unitConfig.ConditionPathExists = map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames; 832 serviceConfig = { 833 Type = "oneshot"; 834 TimeoutSec = 60; 835 ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active httpd.service"; 836 ExecStartPre = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -t"; 837 ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service"; 838 }; 839 }; 840 841 }; 842}