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