Skip to content

Commit dd7670f

Browse files
security.acme: add TLS ALPN challenge
This commit adds the `tlsMode`, `tlsPort` and `conflictingServices` options to `services.acme.[...]` to allow using it even if port 80 is blocked and DNS access is impossible. Integration with nginx's enableACME=true is adjusted accordingly.
1 parent beac199 commit dd7670f

2 files changed

Lines changed: 65 additions & 12 deletions

File tree

nixos/modules/security/acme/default.nix

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,12 @@ let
274274
"--http.port"
275275
data.listenHTTP
276276
]
277+
else if data.tlsMode then
278+
[
279+
"--tls"
280+
"--tls.port"
281+
":${toString data.tlsPort}"
282+
]
277283
else
278284
[
279285
"--http"
@@ -404,6 +410,13 @@ let
404410

405411
renewService = lockfileName: {
406412
description = "Renew ACME certificate for ${cert}";
413+
# stop conflicting services while certs are renewed
414+
conflicts = data.conflictingServices;
415+
# start conflicting services again after cert renewal
416+
# This causes systemd to issue a warning 'multiple trigger source candidates for exit status propagation',
417+
# but this is more robust and not prone to race conditions as invoking systemctl in ExecStartPost/ExecStopPost
418+
onSuccess = data.conflictingServices;
419+
onFailure = data.conflictingServices;
407420
after = [
408421
"network.target"
409422
"network-online.target"
@@ -471,13 +484,17 @@ let
471484
fi
472485
'');
473486
}
474-
//
475-
lib.optionalAttrs
476-
(data.listenHTTP != null && lib.toInt (lib.last (lib.splitString ":" data.listenHTTP)) < 1024)
477-
{
478-
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
479-
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
480-
};
487+
// (
488+
let
489+
needsToOpenPrivilegedPort =
490+
(data.listenHTTP != null && lib.toInt (lib.last (lib.splitString ":" data.listenHTTP)) < 1024)
491+
|| (data.tlsMode && data.tlsPort < 1024);
492+
in
493+
lib.optionalAttrs needsToOpenPrivilegedPort {
494+
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
495+
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
496+
}
497+
);
481498

482499
# Working directory will be /tmp
483500
script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") ''
@@ -670,6 +687,19 @@ let
670687
description = "Group running the ACME client.";
671688
};
672689

690+
conflictingServices = lib.mkOption {
691+
type = lib.types.listOf lib.types.str;
692+
inherit (defaultAndText "conflictingServices" [ ]) default defaultText;
693+
description = ''
694+
List of conflicting systemd services that should be temporarily stopped
695+
while renewing/checking certificates. This might be necessary with `tlsMode=true`
696+
when a webserver occupies the `tlsPort`.
697+
'';
698+
example = ''
699+
[ "nginx.service" ] # stops nginx temporarily while renewing/checking certificates
700+
'';
701+
};
702+
673703
reloadServices = lib.mkOption {
674704
type = lib.types.listOf lib.types.str;
675705
inherit (defaultAndText "reloadServices" [ ]) default defaultText;
@@ -733,6 +763,18 @@ let
733763
'';
734764
};
735765

766+
tlsMode = lib.mkOption {
767+
type = lib.types.bool;
768+
inherit (defaultAndText "tlsMode" false) default defaultText;
769+
description = "Use TLS challenge instead of HTTP.";
770+
};
771+
772+
tlsPort = lib.mkOption {
773+
type = lib.types.port;
774+
inherit (defaultAndText "tlsPort" 443) default defaultText;
775+
description = "Port to use for TLS challenge.";
776+
};
777+
736778
environmentFile = lib.mkOption {
737779
type = lib.types.nullOr lib.types.path;
738780
inherit (defaultAndText "environmentFile" null) default defaultText;
@@ -1090,6 +1132,7 @@ in
10901132
webroot
10911133
listenHTTP
10921134
s3Bucket
1135+
tlsMode
10931136
;
10941137
};
10951138
in
@@ -1101,6 +1144,7 @@ in
11011144
`security.acme.certs.${cert}.webroot`,
11021145
`security.acme.certs.${cert}.listenHTTP` and
11031146
`security.acme.certs.${cert}.s3Bucket`
1147+
`security.acme.certs.${cert}.tlsMode`
11041148
is required.
11051149
Current values: ${(lib.generators.toPretty { } exclusiveAttrs)}.
11061150
'';

nixos/modules/services/web-servers/nginx/default.nix

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ let
1515
vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null
1616
) vhostsConfigs;
1717
vhostCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
18-
dependentCertNames = filter (cert: certs.${cert}.dnsProvider == null) vhostCertNames; # those that might depend on the HTTP server
19-
independentCertNames = filter (cert: certs.${cert}.dnsProvider != null) vhostCertNames; # those that don't depend on the HTTP server
18+
dependentCertNames = filter (
19+
cert: certs.${cert}.dnsProvider == null && !certs.${cert}.tlsMode
20+
) vhostCertNames; # those that might depend on the HTTP server
21+
independentCertNames = filter (
22+
cert: certs.${cert}.dnsProvider != null || certs.${cert}.tlsMode
23+
) vhostCertNames; # those that don't depend on the HTTP server
2024
virtualHosts = mapAttrs (
2125
vhostName: vhostConfig:
2226
let
@@ -422,13 +426,14 @@ let
422426
redirectListen = filter (x: !x.ssl) defaultListen;
423427

424428
# The acme-challenge location doesn't need to be added if we are not using any automated
425-
# certificate provisioning and can also be omitted when we use a certificate obtained via a DNS-01 challenge
429+
# certificate provisioning and can also be omitted when we use a certificate obtained via a DNS-01 or TLS-ALPN challenge
426430
acmeName = if vhost.useACMEHost != null then vhost.useACMEHost else vhost.serverName;
427431
acmeLocation =
428432
optionalString
429433
(
430434
(vhost.enableACME || vhost.useACMEHost != null)
431-
&& config.security.acme.certs.${acmeName}.dnsProvider == null
435+
&& certs.${acmeName}.dnsProvider == null
436+
&& !certs.${acmeName}.tlsMode
432437
)
433438
# Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
434439
# We use ^~ here, so that we don't check any regexes (which could
@@ -1632,7 +1637,11 @@ in
16321637
# if acmeRoot is null inherit config.security.acme
16331638
# Since config.security.acme.certs.<cert>.webroot's own default value
16341639
# should take precedence set priority higher than mkOptionDefault
1635-
webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot;
1640+
webroot =
1641+
if certs.${vhostConfig.serverName}.tlsMode then
1642+
null
1643+
else
1644+
mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot;
16361645
# Also nudge dnsProvider to null in case it is inherited
16371646
dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
16381647
extraDomainNames = vhostConfig.serverAliases;

0 commit comments

Comments
 (0)