mirror of
https://github.com/NixOS/nixpkgs.git
synced 2024-12-18 02:46:28 +00:00
Merge pull request #77578 from m1cr0man/master
Replace simp-le with lego and support DNS-01 challenge
This commit is contained in:
commit
4e0fea3fe2
|
@ -4304,6 +4304,12 @@
|
||||||
email = "wheatdoge@gmail.com";
|
email = "wheatdoge@gmail.com";
|
||||||
name = "Tim Liou";
|
name = "Tim Liou";
|
||||||
};
|
};
|
||||||
|
m1cr0man = {
|
||||||
|
email = "lucas+nix@m1cr0man.com";
|
||||||
|
github = "m1cr0man";
|
||||||
|
githubId = 3044438;
|
||||||
|
name = "Lucas Savva";
|
||||||
|
};
|
||||||
m3tti = {
|
m3tti = {
|
||||||
email = "mathaeus.peter.sander@gmail.com";
|
email = "mathaeus.peter.sander@gmail.com";
|
||||||
name = "Mathaeus Sander";
|
name = "Mathaeus Sander";
|
||||||
|
|
|
@ -660,6 +660,21 @@ auth required pam_succeed_if.so uid >= 1000 quiet
|
||||||
<literal>PRETTY_NAME</literal> in <literal>/etc/os-release</literal>
|
<literal>PRETTY_NAME</literal> in <literal>/etc/os-release</literal>
|
||||||
now uses the short rather than full version string.
|
now uses the short rather than full version string.
|
||||||
</para>
|
</para>
|
||||||
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
The ACME module has switched from simp-le to <link xlink:href="https://github.com/go-acme/lego">lego</link>
|
||||||
|
which allows us to support DNS-01 challenges and wildcard certificates. The following options have been added:
|
||||||
|
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link>,
|
||||||
|
<link linkend="opt-security.acme.certs">security.acme.certs.<name>.dnsProvider</link>,
|
||||||
|
<link linkend="opt-security.acme.certs">security.acme.certs.<name>.credentialsFile</link>,
|
||||||
|
<link linkend="opt-security.acme.certs">security.acme.certs.<name>.dnsPropagationCheck</link>.
|
||||||
|
As well as this, the options <literal>security.acme.acceptTerms</literal> and either
|
||||||
|
<literal>security.acme.email</literal> or <literal>security.acme.certs.<name>.email</literal>
|
||||||
|
must be set in order to use the ACME module.
|
||||||
|
Certificates will be regenerated anew on the next renewal date. The credentials for simp-le are
|
||||||
|
preserved and thus it is possible to roll back to previous versions without breaking certificate
|
||||||
|
generation.
|
||||||
</listitem>
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|
||||||
cfg = config.security.acme;
|
cfg = config.security.acme;
|
||||||
|
@ -9,7 +7,8 @@ let
|
||||||
certOpts = { name, ... }: {
|
certOpts = { name, ... }: {
|
||||||
options = {
|
options = {
|
||||||
webroot = mkOption {
|
webroot = mkOption {
|
||||||
type = types.str;
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
example = "/var/lib/acme/acme-challenges";
|
example = "/var/lib/acme/acme-challenges";
|
||||||
description = ''
|
description = ''
|
||||||
Where the webroot of the HTTP vhost is located.
|
Where the webroot of the HTTP vhost is located.
|
||||||
|
@ -38,7 +37,7 @@ let
|
||||||
|
|
||||||
email = mkOption {
|
email = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = cfg.email;
|
||||||
description = "Contact email address for the CA to be able to reach you.";
|
description = "Contact email address for the CA to be able to reach you.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,20 +75,6 @@ let
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
plugins = mkOption {
|
|
||||||
type = types.listOf (types.enum [
|
|
||||||
"cert.der" "cert.pem" "chain.pem" "external.sh"
|
|
||||||
"fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" "account_reg.json"
|
|
||||||
]);
|
|
||||||
default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" "account_reg.json" ];
|
|
||||||
description = ''
|
|
||||||
Plugins to enable. With default settings simp_le will
|
|
||||||
store public certificate bundle in <filename>fullchain.pem</filename>,
|
|
||||||
private key in <filename>key.pem</filename> and those two previous
|
|
||||||
files combined in <filename>full.pem</filename> in its state directory.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
directory = mkOption {
|
directory = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
|
@ -111,6 +96,46 @@ let
|
||||||
own server roots if needed.
|
own server roots if needed.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
keyType = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "ec384";
|
||||||
|
description = ''
|
||||||
|
Key type to use for private keys.
|
||||||
|
For an up to date list of supported values check the --key-type option
|
||||||
|
at https://go-acme.github.io/lego/usage/cli/#usage.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
dnsProvider = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "route53";
|
||||||
|
description = ''
|
||||||
|
DNS Challenge provider. For a list of supported providers, see the "code"
|
||||||
|
field of the DNS providers listed at https://go-acme.github.io/lego/dns/.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
credentialsFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = ''
|
||||||
|
Path to an EnvironmentFile for the cert's service containing any required and
|
||||||
|
optional environment variables for your selected dnsProvider.
|
||||||
|
To find out what values you need to set, consult the documentation at
|
||||||
|
https://go-acme.github.io/lego/dns/ for the corresponding dnsProvider.
|
||||||
|
'';
|
||||||
|
example = "/var/src/secrets/example.org-route53-api-token";
|
||||||
|
};
|
||||||
|
|
||||||
|
dnsPropagationCheck = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Toggles lego DNS propagation check, which is used alongside DNS-01
|
||||||
|
challenge to ensure the DNS entries required are available.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -130,14 +155,21 @@ in
|
||||||
(mkRemovedOptionModule [ "security" "acme" "directory"] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
|
(mkRemovedOptionModule [ "security" "acme" "directory"] "ACME Directory is now hardcoded to /var/lib/acme and its permisisons are managed by systemd. See https://github.com/NixOS/nixpkgs/issues/53852 for more info.")
|
||||||
(mkRemovedOptionModule [ "security" "acme" "preDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
(mkRemovedOptionModule [ "security" "acme" "preDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
||||||
(mkRemovedOptionModule [ "security" "acme" "activationDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
(mkRemovedOptionModule [ "security" "acme" "activationDelay"] "This option has been removed. If you want to make sure that something executes before certificates are provisioned, add a RequiredBy=acme-\${cert}.service to the service you want to execute before the cert renewal")
|
||||||
|
(mkChangedOptionModule [ "security" "acme" "validMin"] [ "security" "acme" "validMinDays"] (config: config.security.acme.validMin / (24 * 3600)))
|
||||||
];
|
];
|
||||||
options = {
|
options = {
|
||||||
security.acme = {
|
security.acme = {
|
||||||
|
|
||||||
validMin = mkOption {
|
validMinDays = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 30 * 24 * 3600;
|
default = 30;
|
||||||
description = "Minimum remaining validity before renewal in seconds.";
|
description = "Minimum remaining validity before renewal in days.";
|
||||||
|
};
|
||||||
|
|
||||||
|
email = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Contact email address for the CA to be able to reach you.";
|
||||||
};
|
};
|
||||||
|
|
||||||
renewInterval = mkOption {
|
renewInterval = mkOption {
|
||||||
|
@ -173,6 +205,15 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
acceptTerms = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Accept the CA's terms of service. The default provier is Let's Encrypt,
|
||||||
|
you can find their ToS at https://letsencrypt.org/repository/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
certs = mkOption {
|
certs = mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
type = with types; attrsOf (submodule certOpts);
|
type = with types; attrsOf (submodule certOpts);
|
||||||
|
@ -204,27 +245,55 @@ in
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
(mkIf (cfg.certs != { }) {
|
(mkIf (cfg.certs != { }) {
|
||||||
|
|
||||||
|
assertions = let
|
||||||
|
certs = (mapAttrsToList (k: v: v) cfg.certs);
|
||||||
|
in [
|
||||||
|
{
|
||||||
|
assertion = all (certOpts: certOpts.dnsProvider == null || certOpts.webroot == null) certs;
|
||||||
|
message = ''
|
||||||
|
Options `security.acme.certs.<name>.dnsProvider` and
|
||||||
|
`security.acme.certs.<name>.webroot` are mutually exclusive.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
|
||||||
|
message = ''
|
||||||
|
You must define `security.acme.certs.<name>.email` or
|
||||||
|
`security.acme.email` to register with the CA.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = cfg.acceptTerms;
|
||||||
|
message = ''
|
||||||
|
You must accept the CA's terms of service before using
|
||||||
|
the ACME module by setting `security.acme.acceptTerms`
|
||||||
|
to `true`. For Let's Encrypt's ToS see https://letsencrypt.org/repository/
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
systemd.services = let
|
systemd.services = let
|
||||||
services = concatLists servicesLists;
|
services = concatLists servicesLists;
|
||||||
servicesLists = mapAttrsToList certToServices cfg.certs;
|
servicesLists = mapAttrsToList certToServices cfg.certs;
|
||||||
certToServices = cert: data:
|
certToServices = cert: data:
|
||||||
let
|
let
|
||||||
|
# StateDirectory must be relative, and will be created under /var/lib by systemd
|
||||||
lpath = "acme/${cert}";
|
lpath = "acme/${cert}";
|
||||||
|
apath = "/var/lib/${lpath}";
|
||||||
|
spath = "/var/lib/acme/.lego";
|
||||||
rights = if data.allowKeysForGroup then "750" else "700";
|
rights = if data.allowKeysForGroup then "750" else "700";
|
||||||
cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ]
|
globalOpts = [ "-d" data.domain "--email" data.email "--path" "." "--key-type" data.keyType ]
|
||||||
++ optionals (data.email != null) [ "--email" data.email ]
|
++ optionals (cfg.acceptTerms) [ "--accept-tos" ]
|
||||||
++ concatMap (p: [ "-f" p ]) data.plugins
|
++ optionals (data.dnsProvider != null && !data.dnsPropagationCheck) [ "--dns.disable-cp" ]
|
||||||
++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains)
|
++ concatLists (mapAttrsToList (name: root: [ "-d" name ]) data.extraDomains)
|
||||||
|
++ (if data.dnsProvider != null then [ "--dns" data.dnsProvider ] else [ "--http" "--http.webroot" data.webroot ])
|
||||||
++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)];
|
++ optionals (cfg.server != null || data.server != null) ["--server" (if data.server == null then cfg.server else data.server)];
|
||||||
|
runOpts = escapeShellArgs (globalOpts ++ [ "run" ]);
|
||||||
|
renewOpts = escapeShellArgs (globalOpts ++ [ "renew" "--days" (toString cfg.validMinDays) ]);
|
||||||
acmeService = {
|
acmeService = {
|
||||||
description = "Renew ACME Certificate for ${cert}";
|
description = "Renew ACME Certificate for ${cert}";
|
||||||
after = [ "network.target" "network-online.target" ];
|
after = [ "network.target" "network-online.target" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
# simp_le uses requests, which uses certifi under the hood,
|
|
||||||
# which doesn't respect the system trust store.
|
|
||||||
# At least in the acme test, we provision a fake CA, impersonating the LE endpoint.
|
|
||||||
# REQUESTS_CA_BUNDLE is a way to teach python requests to use something else
|
|
||||||
environment.REQUESTS_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
# With RemainAfterExit the service is considered active even
|
# With RemainAfterExit the service is considered active even
|
||||||
|
@ -233,18 +302,37 @@ in
|
||||||
# the permissions of the StateDirectory get adjusted
|
# the permissions of the StateDirectory get adjusted
|
||||||
# according to the specified group
|
# according to the specified group
|
||||||
RemainAfterExit = true;
|
RemainAfterExit = true;
|
||||||
SuccessExitStatus = [ "0" "1" ];
|
|
||||||
User = data.user;
|
User = data.user;
|
||||||
Group = data.group;
|
Group = data.group;
|
||||||
PrivateTmp = true;
|
PrivateTmp = true;
|
||||||
StateDirectory = lpath;
|
StateDirectory = "acme/.lego ${lpath}";
|
||||||
StateDirectoryMode = rights;
|
StateDirectoryMode = rights;
|
||||||
WorkingDirectory = "/var/lib/${lpath}";
|
WorkingDirectory = spath;
|
||||||
ExecStart = "${pkgs.simp_le}/bin/simp_le ${escapeShellArgs cmdline}";
|
# Only try loading the credentialsFile if the dns challenge is enabled
|
||||||
|
EnvironmentFile = if data.dnsProvider != null then data.credentialsFile else null;
|
||||||
|
ExecStart = pkgs.writeScript "acme-start" ''
|
||||||
|
#!${pkgs.runtimeShell} -e
|
||||||
|
${pkgs.lego}/bin/lego ${renewOpts} || ${pkgs.lego}/bin/lego ${runOpts}
|
||||||
|
'';
|
||||||
ExecStartPost =
|
ExecStartPost =
|
||||||
let
|
let
|
||||||
|
keyName = builtins.replaceStrings ["*"] ["_"] data.domain;
|
||||||
script = pkgs.writeScript "acme-post-start" ''
|
script = pkgs.writeScript "acme-post-start" ''
|
||||||
#!${pkgs.runtimeShell} -e
|
#!${pkgs.runtimeShell} -e
|
||||||
|
cd ${apath}
|
||||||
|
|
||||||
|
# Test that existing cert is older than new cert
|
||||||
|
KEY=${spath}/certificates/${keyName}.key
|
||||||
|
if [ -e $KEY -a $KEY -nt key.pem ]; then
|
||||||
|
cp -p ${spath}/certificates/${keyName}.key key.pem
|
||||||
|
cp -p ${spath}/certificates/${keyName}.crt cert.pem
|
||||||
|
cp -p ${spath}/certificates/${keyName}.issuer.crt chain.pem
|
||||||
|
cat cert.pem chain.pem > fullchain.pem
|
||||||
|
cat key.pem cert.pem chain.pem > full.pem
|
||||||
|
chmod ${rights} *.pem
|
||||||
|
chown '${data.user}:${data.group}' *.pem
|
||||||
|
fi
|
||||||
|
|
||||||
${data.postRun}
|
${data.postRun}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
@ -276,17 +364,17 @@ in
|
||||||
-out $workdir/server.crt
|
-out $workdir/server.crt
|
||||||
|
|
||||||
# Copy key to destination
|
# Copy key to destination
|
||||||
cp $workdir/server.key /var/lib/${lpath}/key.pem
|
cp $workdir/server.key ${apath}/key.pem
|
||||||
|
|
||||||
# Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
|
# Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
|
||||||
cat $workdir/{server.crt,ca.crt} > "/var/lib/${lpath}/fullchain.pem"
|
cat $workdir/{server.crt,ca.crt} > "${apath}/fullchain.pem"
|
||||||
|
|
||||||
# Create full.pem for e.g. lighttpd
|
# Create full.pem for e.g. lighttpd
|
||||||
cat $workdir/{server.key,server.crt,ca.crt} > "/var/lib/${lpath}/full.pem"
|
cat $workdir/{server.key,server.crt,ca.crt} > "${apath}/full.pem"
|
||||||
|
|
||||||
# Give key acme permissions
|
# Give key acme permissions
|
||||||
chown '${data.user}:${data.group}' "/var/lib/${lpath}/"{key,fullchain,full}.pem
|
chown '${data.user}:${data.group}' "${apath}/"{key,fullchain,full}.pem
|
||||||
chmod ${rights} "/var/lib/${lpath}/"{key,fullchain,full}.pem
|
chmod ${rights} "${apath}/"{key,fullchain,full}.pem
|
||||||
'';
|
'';
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
|
@ -297,7 +385,7 @@ in
|
||||||
};
|
};
|
||||||
unitConfig = {
|
unitConfig = {
|
||||||
# Do not create self-signed key when key already exists
|
# Do not create self-signed key when key already exists
|
||||||
ConditionPathExists = "!/var/lib/${lpath}/key.pem";
|
ConditionPathExists = "!${apath}/key.pem";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in (
|
in (
|
||||||
|
@ -309,8 +397,7 @@ in
|
||||||
servicesAttr;
|
servicesAttr;
|
||||||
|
|
||||||
systemd.tmpfiles.rules =
|
systemd.tmpfiles.rules =
|
||||||
flip mapAttrsToList cfg.certs
|
map (data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}") (filter (data: data.webroot != null) (attrValues cfg.certs));
|
||||||
(cert: data: "d ${data.webroot}/.well-known/acme-challenge - ${data.user} ${data.group}");
|
|
||||||
|
|
||||||
systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
|
systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
|
||||||
("acme-${cert}")
|
("acme-${cert}")
|
||||||
|
@ -334,7 +421,7 @@ in
|
||||||
];
|
];
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
maintainers = with lib.maintainers; [ abbradar fpletz globin ];
|
maintainers = with lib.maintainers; [ abbradar fpletz globin m1cr0man ];
|
||||||
doc = ./acme.xml;
|
doc = ./acme.xml;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<para>
|
<para>
|
||||||
NixOS supports automatic domain validation & certificate retrieval and
|
NixOS supports automatic domain validation & certificate retrieval and
|
||||||
renewal using the ACME protocol. This is currently only implemented by and
|
renewal using the ACME protocol. This is currently only implemented by and
|
||||||
for Let's Encrypt. The alternative ACME client <literal>simp_le</literal> is
|
for Let's Encrypt. The alternative ACME client <literal>lego</literal> is
|
||||||
used under the hood.
|
used under the hood.
|
||||||
</para>
|
</para>
|
||||||
<section xml:id="module-security-acme-prerequisites">
|
<section xml:id="module-security-acme-prerequisites">
|
||||||
|
|
|
@ -1,17 +1,50 @@
|
||||||
let
|
let
|
||||||
commonConfig = ./common/letsencrypt/common.nix;
|
commonConfig = ./common/letsencrypt/common.nix;
|
||||||
|
|
||||||
|
dnsScript = {writeScript, dnsAddress, bash, curl}: writeScript "dns-hook.sh" ''
|
||||||
|
#!${bash}/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
echo '[INFO]' "[$2]" 'dns-hook.sh' $*
|
||||||
|
if [ "$1" = "present" ]; then
|
||||||
|
${curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt
|
||||||
|
else
|
||||||
|
${curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
in import ./make-test-python.nix {
|
in import ./make-test-python.nix {
|
||||||
name = "acme";
|
name = "acme";
|
||||||
|
|
||||||
nodes = rec {
|
nodes = rec {
|
||||||
letsencrypt = ./common/letsencrypt;
|
letsencrypt = { nodes, lib, ... }: {
|
||||||
|
imports = [ ./common/letsencrypt ];
|
||||||
|
networking.nameservers = lib.mkForce [
|
||||||
|
nodes.dnsserver.config.networking.primaryIPAddress
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
acmeStandalone = { config, pkgs, ... }: {
|
dnsserver = { nodes, pkgs, ... }: {
|
||||||
|
networking.firewall.allowedTCPPorts = [ 8055 53 ];
|
||||||
|
networking.firewall.allowedUDPPorts = [ 53 ];
|
||||||
|
systemd.services.pebble-challtestsrv = {
|
||||||
|
enable = true;
|
||||||
|
description = "Pebble ACME challenge test server";
|
||||||
|
wantedBy = [ "network.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.config.networking.primaryIPAddress}'";
|
||||||
|
# Required to bind on privileged ports.
|
||||||
|
User = "root";
|
||||||
|
Group = "root";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
acmeStandalone = { nodes, lib, config, pkgs, ... }: {
|
||||||
imports = [ commonConfig ];
|
imports = [ commonConfig ];
|
||||||
|
networking.nameservers = lib.mkForce [
|
||||||
|
nodes.dnsserver.config.networking.primaryIPAddress
|
||||||
|
];
|
||||||
networking.firewall.allowedTCPPorts = [ 80 ];
|
networking.firewall.allowedTCPPorts = [ 80 ];
|
||||||
networking.extraHosts = ''
|
|
||||||
${config.networking.primaryIPAddress} standalone.com
|
|
||||||
'';
|
|
||||||
security.acme = {
|
security.acme = {
|
||||||
server = "https://acme-v02.api.letsencrypt.org/dir";
|
server = "https://acme-v02.api.letsencrypt.org/dir";
|
||||||
certs."standalone.com" = {
|
certs."standalone.com" = {
|
||||||
|
@ -29,14 +62,12 @@ in import ./make-test-python.nix {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
webserver = { config, pkgs, ... }: {
|
webserver = { nodes, config, pkgs, lib, ... }: {
|
||||||
imports = [ commonConfig ];
|
imports = [ commonConfig ];
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||||
|
networking.nameservers = lib.mkForce [
|
||||||
networking.extraHosts = ''
|
nodes.dnsserver.config.networking.primaryIPAddress
|
||||||
${config.networking.primaryIPAddress} a.example.com
|
];
|
||||||
${config.networking.primaryIPAddress} b.example.com
|
|
||||||
'';
|
|
||||||
|
|
||||||
# A target remains active. Use this to probe the fact that
|
# A target remains active. Use this to probe the fact that
|
||||||
# a service fired eventhough it is not RemainAfterExit
|
# a service fired eventhough it is not RemainAfterExit
|
||||||
|
@ -61,14 +92,11 @@ in import ./make-test-python.nix {
|
||||||
|
|
||||||
nesting.clone = [
|
nesting.clone = [
|
||||||
({pkgs, ...}: {
|
({pkgs, ...}: {
|
||||||
|
|
||||||
networking.extraHosts = ''
|
|
||||||
${config.networking.primaryIPAddress} b.example.com
|
|
||||||
'';
|
|
||||||
systemd.targets."acme-finished-b.example.com" = {};
|
systemd.targets."acme-finished-b.example.com" = {};
|
||||||
systemd.services."acme-b.example.com" = {
|
systemd.services."acme-b.example.com" = {
|
||||||
wants = [ "acme-finished-b.example.com.target" ];
|
wants = [ "acme-finished-b.example.com.target" ];
|
||||||
before = [ "acme-finished-b.example.com.target" ];
|
before = [ "acme-finished-b.example.com.target" ];
|
||||||
|
after = [ "nginx.service" ];
|
||||||
};
|
};
|
||||||
services.nginx.virtualHosts."b.example.com" = {
|
services.nginx.virtualHosts."b.example.com" = {
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
|
@ -79,15 +107,48 @@ in import ./make-test-python.nix {
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
({pkgs, config, nodes, lib, ...}: {
|
||||||
|
security.acme.certs."example.com" = {
|
||||||
|
domain = "*.example.com";
|
||||||
|
dnsProvider = "exec";
|
||||||
|
dnsPropagationCheck = false;
|
||||||
|
credentialsFile = with pkgs; writeText "wildcard.env" ''
|
||||||
|
EXEC_PATH=${dnsScript { inherit writeScript bash curl; dnsAddress = nodes.dnsserver.config.networking.primaryIPAddress; }}
|
||||||
|
'';
|
||||||
|
user = config.services.nginx.user;
|
||||||
|
group = config.services.nginx.group;
|
||||||
|
};
|
||||||
|
systemd.targets."acme-finished-example.com" = {};
|
||||||
|
systemd.services."acme-example.com" = {
|
||||||
|
wants = [ "acme-finished-example.com.target" ];
|
||||||
|
before = [ "acme-finished-example.com.target" "nginx.service" ];
|
||||||
|
wantedBy = [ "nginx.service" ];
|
||||||
|
};
|
||||||
|
services.nginx.virtualHosts."c.example.com" = {
|
||||||
|
forceSSL = true;
|
||||||
|
sslCertificate = config.security.acme.certs."example.com".directory + "/cert.pem";
|
||||||
|
sslTrustedCertificate = config.security.acme.certs."example.com".directory + "/full.pem";
|
||||||
|
sslCertificateKey = config.security.acme.certs."example.com".directory + "/key.pem";
|
||||||
|
locations."/".root = pkgs.runCommand "docroot" {} ''
|
||||||
|
mkdir -p "$out"
|
||||||
|
echo hello world > "$out/index.html"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
client = commonConfig;
|
client = {nodes, lib, ...}: {
|
||||||
|
imports = [ commonConfig ];
|
||||||
|
networking.nameservers = lib.mkForce [
|
||||||
|
nodes.dnsserver.config.networking.primaryIPAddress
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = {nodes, ...}:
|
testScript = {nodes, ...}:
|
||||||
let
|
let
|
||||||
newServerSystem = nodes.webserver2.config.system.build.toplevel;
|
newServerSystem = nodes.webserver.config.system.build.toplevel;
|
||||||
switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
|
switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
|
||||||
in
|
in
|
||||||
# Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
|
# Note, wait_for_unit does not work for oneshot services that do not have RemainAfterExit=true,
|
||||||
|
@ -97,6 +158,17 @@ in import ./make-test-python.nix {
|
||||||
# can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do
|
# can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do
|
||||||
''
|
''
|
||||||
client.start()
|
client.start()
|
||||||
|
dnsserver.start()
|
||||||
|
|
||||||
|
letsencrypt.wait_for_unit("default.target")
|
||||||
|
dnsserver.wait_for_unit("pebble-challtestsrv.service")
|
||||||
|
client.succeed(
|
||||||
|
'curl --data \'{"host": "acme-v02.api.letsencrypt.org", "addresses": ["${nodes.letsencrypt.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a'
|
||||||
|
)
|
||||||
|
client.succeed(
|
||||||
|
'curl --data \'{"host": "standalone.com", "addresses": ["${nodes.acmeStandalone.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a'
|
||||||
|
)
|
||||||
|
|
||||||
letsencrypt.start()
|
letsencrypt.start()
|
||||||
acmeStandalone.start()
|
acmeStandalone.start()
|
||||||
|
|
||||||
|
@ -129,5 +201,17 @@ in import ./make-test-python.nix {
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF 'hello world'"
|
"curl --cacert /tmp/ca.crt https://b.example.com/ | grep -qF 'hello world'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with subtest("Can request wildcard certificates using DNS-01 challenge"):
|
||||||
|
webserver.succeed(
|
||||||
|
"${switchToNewServer}"
|
||||||
|
)
|
||||||
|
webserver.succeed(
|
||||||
|
"/run/current-system/fine-tune/child-2/bin/switch-to-configuration test"
|
||||||
|
)
|
||||||
|
webserver.wait_for_unit("acme-finished-example.com.target")
|
||||||
|
client.succeed(
|
||||||
|
"curl --cacert /tmp/ca.crt https://c.example.com/ | grep -qF 'hello world'"
|
||||||
|
)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,8 @@ in {
|
||||||
nodes.letsencrypt.config.networking.primaryIPAddress
|
nodes.letsencrypt.config.networking.primaryIPAddress
|
||||||
];
|
];
|
||||||
|
|
||||||
|
security.acme.acceptTerms = true;
|
||||||
|
security.acme.email = "webmaster@example.com";
|
||||||
|
|
||||||
security.pki.certificateFiles = [ letsencrypt-ca ];
|
security.pki.certificateFiles = [ letsencrypt-ca ];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue