Skip to content

Commit f462e25

Browse files
authored
acme: Add csr option (#376334)
2 parents b475c48 + 123d134 commit f462e25

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

nixos/doc/manual/release-notes/rl-2505.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,8 @@
638638
They are still expected to be working until future version 5.0.0, but will generate warnings in logs.
639639
Read the [release notes](https://www.authelia.com/blog/4.39-release-notes/) for human readable summaries of the changes.
640640
641+
- `security.acme` now supports renewal using CSRs (Certificate Signing Request) through the options `security.acme.*.csr` and `security.acme.*.csrKey`.
642+
641643
- `programs.fzf.keybindings` now supports the fish shell.
642644
643645
- `gerbera` now has wavpack support.

nixos/modules/security/acme/default.nix

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,16 @@ let
236236

237237
# Create hashes for cert data directories based on configuration
238238
# Flags are separated to avoid collisions
239-
hashData = with builtins; ''
240-
${lib.concatStringsSep " " data.extraLegoFlags} -
241-
${lib.concatStringsSep " " data.extraLegoRunFlags} -
242-
${lib.concatStringsSep " " data.extraLegoRenewFlags} -
243-
${toString acmeServer} ${toString data.dnsProvider}
244-
${toString data.ocspMustStaple} ${data.keyType}
245-
'';
239+
hashData =
240+
with builtins;
241+
''
242+
${lib.concatStringsSep " " data.extraLegoFlags} -
243+
${lib.concatStringsSep " " data.extraLegoRunFlags} -
244+
${lib.concatStringsSep " " data.extraLegoRenewFlags} -
245+
${toString acmeServer} ${toString data.dnsProvider}
246+
${toString data.ocspMustStaple} ${data.keyType}
247+
''
248+
+ (lib.optionalString (data.csr != null) (" - " + data.csr));
246249
certDir = mkHash hashData;
247250
# TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
248251
domainHash = mkHash "${lib.concatStringsSep " " extraDomains} ${data.domain}";
@@ -286,18 +289,24 @@ let
286289
"--accept-tos" # Checking the option is covered by the assertions
287290
"--path"
288291
"."
289-
"-d"
290-
data.domain
291292
"--email"
292293
data.email
293-
"--key-type"
294-
data.keyType
295294
]
296295
++ protocolOpts
297296
++ lib.optionals (acmeServer != null) [
298297
"--server"
299298
acmeServer
300299
]
300+
++ lib.optionals (data.csr != null) [
301+
"--csr"
302+
data.csr
303+
]
304+
++ lib.optionals (data.csr == null) [
305+
"--key-type"
306+
data.keyType
307+
"-d"
308+
data.domain
309+
]
301310
++ lib.concatMap (name: [
302311
"-d"
303312
name
@@ -327,6 +336,8 @@ let
327336
webroots = lib.remove null (
328337
lib.unique (builtins.map (certAttrs: certAttrs.webroot) (lib.attrValues config.security.acme.certs))
329338
);
339+
340+
certificateKey = if data.csrKey != null then "${data.csrKey}" else "certificates/${keyName}.key";
330341
in
331342
{
332343
inherit accountHash cert selfsignedDeps;
@@ -529,7 +540,7 @@ let
529540
# Check if we can renew.
530541
# We can only renew if the list of domains has not changed.
531542
# We also need an account key. Avoids #190493
532-
if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' ] && [ -e 'certificates/${keyName}.crt' ] && [ -n "$(find accounts -name '${data.email}.key')" ]; then
543+
if cmp -s domainhash.txt certificates/domainhash.txt && [ -e '${certificateKey}' ] && [ -e 'certificates/${keyName}.crt' ] && [ -n "$(find accounts -name '${data.email}.key')" ]; then
533544
534545
# Even if a cert is not expired, it may be revoked by the CA.
535546
# Try to renew, and silently fail if the cert is not expired.
@@ -564,7 +575,7 @@ let
564575
touch out/renewed
565576
echo Installing new certificate
566577
cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
567-
cp -vp 'certificates/${keyName}.key' out/key.pem
578+
cp -vp '${certificateKey}' out/key.pem
568579
cp -vp 'certificates/${keyName}.issuer.crt' out/chain.pem
569580
ln -sf fullchain.pem out/cert.pem
570581
cat out/key.pem out/fullchain.pem > out/full.pem
@@ -845,6 +856,18 @@ let
845856
description = "Domain to fetch certificate for (defaults to the entry name).";
846857
};
847858

859+
csr = lib.mkOption {
860+
type = lib.types.nullOr lib.types.str;
861+
default = null;
862+
description = "Path to a certificate signing request to apply when fetching the certificate.";
863+
};
864+
865+
csrKey = lib.mkOption {
866+
type = lib.types.nullOr lib.types.str;
867+
default = null;
868+
description = "Path to the private key to the matching certificate signing request.";
869+
};
870+
848871
extraDomainNames = lib.mkOption {
849872
type = lib.types.listOf lib.types.str;
850873
default = [ ];
@@ -1113,6 +1136,17 @@ in
11131136
used for variables suffixed by "_FILE".
11141137
'';
11151138
}
1139+
1140+
{
1141+
assertion = lib.all (
1142+
certOpts:
1143+
(certOpts.csr == null && certOpts.csrKey == null)
1144+
|| (certOpts.csr != null && certOpts.csrKey != null)
1145+
) certs;
1146+
message = ''
1147+
When passing a certificate signing request both `security.acme.certs.${cert}.csr` and `security.acme.certs.${cert}.csrKey` need to be set.
1148+
'';
1149+
}
11161150
]) cfg.certs
11171151
));
11181152

nixos/tests/acme/http01-builtin.nix

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,45 @@ in
9999
"builtin-3.${domain}".listenHTTP = ":80";
100100
};
101101
};
102+
103+
csr.configuration =
104+
let
105+
conf = pkgs.writeText "openssl.csr.conf" ''
106+
[req]
107+
default_bits = 2048
108+
prompt = no
109+
default_md = sha256
110+
req_extensions = req_ext
111+
distinguished_name = dn
112+
113+
[ dn ]
114+
CN = ${config.networking.fqdn}
115+
116+
[ req_ext ]
117+
subjectAltName = @alt_names
118+
119+
[ alt_names ]
120+
DNS.1 = ${config.networking.fqdn}
121+
'';
122+
csrData =
123+
pkgs.runCommandNoCC "csr-and-key"
124+
{
125+
buildInputs = [ pkgs.openssl ];
126+
}
127+
''
128+
mkdir -p $out
129+
openssl req -new -newkey rsa:2048 -nodes \
130+
-keyout $out/key.pem \
131+
-out $out/request.csr \
132+
-config ${conf}
133+
'';
134+
in
135+
{
136+
security.acme.certs."${config.networking.fqdn}" = {
137+
csr = "${csrData}/request.csr";
138+
csrKey = "${csrData}/key.pem";
139+
};
140+
};
102141
};
103142
};
104143
};
@@ -211,5 +250,10 @@ in
211250
212251
with subtest("Validate permissions (self-signed)"):
213252
check_permissions(builtin, cert, "acme")
253+
254+
with subtest("Can renew using a CSR"):
255+
builtin.succeed(f"systemctl clean acme-{cert}.service --what=state")
256+
switch_to(builtin, "csr")
257+
check_issuer(builtin, cert, "pebble")
214258
'';
215259
}

0 commit comments

Comments
 (0)