nixos/preload: init
[NixPkgs.git] / nixos / modules / services / web-servers / apache-httpd / default.nix
blob588f5ee4d003afdc364dcc10658b41ac01579215
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
7   cfg = config.services.httpd;
9   certs = config.security.acme.certs;
11   runtimeDir = "/run/httpd";
13   pkg = cfg.package.out;
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   '';
21   php = cfg.phpPackage.override { apxs2Support = true; apacheHttpd = pkg; };
23   phpModuleName = let
24     majorVersion = lib.versions.major (lib.getVersion php);
25   in (if majorVersion == "8" then "php" else "php${majorVersion}");
27   mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
29   vhosts = attrValues cfg.virtualHosts;
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);
36   dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
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     ;
46   listenInfo = unique (concatMap mkListenInfo vhosts);
48   enableHttp2 = any (vhost: vhost.http2) vhosts;
49   enableSSL = any (listen: listen.ssl) listenInfo;
50   enableUserDir = any (vhost: vhost.enableUserDir) vhosts;
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;
71   loggingConf = (if cfg.logFormat != "none" then ''
72     ErrorLog ${cfg.logDir}/error.log
74     LogLevel notice
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
81     CustomLog ${cfg.logDir}/access.log ${cfg.logFormat}
82   '' else ''
83     ErrorLog /dev/null
84   '');
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   '';
102   sslConf = ''
103     <IfModule mod_ssl.c>
104         SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
106         Mutex posixsem
108         SSLRandomSeed startup builtin
109         SSLRandomSeed connect builtin
111         SSLProtocol ${cfg.sslProtocols}
112         SSLCipherSuite ${cfg.sslCiphers}
113         SSLHonorCipherOrder on
114     </IfModule>
115   '';
118   mimeConf = ''
119     TypesConfig ${pkg}/conf/mime.types
121     AddType application/x-x509-ca-cert .crt
122     AddType application/x-pkcs7-crl    .crl
123     AddType application/x-httpd-php    .php .phtml
125     <IfModule mod_mime_magic.c>
126         MIMEMagicFile ${pkg}/conf/magic
127     </IfModule>
128   '';
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   '';
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);
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.";
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;
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   ;
202   mkVHostCommonConf = hostOpts:
203     let
204       documentRoot = if hostOpts.documentRoot != null
205         then hostOpts.documentRoot
206         else pkgs.emptyDirectory
207       ;
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         ''}
237         ${optionalString (hostOpts.robotsEntries != "") ''
238           Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries}
239         ''}
241         DocumentRoot "${documentRoot}"
243         <Directory "${documentRoot}">
244             Options Indexes FollowSymLinks
245             AllowOverride None
246             Require all granted
247         </Directory>
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         ''}
264         ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") ''
265           RedirectPermanent / ${hostOpts.globalRedirect}
266         ''}
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         }
280         ${mkLocations hostOpts.locations}
281         ${hostOpts.extraConfig}
282       ''
283   ;
286   confFile = pkgs.writeText "httpd.conf" ''
288     ServerRoot ${pkg}
289     ServerName ${config.networking.hostName}
290     DefaultRuntimeDir ${runtimeDir}/runtime
292     PidFile ${runtimeDir}/httpd.pid
294     ${optionalString (cfg.mpm != "prefork") ''
295       # mod_cgid requires this.
296       ScriptSock ${runtimeDir}/cgisock
297     ''}
299     <IfModule prefork.c>
300         MaxClients           ${toString cfg.maxClients}
301         MaxRequestsPerChild  ${toString cfg.maxRequestsPerChild}
302     </IfModule>
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     }
310     User ${cfg.user}
311     Group ${cfg.group}
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     }
322     AddHandler type-map var
324     <Files ~ "^\.ht">
325         Require all denied
326     </Files>
328     ${mimeConf}
329     ${loggingConf}
330     ${browserHacks}
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
337     TraceEnable off
339     ${sslConf}
341     ${optionalString cfg.package.luaSupport luaSetPaths}
343     # Fascist default - deny access to everything.
344     <Directory />
345         Options FollowSymLinks
346         AllowOverride None
347         Require all denied
348     </Directory>
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>
357     ${cfg.extraConfig}
359     ${concatMapStringsSep "\n" mkVHostConf vhosts}
360   '';
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     '';
374   mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
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" ])
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   ];
401   # interface
403   options = {
405     services.httpd = {
407       enable = mkEnableOption (lib.mdDoc "the Apache HTTP Server");
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       };
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       };
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       };
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       };
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       };
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       };
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       };
485       user = mkOption {
486         type = types.str;
487         default = "wwwrun";
488         description = lib.mdDoc ''
489           User account under which httpd children processes run.
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       };
499       group = mkOption {
500         type = types.str;
501         default = "wwwrun";
502         description = lib.mdDoc ''
503           Group under which httpd children processes run.
504         '';
505       };
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       };
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       };
548       enableMellon = mkOption {
549         type = types.bool;
550         default = false;
551         description = lib.mdDoc "Whether to enable the mod_auth_mellon module.";
552       };
554       enablePHP = mkOption {
555         type = types.bool;
556         default = false;
557         description = lib.mdDoc "Whether to enable the PHP module.";
558       };
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       };
569       enablePerl = mkOption {
570         type = types.bool;
571         default = false;
572         description = lib.mdDoc "Whether to enable the Perl module (mod_perl).";
573       };
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       };
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       };
603       maxClients = mkOption {
604         type = types.int;
605         default = 150;
606         example = 8;
607         description = lib.mdDoc "Maximum number of httpd processes (prefork)";
608       };
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       };
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       };
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     };
633   };
635   # implementation
637   config = mkIf cfg.enable {
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;
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);
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     };
689     users.groups = optionalAttrs (cfg.group == "wwwrun") {
690       wwwrun.gid = config.ids.gids.wwwrun;
691     };
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;
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     ];
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     };
733     services.httpd.phpOptions =
734       ''
735         ; Don't advertise PHP
736         expose_php = off
737       '' + optionalString (config.time.timeZone != null) ''
739         ; Apparently PHP doesn't use $TZ.
740         date.timezone = "${config.time.timeZone}"
741       '';
743     services.httpd.extraModules = mkBefore [
744       # HTTP authentication mechanisms: basic and digest.
745       "auth_basic" "auth_digest"
747       # Authentication: is the user who he claims to be?
748       "authn_file" "authn_dbm" "authn_anon"
750       # Authorization: is the user allowed access?
751       "authz_user" "authz_groupfile" "authz_host"
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"
761       # For compatibility with old configurations, the new module mod_access_compat is provided.
762       "access_compat"
763     ];
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         ];
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 ];
782         path = [ pkg pkgs.coreutils pkgs.gnugrep ];
784         environment =
785           optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
786           // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };
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           '';
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       };
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     };
841   };