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