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