{apache,caddy,nginx}: not "before" ACME certs using DNS validation (#336412)

This commit is contained in:
K900 2024-11-08 18:50:28 +03:00 committed by GitHub
commit 0453fe2395
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 40 deletions

View File

@ -217,7 +217,7 @@ let
protocolOpts = if useDns then (
[ "--dns" data.dnsProvider ]
++ lib.optionals (!data.dnsPropagationCheck) [ "--dns.disable-cp" ]
++ lib.optionals (!data.dnsPropagationCheck) [ "--dns.propagation-disable-ans" ]
++ lib.optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ]
) else if data.s3Bucket != null then [ "--http" "--http.s3-bucket" data.s3Bucket ]
else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ]

View File

@ -1,4 +1,21 @@
{ cert, group, groups, user }: {
assertion = cert.group == group || builtins.any (u: u == user) groups.${cert.group}.members;
message = "Group for certificate ${cert.domain} must be ${group}, or user ${user} must be a member of group ${cert.group}";
lib:
{ cert, groups, services }:
let
catSep = builtins.concatStringsSep;
svcGroups = svc:
(lib.optional (svc.serviceConfig ? Group) svc.serviceConfig.Group)
++ (svc.serviceConfig.SupplementaryGroups or [ ]);
in
{
assertion = builtins.all (svc:
svc.serviceConfig.User or "root" == "root"
|| builtins.elem svc.serviceConfig.User groups.${cert.group}.members
|| builtins.elem cert.group (svcGroups svc)
) services;
message = "Certificate ${cert.domain} (group=${cert.group}) must be readable by service(s) ${
catSep ", " (map (svc: "${svc.name} (user=${svc.serviceConfig.User} groups=${catSep " " (svcGroups svc)})") services)
}";
}

View File

@ -33,7 +33,9 @@ let
certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName;
}) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts);
dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
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
@ -371,7 +373,7 @@ let
echo "$options" >> $out
'';
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
in
@ -641,10 +643,10 @@ in
'';
}
] ++ map (name: mkCertOwnershipAssertion {
inherit (cfg) group user;
cert = config.security.acme.certs.${name};
groups = config.users.groups;
}) dependentCertNames;
services = [ config.systemd.services.httpd ] ++ lib.optional (vhostCertNames != []) config.systemd.services.httpd-config-reload;
}) vhostCertNames;
warnings =
mapAttrsToList (name: hostOpts: ''
@ -747,8 +749,10 @@ in
systemd.services.httpd = {
description = "Apache HTTPD";
wantedBy = [ "multi-user.target" ];
wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
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 ];
@ -789,9 +793,9 @@ in
# 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") dependentCertNames;
sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
in mkIf (sslServices != []) {
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
@ -801,7 +805,7 @@ in
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") dependentCertNames;
unitConfig.ConditionPathExists = map (certName: certs.${certName}.directory + "/fullchain.pem") vhostCertNames;
serviceConfig = {
Type = "oneshot";
TimeoutSec = 60;

View File

@ -5,8 +5,12 @@ with lib;
let
cfg = config.services.caddy;
certs = config.security.acme.certs;
virtualHosts = attrValues cfg.virtualHosts;
acmeVHosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
acmeEnabledVhosts = filter (hostOpts: hostOpts.useACMEHost != null) virtualHosts;
vhostCertNames = unique (map (hostOpts: hostOpts.useACMEHost) 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
mkVHostConf = hostOpts:
let
@ -51,9 +55,7 @@ let
configPath = "/etc/${etcConfigFile}";
acmeHosts = unique (catAttrs "useACMEHost" acmeVHosts);
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
in
{
imports = [
@ -329,10 +331,10 @@ in
message = "To specify an adapter other than 'caddyfile' please provide your own configuration via `services.caddy.configFile`";
}
] ++ map (name: mkCertOwnershipAssertion {
inherit (cfg) group user;
cert = config.security.acme.certs.${name};
groups = config.users.groups;
}) acmeHosts;
services = [ config.systemd.services.caddy ];
}) vhostCertNames;
services.caddy.globalConfig = ''
${optionalString (cfg.email != null) "email ${cfg.email}"}
@ -348,9 +350,10 @@ in
systemd.packages = [ cfg.package ];
systemd.services.caddy = {
wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts;
after = map (hostOpts: "acme-selfsigned-${hostOpts.useACMEHost}.service") acmeVHosts;
before = map (hostOpts: "acme-${hostOpts.useACMEHost}.service") acmeVHosts;
wants = map (certName: "acme-finished-${certName}.target") vhostCertNames;
after = 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;
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 14400;
@ -397,10 +400,10 @@ in
security.acme.certs =
let
certCfg = map (useACMEHost: nameValuePair useACMEHost {
certCfg = map (certName: nameValuePair certName {
group = mkDefault cfg.group;
reloadServices = [ "caddy.service" ];
}) acmeHosts;
}) vhostCertNames;
in
listToAttrs certCfg;

View File

@ -7,7 +7,9 @@ let
inherit (config.security.acme) certs;
vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs;
dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
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
virtualHosts = mapAttrs (vhostName: vhostConfig:
let
serverName = if vhostConfig.serverName != null
@ -471,7 +473,7 @@ let
'') authDef)
);
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
oldHTTP2 = (versionOlder cfg.package.version "1.25.1" && !(cfg.package.pname == "angie" || cfg.package.pname == "angieQuic"));
in
@ -1209,10 +1211,10 @@ in
'';
}
] ++ map (name: mkCertOwnershipAssertion {
inherit (cfg) group user;
cert = config.security.acme.certs.${name};
groups = config.users.groups;
}) dependentCertNames;
services = [ config.systemd.services.nginx ] ++ lib.optional (cfg.enableReload || vhostCertNames != []) config.systemd.services.nginx-config-reload;
}) vhostCertNames;
services.nginx.additionalModules = optional cfg.recommendedBrotliSettings pkgs.nginxModules.brotli
++ lib.optional cfg.recommendedZstdSettings pkgs.nginxModules.zstd;
@ -1236,8 +1238,10 @@ in
systemd.services.nginx = {
description = "Nginx Web Server";
wantedBy = [ "multi-user.target" ];
wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames);
after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames;
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
# Nginx needs to be started in order to be able to request certificates
# (it's hosting the acme challenge after all)
# This fixes https://github.com/NixOS/nixpkgs/issues/81842
@ -1316,9 +1320,9 @@ in
# which allows the acme-finished-$cert.target to signify the successful updating
# of certs end-to-end.
systemd.services.nginx-config-reload = let
sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
in mkIf (cfg.enableReload || sslServices != []) {
sslServices = map (certName: "acme-${certName}.service") vhostCertNames;
sslTargets = map (certName: "acme-finished-${certName}.target") vhostCertNames;
in mkIf (cfg.enableReload || vhostCertNames != []) {
wants = optionals cfg.enableReload [ "nginx.service" ];
wantedBy = sslServices ++ [ "multi-user.target" ];
# Before the finished targets, after the renew services.
@ -1329,7 +1333,7 @@ in
restartTriggers = optionals cfg.enableReload [ configFile ];
# Block reloading if not all certs exist yet.
# Happens when config changes add new vhosts/certs.
unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") vhostCertNames);
serviceConfig = {
Type = "oneshot";
TimeoutSec = 60;

View File

@ -54,11 +54,6 @@ let
testCerts = import ./snakeoil-certs.nix;
domain = testCerts.domain;
resolver = let
message = "You need to define a resolver for the acme test module.";
firstNS = lib.head config.networking.nameservers;
in if config.networking.nameservers == [] then throw message else firstNS;
pebbleConf.pebble = {
listenAddress = "0.0.0.0:443";
managementListenAddress = "0.0.0.0:15000";