0
0
Fork 0
mirror of https://github.com/NixOS/nixpkgs.git synced 2025-07-14 06:00:33 +03:00

{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 ( protocolOpts = if useDns then (
[ "--dns" data.dnsProvider ] [ "--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 ] ++ lib.optionals (data.dnsResolver != null) [ "--dns.resolvers" data.dnsResolver ]
) else if data.s3Bucket != null then [ "--http" "--http.s3-bucket" data.s3Bucket ] ) else if data.s3Bucket != null then [ "--http" "--http.s3-bucket" data.s3Bucket ]
else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ] else if data.listenHTTP != null then [ "--http" "--http.port" data.listenHTTP ]

View file

@ -1,4 +1,21 @@
{ cert, group, groups, user }: { lib:
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}"; { 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; certName = if hostOpts.useACMEHost != null then hostOpts.useACMEHost else hostOpts.hostName;
}) (filter (hostOpts: hostOpts.enableACME || hostOpts.useACMEHost != null) vhosts); }) (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: mkListenInfo = hostOpts:
if hostOpts.listen != [] then if hostOpts.listen != [] then
@ -371,7 +373,7 @@ let
echo "$options" >> $out echo "$options" >> $out
''; '';
mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix; mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix lib;
in in
@ -641,10 +643,10 @@ in
''; '';
} }
] ++ map (name: mkCertOwnershipAssertion { ] ++ map (name: mkCertOwnershipAssertion {
inherit (cfg) group user;
cert = config.security.acme.certs.${name}; cert = config.security.acme.certs.${name};
groups = config.users.groups; groups = config.users.groups;
}) dependentCertNames; services = [ config.systemd.services.httpd ] ++ lib.optional (vhostCertNames != []) config.systemd.services.httpd-config-reload;
}) vhostCertNames;
warnings = warnings =
mapAttrsToList (name: hostOpts: '' mapAttrsToList (name: hostOpts: ''
@ -747,8 +749,10 @@ in
systemd.services.httpd = { systemd.services.httpd = {
description = "Apache HTTPD"; description = "Apache HTTPD";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) dependentCertNames); wants = concatLists (map (certName: [ "acme-finished-${certName}.target" ]) vhostCertNames);
after = [ "network.target" ] ++ map (certName: "acme-selfsigned-${certName}.service") dependentCertNames; 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; before = map (certName: "acme-${certName}.service") dependentCertNames;
restartTriggers = [ cfg.configFile ]; restartTriggers = [ cfg.configFile ];
@ -789,9 +793,9 @@ in
# which allows the acme-finished-$cert.target to signify the successful updating # which allows the acme-finished-$cert.target to signify the successful updating
# of certs end-to-end. # of certs end-to-end.
systemd.services.httpd-config-reload = let systemd.services.httpd-config-reload = let
sslServices = map (certName: "acme-${certName}.service") dependentCertNames; sslServices = map (certName: "acme-${certName}.service") vhostCertNames;
sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames; sslTargets = map (certName: "acme-finished-${certName}.target") vhostCertNames;
in mkIf (sslServices != []) { in mkIf (vhostCertNames != []) {
wantedBy = sslServices ++ [ "multi-user.target" ]; wantedBy = sslServices ++ [ "multi-user.target" ];
# Before the finished targets, after the renew services. # Before the finished targets, after the renew services.
# This service might be needed for HTTP-01 challenges, but we only want to confirm # This service might be needed for HTTP-01 challenges, but we only want to confirm
@ -801,7 +805,7 @@ in
restartTriggers = [ cfg.configFile ]; restartTriggers = [ cfg.configFile ];
# Block reloading if not all certs exist yet. # Block reloading if not all certs exist yet.
# Happens when config changes add new vhosts/certs. # 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 = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
TimeoutSec = 60; TimeoutSec = 60;

View file

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

View file

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

View file

@ -54,11 +54,6 @@ let
testCerts = import ./snakeoil-certs.nix; testCerts = import ./snakeoil-certs.nix;
domain = testCerts.domain; 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 = { pebbleConf.pebble = {
listenAddress = "0.0.0.0:443"; listenAddress = "0.0.0.0:443";
managementListenAddress = "0.0.0.0:15000"; managementListenAddress = "0.0.0.0:15000";