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}