Christian Theune 2d0a489125 nixos/acme: improve scalability - reduce superfluous unit activations
The previous setup caused all renewal units to be triggered upon
ever so slight changes in config. In larger setups (100+ certificates)
adding a new certificate caused high system load and/or large memory
consumption issues. The memory issues are already a alleviated with
the locking mechanism. However, this then causes long delays upwards
of multiple minutes depending on individual runs and also caused
superfluous activations.

In this change we streamline the overall setup of units:

1. The unit that other services can depend upon is 'acme-{cert}.service'.
We call this the 'base unit'. As this one as `RemainAfterExit` set
the `acme-finished-{cert}` targets are not required any longer.

2. We now always generate initial self-signed certificates to simplify
the dependency structure. This deprecates the `preliminarySelfsigned`
option.

3. The `acme-order-renew-{cert}` service gets activated after the base
unit and services using certificates have started and performs all acme
interactions. When it finishes others services (like web servers) will
be notified through the `reloadServices` option or they can use
`wantedBy` and `after` dependencies if they implement their own reload
units.

The renewal timer also triggers this unit.

4. The timer unit is explicitly blocked from being started by s-t-c.

5. Permission management has been cleaned up a bit: there was an
   inconsistency between having the .lego files set to 600 vs 640
   on the exposed side. This is unified to 640 now.

6. Exempt the account target from being restarted by s-t-c. This will
   happen automatically if something relevant to the account changes.
2025-08-08 16:28:42 +02:00

991 lines
30 KiB
Nix

{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.httpd;
certs = config.security.acme.certs;
runtimeDir = "/run/httpd";
pkg = cfg.package.out;
apachectl = pkgs.runCommand "apachectl" { meta.priority = -1; } ''
mkdir -p $out/bin
cp ${pkg}/bin/apachectl $out/bin/apachectl
sed -i $out/bin/apachectl -e 's|$HTTPD -t|$HTTPD -t -f /etc/httpd/httpd.conf|'
'';
php = cfg.phpPackage.override {
apxs2Support = true;
apacheHttpd = pkg;
};
phpModuleName =
let
majorVersion = lib.versions.major (lib.getVersion php);
in
(if majorVersion == "8" then "php" else "php${majorVersion}");
mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
vhosts = attrValues cfg.virtualHosts;
# certName is used later on to determine systemd service names.
acmeEnabledVhosts = map (
hostOpts:
hostOpts
// {
certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName;
}
) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts);
vhostCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
mkListenInfo =
hostOpts:
if hostOpts.listen != [ ] then
hostOpts.listen
else
optionals (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) (
map (addr: {
ip = addr;
port = 443;
ssl = true;
}) hostOpts.listenAddresses
)
++ optionals (!hostOpts.onlySSL) (
map (addr: {
ip = addr;
port = 80;
ssl = false;
}) hostOpts.listenAddresses
);
listenInfo = unique (concatMap mkListenInfo vhosts);
enableHttp2 = any (vhost: vhost.http2) vhosts;
enableSSL = any (listen: listen.ssl) listenInfo;
enableUserDir = any (vhost: vhost.enableUserDir) vhosts;
# NOTE: generally speaking order of modules is very important
modules = [
# required apache modules our httpd service cannot run without
"authn_core"
"authz_core"
"log_config"
"mime"
"autoindex"
"negotiation"
"dir"
"alias"
"rewrite"
"unixd"
"slotmem_shm"
"socache_shmcb"
"mpm_${cfg.mpm}"
]
++ (if cfg.mpm == "prefork" then [ "cgi" ] else [ "cgid" ])
++ optional enableHttp2 "http2"
++ optional enableSSL "ssl"
++ optional enableUserDir "userdir"
++ optional cfg.enableMellon {
name = "auth_mellon";
path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so";
}
++ optional cfg.enablePHP {
name = phpModuleName;
path = "${php}/modules/lib${phpModuleName}.so";
}
++ optional cfg.enablePerl {
name = "perl";
path = "${mod_perl}/modules/mod_perl.so";
}
++ cfg.extraModules;
loggingConf = (
if cfg.logFormat != "none" then
''
ErrorLog ${cfg.logDir}/error.log
LogLevel notice
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
CustomLog ${cfg.logDir}/access.log ${cfg.logFormat}
''
else
''
ErrorLog /dev/null
''
);
browserHacks = ''
<IfModule mod_setenvif.c>
BrowserMatch "Mozilla/2" nokeepalive
BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
BrowserMatch "RealPlayer 4\.0" force-response-1.0
BrowserMatch "Java/1\.0" force-response-1.0
BrowserMatch "JDK/1\.0" force-response-1.0
BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
BrowserMatch "^WebDrive" redirect-carefully
BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully
BrowserMatch "^gnome-vfs" redirect-carefully
</IfModule>
'';
sslConf = ''
<IfModule mod_ssl.c>
SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)
Mutex posixsem
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLProtocol ${cfg.sslProtocols}
SSLCipherSuite ${cfg.sslCiphers}
SSLHonorCipherOrder on
</IfModule>
'';
mimeConf = ''
TypesConfig ${pkg}/conf/mime.types
AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl .crl
AddType application/x-httpd-php .php .phtml
<IfModule mod_mime_magic.c>
MIMEMagicFile ${pkg}/conf/magic
</IfModule>
'';
luaSetPaths =
let
# support both lua and lua.withPackages derivations
luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion;
in
''
<IfModule mod_lua.c>
LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so
LuaPackagePath ${cfg.package.lua5}/share/lua/${luaversion}/?.lua
</IfModule>
'';
mkVHostConf =
hostOpts:
let
adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts);
listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts);
useACME = hostOpts.enableACME || hostOpts.useACMEHost != null;
sslCertDir =
if hostOpts.enableACME then
certs.${hostOpts.hostName}.directory
else if hostOpts.useACMEHost != null then
certs.${hostOpts.useACMEHost}.directory
else
abort "This case should never happen.";
sslServerCert = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerCert;
sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain;
acmeChallenge = optionalString (useACME && hostOpts.acmeRoot != null) ''
Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
<Directory "${hostOpts.acmeRoot}">
AllowOverride None
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
Require method GET POST OPTIONS
Require all granted
</Directory>
'';
in
optionalString (listen != [ ]) ''
<VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}>
ServerName ${hostOpts.hostName}
${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"}
<IfModule mod_ssl.c>
SSLEngine off
</IfModule>
${acmeChallenge}
${
if hostOpts.forceSSL then
''
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC]
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
</IfModule>
''
else
mkVHostCommonConf hostOpts
}
</VirtualHost>
''
+ optionalString (listenSSL != [ ]) ''
<VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listenSSL}>
ServerName ${hostOpts.hostName}
${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"}
SSLEngine on
SSLCertificateFile ${sslServerCert}
SSLCertificateKeyFile ${sslServerKey}
${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"}
${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"}
${acmeChallenge}
${mkVHostCommonConf hostOpts}
</VirtualHost>
'';
mkVHostCommonConf =
hostOpts:
let
documentRoot = if hostOpts.documentRoot != null then hostOpts.documentRoot else pkgs.emptyDirectory;
mkLocations =
locations:
concatStringsSep "\n" (
map (config: ''
<Location ${config.location}>
${optionalString (config.proxyPass != null) ''
<IfModule mod_proxy.c>
ProxyPass ${config.proxyPass}
ProxyPassReverse ${config.proxyPass}
</IfModule>
''}
${optionalString (config.index != null) ''
<IfModule mod_dir.c>
DirectoryIndex ${config.index}
</IfModule>
''}
${optionalString (config.alias != null) ''
<IfModule mod_alias.c>
Alias "${config.alias}"
</IfModule>
''}
${config.extraConfig}
</Location>
'') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))
);
in
''
${optionalString cfg.logPerVirtualHost ''
ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log
CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat}
''}
${optionalString (hostOpts.robotsEntries != "") ''
Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries}
''}
DocumentRoot "${documentRoot}"
<Directory "${documentRoot}">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
${optionalString hostOpts.enableUserDir ''
UserDir public_html
UserDir disabled root
<Directory "/home/*/public_html">
AllowOverride FileInfo AuthConfig Limit Indexes
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
<Limit GET POST OPTIONS>
Require all granted
</Limit>
<LimitExcept GET POST OPTIONS>
Require all denied
</LimitExcept>
</Directory>
''}
${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") ''
RedirectPermanent / ${hostOpts.globalRedirect}
''}
${
let
makeDirConf = elem: ''
Alias ${elem.urlPath} ${elem.dir}/
<Directory ${elem.dir}>
Options +Indexes
Require all granted
AllowOverride All
</Directory>
'';
in
concatMapStrings makeDirConf hostOpts.servedDirs
}
${mkLocations hostOpts.locations}
${hostOpts.extraConfig}
'';
confFile = pkgs.writeText "httpd.conf" ''
ServerRoot ${pkg}
ServerName ${config.networking.hostName}
DefaultRuntimeDir ${runtimeDir}/runtime
PidFile ${runtimeDir}/httpd.pid
${optionalString (cfg.mpm != "prefork") ''
# mod_cgid requires this.
ScriptSock ${runtimeDir}/cgisock
''}
<IfModule prefork.c>
MaxClients ${toString cfg.maxClients}
MaxRequestsPerChild ${toString cfg.maxRequestsPerChild}
</IfModule>
${
let
toStr =
listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}";
uniqueListen = uniqList { inputList = map toStr listenInfo; };
in
concatStringsSep "\n" uniqueListen
}
User ${cfg.user}
Group ${cfg.group}
${
let
mkModule =
module:
if isString module then
{
name = module;
path = "${pkg}/modules/mod_${module}.so";
}
else if isAttrs module then
{ inherit (module) name path; }
else
throw "Expecting either a string or attribute set including a name and path.";
in
concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (
unique (map mkModule modules)
)
}
AddHandler type-map var
<Files ~ "^\.ht">
Require all denied
</Files>
${mimeConf}
${loggingConf}
${browserHacks}
Include ${pkg}/conf/extra/httpd-default.conf
Include ${pkg}/conf/extra/httpd-autoindex.conf
Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf
Include ${pkg}/conf/extra/httpd-languages.conf
TraceEnable off
${sslConf}
${optionalString cfg.package.luaSupport luaSetPaths}
# Fascist default - deny access to everything.
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
# But do allow access to files in the store so that we don't have
# to generate <Directory> clauses for every generated file that we
# want to serve.
<Directory /nix/store>
Require all granted
</Directory>
${cfg.extraConfig}
${concatMapStringsSep "\n" mkVHostConf vhosts}
'';
# Generate the PHP configuration file. Should probably be factored
# out into a separate module.
phpIni =
pkgs.runCommand "php.ini"
{
options = cfg.phpOptions;
preferLocalBuild = true;
}
''
cat ${php}/etc/php.ini > $out
cat ${php.phpIni} > $out
echo "$options" >> $out
'';
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
in
{
imports = [
(mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ]
"Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly."
)
(mkRemovedOptionModule [
"services"
"httpd"
"stateDir"
] "The httpd module now uses /run/httpd as a runtime directory.")
(mkRenamedOptionModule [ "services" "httpd" "multiProcessingModule" ] [ "services" "httpd" "mpm" ])
# virtualHosts options
(mkRemovedOptionModule [
"services"
"httpd"
"documentRoot"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"enableSSL"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"enableUserDir"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"globalRedirect"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"hostName"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"listen"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"robotsEntries"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"servedDirs"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"servedFiles"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"serverAliases"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"sslServerCert"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"sslServerChain"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [
"services"
"httpd"
"sslServerKey"
] "Please define a virtual host using `services.httpd.virtualHosts`.")
];
# interface
options = {
services.httpd = {
enable = mkEnableOption "the Apache HTTP Server";
package = mkPackageOption pkgs "apacheHttpd" { };
configFile = mkOption {
type = types.path;
default = confFile;
defaultText = literalExpression "confFile";
example = literalExpression ''pkgs.writeText "httpd.conf" "# my custom config file ..."'';
description = ''
Override the configuration file used by Apache. By default,
NixOS generates one automatically.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Configuration lines appended to the generated Apache
configuration file. Note that this mechanism will not work
when {option}`configFile` is overridden.
'';
};
extraModules = mkOption {
type = types.listOf types.unspecified;
default = [ ];
example = literalExpression ''
[
"proxy_connect"
{ name = "jk"; path = "''${pkgs.apacheHttpdPackages.mod_jk}/modules/mod_jk.so"; }
]
'';
description = ''
Additional Apache modules to be used. These can be
specified as a string in the case of modules distributed
with Apache, or as an attribute set specifying the
{var}`name` and {var}`path` of the
module.
'';
};
adminAddr = mkOption {
type = types.nullOr types.str;
example = "admin@example.org";
default = null;
description = "E-mail address of the server administrator.";
};
logFormat = mkOption {
type = types.str;
default = "common";
example = "combined";
description = ''
Log format for log files. Possible values are: combined, common, referer, agent, none.
See <https://httpd.apache.org/docs/2.4/logs.html> for more details.
'';
};
logPerVirtualHost = mkOption {
type = types.bool;
default = true;
description = ''
If enabled, each virtual host gets its own
{file}`access.log` and
{file}`error.log`, namely suffixed by the
{option}`hostName` of the virtual host.
'';
};
user = mkOption {
type = types.str;
default = "wwwrun";
description = ''
User account under which httpd children processes run.
If you require the main httpd process to run as
`root` add the following configuration:
```
systemd.services.httpd.serviceConfig.User = lib.mkForce "root";
```
'';
};
group = mkOption {
type = types.str;
default = "wwwrun";
description = ''
Group under which httpd children processes run.
'';
};
logDir = mkOption {
type = types.path;
default = "/var/log/httpd";
description = ''
Directory for Apache's log files. It is created automatically.
'';
};
virtualHosts = mkOption {
type = with types; attrsOf (submodule (import ./vhost-options.nix));
default = {
localhost = {
documentRoot = "${pkg}/htdocs";
};
};
defaultText = literalExpression ''
{
localhost = {
documentRoot = "''${package.out}/htdocs";
};
}
'';
example = literalExpression ''
{
"foo.example.com" = {
forceSSL = true;
documentRoot = "/var/www/foo.example.com"
};
"bar.example.com" = {
addSSL = true;
documentRoot = "/var/www/bar.example.com";
};
}
'';
description = ''
Specification of the virtual hosts served by Apache. Each
element should be an attribute set specifying the
configuration of the virtual host.
'';
};
enableMellon = mkEnableOption "the mod_auth_mellon module";
enablePHP = mkEnableOption "the PHP module";
phpPackage = mkPackageOption pkgs "php" { };
enablePerl = mkEnableOption "the Perl module (mod_perl)";
phpOptions = mkOption {
type = types.lines;
default = "";
example = ''
date.timezone = "CET"
'';
description = ''
Options appended to the PHP configuration file {file}`php.ini`.
'';
};
mpm = mkOption {
type = types.enum [
"event"
"prefork"
"worker"
];
default = "event";
example = "worker";
description = ''
Multi-processing module to be used by Apache. Available
modules are `prefork` (handles each
request in a separate child process), `worker`
(hybrid approach that starts a number of child processes
each running a number of threads) and `event`
(the default; a recent variant of `worker`
that handles persistent connections more efficiently).
'';
};
maxClients = mkOption {
type = types.int;
default = 150;
example = 8;
description = "Maximum number of httpd processes (prefork)";
};
maxRequestsPerChild = mkOption {
type = types.int;
default = 0;
example = 500;
description = ''
Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited.
'';
};
sslCiphers = mkOption {
type = types.str;
default = "HIGH:!aNULL:!MD5:!EXP";
description = "Cipher Suite available for negotiation in SSL proxy handshake.";
};
sslProtocols = mkOption {
type = types.str;
default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1";
example = "All -SSLv2 -SSLv3";
description = "Allowed SSL/TLS protocol versions.";
};
};
};
# implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = all (hostOpts: !hostOpts.enableSSL) vhosts;
message = ''
The option `services.httpd.virtualHosts.<name>.enableSSL` no longer has any effect; please remove it.
Select one of `services.httpd.virtualHosts.<name>.addSSL`, `services.httpd.virtualHosts.<name>.forceSSL`,
or `services.httpd.virtualHosts.<name>.onlySSL`.
'';
}
{
assertion = all (
hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)
) vhosts;
message = ''
Options `services.httpd.virtualHosts.<name>.addSSL`,
`services.httpd.virtualHosts.<name>.onlySSL` and `services.httpd.virtualHosts.<name>.forceSSL`
are mutually exclusive.
'';
}
{
assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts;
message = ''
Options `services.httpd.virtualHosts.<name>.enableACME` and
`services.httpd.virtualHosts.<name>.useACMEHost` are mutually exclusive.
'';
}
{
assertion = cfg.enablePHP -> php.ztsSupport;
message = ''
The php package provided by `services.httpd.phpPackage` is not built with zts support. Please
ensure the php has zts support by settings `services.httpd.phpPackage = php.override { ztsSupport = true; }`
'';
}
]
++ map (
name:
mkCertOwnershipAssertion {
cert = config.security.acme.certs.${name};
groups = config.users.groups;
services = [
config.systemd.services.httpd
]
++ lib.optional (vhostCertNames != [ ]) config.systemd.services.httpd-config-reload;
}
) vhostCertNames;
warnings = mapAttrsToList (name: hostOpts: ''
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.
'') (filterAttrs (name: hostOpts: hostOpts.servedFiles != [ ]) cfg.virtualHosts);
users.users = optionalAttrs (cfg.user == "wwwrun") {
wwwrun = {
group = cfg.group;
description = "Apache httpd user";
uid = config.ids.uids.wwwrun;
};
};
users.groups = optionalAttrs (cfg.group == "wwwrun") {
wwwrun.gid = config.ids.gids.wwwrun;
};
security.acme.certs =
let
acmePairs = map (
hostOpts:
let
hasRoot = hostOpts.acmeRoot != null;
in
nameValuePair hostOpts.hostName {
group = mkDefault cfg.group;
# if acmeRoot is null inherit config.security.acme
# Since config.security.acme.certs.<cert>.webroot's own default value
# should take precedence set priority higher than mkOptionDefault
webroot = mkOverride (if hasRoot then 1000 else 2000) hostOpts.acmeRoot;
# Also nudge dnsProvider to null in case it is inherited
dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
extraDomainNames = hostOpts.serverAliases;
# Use the vhost-specific email address if provided, otherwise let
# security.acme.email or security.acme.certs.<cert>.email be used.
email = mkOverride 2000 (if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr);
# Filter for enableACME-only vhosts. Don't want to create dud certs
}
) (filter (hostOpts: hostOpts.useACMEHost == null) acmeEnabledVhosts);
in
listToAttrs acmePairs;
# httpd requires a stable path to the configuration file for reloads
environment.etc."httpd/httpd.conf".source = cfg.configFile;
environment.systemPackages = [
apachectl
pkg
];
services.logrotate = optionalAttrs (cfg.logFormat != "none") {
enable = mkDefault true;
settings.httpd = {
files = "${cfg.logDir}/*.log";
su = "${cfg.user} ${cfg.group}";
frequency = "daily";
rotate = 28;
sharedscripts = true;
compress = true;
delaycompress = true;
postrotate = "systemctl reload httpd.service > /dev/null 2>/dev/null || true";
};
};
services.httpd.phpOptions = ''
; Don't advertise PHP
expose_php = off
''
+ optionalString (config.time.timeZone != null) ''
; Apparently PHP doesn't use $TZ.
date.timezone = "${config.time.timeZone}"
'';
services.httpd.extraModules = mkBefore [
# HTTP authentication mechanisms: basic and digest.
"auth_basic"
"auth_digest"
# Authentication: is the user who he claims to be?
"authn_file"
"authn_dbm"
"authn_anon"
# Authorization: is the user allowed access?
"authz_user"
"authz_groupfile"
"authz_host"
# Other modules.
"ext_filter"
"include"
"env"
"mime_magic"
"cern_meta"
"expires"
"headers"
"usertrack"
"setenvif"
"dav"
"status"
"asis"
"info"
"dav_fs"
"vhost_alias"
"imagemap"
"actions"
"speling"
"proxy"
"proxy_http"
"cache"
"cache_disk"
# For compatibility with old configurations, the new module mod_access_compat is provided.
"access_compat"
];
systemd.tmpfiles.rules =
let
svc = config.systemd.services.httpd.serviceConfig;
in
[
"d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}"
"Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
];
systemd.services.httpd = {
description = "Apache HTTPD";
wantedBy = [ "multi-user.target" ];
wants = concatLists (map (certName: [ "acme-${certName}.service" ]) vhostCertNames);
after = [
"network.target"
]
# Ensure httpd runs with baseline certificates in place.
++ map (certName: "acme-${certName}.service") vhostCertNames;
# Ensure httpd runs (with current config) before the actual ACME jobs run
before = map (certName: "acme-order-renew-${certName}.service") vhostCertNames;
restartTriggers = [ cfg.configFile ];
path = [
pkg
pkgs.coreutils
pkgs.gnugrep
];
environment =
optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
// optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; };
preStart = ''
# Get rid of old semaphores. These tend to accumulate across
# server restarts, eventually preventing it from restarting
# successfully.
for i in $(${pkgs.util-linux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do
${pkgs.util-linux}/bin/ipcrm -s $i
done
'';
serviceConfig = {
ExecStart = "@${pkg}/bin/httpd httpd -f /etc/httpd/httpd.conf";
ExecStop = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful-stop";
ExecReload = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -k graceful";
User = cfg.user;
Group = cfg.group;
Type = "forking";
PIDFile = "${runtimeDir}/httpd.pid";
Restart = "always";
RestartSec = "5s";
RuntimeDirectory = "httpd httpd/runtime";
RuntimeDirectoryMode = "0750";
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
};
};
# postRun hooks on cert renew can't be used to restart Apache since renewal
# runs as the unprivileged acme user. sslTargets are added to wantedBy + before
# which allows the acme-order-renew-$cert.service to signify the successful updating
# of certs end-to-end.
systemd.services.httpd-config-reload =
let
sslServices = map (certName: "acme-order-renew-${certName}.service") vhostCertNames;
in
mkIf (vhostCertNames != [ ]) {
wantedBy = sslServices ++ [ "multi-user.target" ];
# Before the finished targets, after the renew services.
# This service might be needed for HTTP-01 challenges, but we only want to confirm
# certs are updated _after_ config has been reloaded.
after = sslServices;
restartTriggers = [ cfg.configFile ];
# Block reloading if not all certs exist yet.
# Happens when config changes add new vhosts/certs.
unitConfig.ConditionPathExists = map (
certName: certs.${certName}.directory + "/fullchain.pem"
) vhostCertNames;
serviceConfig = {
Type = "oneshot";
TimeoutSec = 60;
ExecCondition = "/run/current-system/systemd/bin/systemctl -q is-active httpd.service";
ExecStartPre = "${pkg}/bin/httpd -f /etc/httpd/httpd.conf -t";
ExecStart = "/run/current-system/systemd/bin/systemctl reload httpd.service";
};
};
};
}