{ 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); dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server 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 = '' 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 ''; sslConf = '' SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000) Mutex posixsem SSLRandomSeed startup builtin SSLRandomSeed connect builtin SSLProtocol ${cfg.sslProtocols} SSLCipherSuite ${cfg.sslCiphers} SSLHonorCipherOrder on ''; 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 MIMEMagicFile ${pkg}/conf/magic ''; luaSetPaths = let # support both lua and lua.withPackages derivations luaversion = cfg.package.lua5.lua.luaversion or cfg.package.lua5.luaversion; in '' LuaPackageCPath ${cfg.package.lua5}/lib/lua/${luaversion}/?.so LuaPackagePath ${cfg.package.lua5}/share/lua/${luaversion}/?.lua ''; 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/" AllowOverride None Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec Require method GET POST OPTIONS Require all granted ''; in optionalString (listen != []) '' ServerName ${hostOpts.hostName} ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} ${optionalString (adminAddr != null) "ServerAdmin ${adminAddr}"} SSLEngine off ${acmeChallenge} ${if hostOpts.forceSSL then '' RewriteEngine on RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC] RewriteCond %{HTTPS} off RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} '' else mkVHostCommonConf hostOpts} '' + optionalString (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} '' ; mkVHostCommonConf = hostOpts: let documentRoot = if hostOpts.documentRoot != null then hostOpts.documentRoot else pkgs.emptyDirectory ; mkLocations = locations: concatStringsSep "\n" (map (config: '' ${optionalString (config.proxyPass != null) '' ProxyPass ${config.proxyPass} ProxyPassReverse ${config.proxyPass} ''} ${optionalString (config.index != null) '' DirectoryIndex ${config.index} ''} ${optionalString (config.alias != null) '' Alias "${config.alias}" ''} ${config.extraConfig} '') (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}" Options Indexes FollowSymLinks AllowOverride None Require all granted ${optionalString hostOpts.enableUserDir '' UserDir public_html UserDir disabled root AllowOverride FileInfo AuthConfig Limit Indexes Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec Require all granted Require all denied ''} ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") '' RedirectPermanent / ${hostOpts.globalRedirect} ''} ${ let makeDirConf = elem: '' Alias ${elem.urlPath} ${elem.dir}/ Options +Indexes Require all granted AllowOverride All ''; 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 ''} MaxClients ${toString cfg.maxClients} MaxRequestsPerChild ${toString cfg.maxRequestsPerChild} ${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 Require all denied ${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. Options FollowSymLinks AllowOverride None Require all denied # But do allow access to files in the store so that we don't have # to generate clauses for every generated file that we # want to serve. Require all granted ${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 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..enableSSL` no longer has any effect; please remove it. Select one of `services.httpd.virtualHosts..addSSL`, `services.httpd.virtualHosts..forceSSL`, or `services.httpd.virtualHosts..onlySSL`. ''; } { assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts; message = '' Options `services.httpd.virtualHosts..addSSL`, `services.httpd.virtualHosts..onlySSL` and `services.httpd.virtualHosts..forceSSL` are mutually exclusive. ''; } { assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts; message = '' Options `services.httpd.virtualHosts..enableACME` and `services.httpd.virtualHosts..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..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..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-finished-${certName}.target" ]) vhostCertNames); after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") vhostCertNames ++ map (certName: "acme-${certName}.service") independentCertNames; # avoid loading self-signed key w/ real cert, or vice-versa before = map (certName: "acme-${certName}.service") dependentCertNames; 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-finished-$cert.target to signify the successful updating # of certs end-to-end. systemd.services.httpd-config-reload = let sslServices = map (certName: "acme-${certName}.service") vhostCertNames; sslTargets = map (certName: "acme-finished-${certName}.target") 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. before = sslTargets; 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"; }; }; }; }