nixos/acme: Add defaults and inheritDefaults option

Allows configuring many default settings for certificates,
all of which can still be overridden on a per-cert basis.
Some options have been moved into .defaults from security.acme,
namely email, server, validMinDays and renewInterval. These
changes will not break existing configurations thanks to
mkChangedOptionModule.

With this, it is also now possible to configure DNS-01 with
web servers whose virtualHosts utilise enableACME. The only
requirement is you set `acmeRoot = null` for each vhost.

The test suite has been revamped to cover these additions
and also to generally make it easier to maintain. Test config
for apache and nginx has been fully standardised, and it
is now much easier to add a new web server if it follows
the same configuration patterns as those two. I have also
optimised the use of switch-to-configuration which should
speed up testing.
This commit is contained in:
Lucas Savva 2021-11-28 17:03:31 +00:00
parent a7f0001328
commit 377c6bcefc
No known key found for this signature in database
GPG Key ID: E4EC5BF2E2F116A2
8 changed files with 536 additions and 436 deletions

View File

@ -128,7 +128,7 @@ let
}; };
certToConfig = cert: data: let certToConfig = cert: data: let
acmeServer = if data.server != null then data.server else cfg.server; acmeServer = data.server;
useDns = data.dnsProvider != null; useDns = data.dnsProvider != null;
destPath = "/var/lib/acme/${cert}"; destPath = "/var/lib/acme/${cert}";
selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ]; selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
@ -211,7 +211,7 @@ let
description = "Renew ACME Certificate for ${cert}"; description = "Renew ACME Certificate for ${cert}";
wantedBy = [ "timers.target" ]; wantedBy = [ "timers.target" ];
timerConfig = { timerConfig = {
OnCalendar = cfg.renewInterval; OnCalendar = data.renewInterval;
Unit = "acme-${cert}.service"; Unit = "acme-${cert}.service";
Persistent = "yes"; Persistent = "yes";
@ -356,7 +356,7 @@ let
expiration_s=$[expiration_date - now] expiration_s=$[expiration_date - now]
expiration_days=$[expiration_s / (3600 * 24)] # rounds down expiration_days=$[expiration_s / (3600 * 24)] # rounds down
[[ $expiration_days -gt ${toString cfg.validMinDays} ]] [[ $expiration_days -gt ${toString data.validMinDays} ]]
} }
${optionalString (data.webroot != null) '' ${optionalString (data.webroot != null) ''
@ -380,11 +380,12 @@ let
# Even if a cert is not expired, it may be revoked by the CA. # Even if a cert is not expired, it may be revoked by the CA.
# Try to renew, and silently fail if the cert is not expired. # Try to renew, and silently fail if the cert is not expired.
# Avoids #85794 and resolves #129838 # Avoids #85794 and resolves #129838
if ! lego ${renewOpts} --days ${toString cfg.validMinDays}; then if ! lego ${renewOpts} --days ${toString data.validMinDays}; then
if is_expiration_skippable out/full.pem; then if is_expiration_skippable out/full.pem; then
echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString cfg.validMinDays} days" echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days"
else else
exit 3 # High number to avoid Systemd reserved codes.
exit 11
fi fi
fi fi
@ -394,8 +395,9 @@ let
echo Failed to fetch certificates. \ echo Failed to fetch certificates. \
This may mean your DNS records are set up incorrectly. \ This may mean your DNS records are set up incorrectly. \
${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."} ${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
# Exit 2 so that users can potentially amend SuccessExitStatus to ignore this error. # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
exit 2 # High number to avoid Systemd reserved codes.
exit 10
fi fi
mv domainhash.txt certificates/ mv domainhash.txt certificates/
@ -423,8 +425,182 @@ let
certConfigs = mapAttrs certToConfig cfg.certs; certConfigs = mapAttrs certToConfig cfg.certs;
# These options can be specified within
# security.acme or security.acme.certs.<name>
inheritableOpts =
{ inheritDefaults ? false, defaults ? null }: {
validMinDays = mkOption {
type = types.int;
default = if inheritDefaults then defaults.validMinDays else 30;
description = "Minimum remaining validity before renewal in days.";
};
renewInterval = mkOption {
type = types.str;
default = if inheritDefaults then defaults.renewInterval else "daily";
description = ''
Systemd calendar expression when to check for renewal. See
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
enableDebugLogs = mkEnableOption "debug logging for this certificate" // {
default = if inheritDefaults then defaults.enableDebugLogs else true;
};
webroot = mkOption {
type = types.nullOr types.str;
default = if inheritDefaults then defaults.webroot else null;
example = "/var/lib/acme/acme-challenge";
description = ''
Where the webroot of the HTTP vhost is located.
<filename>.well-known/acme-challenge/</filename> directory
will be created below the webroot if it doesn't exist.
<literal>http://example.org/.well-known/acme-challenge/</literal> must also
be available (notice unencrypted HTTP).
'';
};
server = mkOption {
type = types.nullOr types.str;
default = if inheritDefaults then defaults.server else null;
description = ''
ACME Directory Resource URI. Defaults to Let's Encrypt's
production endpoint,
<link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
'';
};
email = mkOption {
type = types.str;
default = if inheritDefaults then defaults.email else null;
description = ''
Email address for account creation and correspondence from the CA.
It is recommended to use the same email for all certs to avoid account
creation limits.
'';
};
group = mkOption {
type = types.str;
default = if inheritDefaults then defaults.group else "acme";
description = "Group running the ACME client.";
};
reloadServices = mkOption {
type = types.listOf types.str;
default = if inheritDefaults then defaults.reloadServices else [];
description = ''
The list of systemd services to call <code>systemctl try-reload-or-restart</code>
on.
'';
};
postRun = mkOption {
type = types.lines;
default = if inheritDefaults then defaults.postRun else "";
example = "cp full.pem backup.pem";
description = ''
Commands to run after new certificates go live. Note that
these commands run as the root user.
Executed in the same directory with the new certificate.
'';
};
keyType = mkOption {
type = types.str;
default = if inheritDefaults then defaults.keyType else "ec256";
description = ''
Key type to use for private keys.
For an up to date list of supported values check the --key-type option
at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
'';
};
dnsProvider = mkOption {
type = types.nullOr types.str;
default = if inheritDefaults then defaults.dnsProvider else null;
example = "route53";
description = ''
DNS Challenge provider. For a list of supported providers, see the "code"
field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>.
'';
};
dnsResolver = mkOption {
type = types.nullOr types.str;
default = if inheritDefaults then defaults.dnsResolver else null;
example = "1.1.1.1:53";
description = ''
Set the resolver to use for performing recursive DNS queries. Supported:
host:port. The default is to use the system resolvers, or Google's DNS
resolvers if the system's cannot be determined.
'';
};
credentialsFile = mkOption {
type = types.path;
default = if inheritDefaults then defaults.credentialsFile else null;
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
<link xlink:href="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 = if inheritDefaults then defaults.dnsPropagationCheck else true;
description = ''
Toggles lego DNS propagation check, which is used alongside DNS-01
challenge to ensure the DNS entries required are available.
'';
};
ocspMustStaple = mkOption {
type = types.bool;
default = if inheritDefaults then defaults.ocspMustStaple else false;
description = ''
Turns on the OCSP Must-Staple TLS extension.
Make sure you know what you're doing! See:
<itemizedlist>
<listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem>
<listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem>
</itemizedlist>
'';
};
extraLegoFlags = mkOption {
type = types.listOf types.str;
default = if inheritDefaults then defaults.extraLegoFlags else [];
description = ''
Additional global flags to pass to all lego commands.
'';
};
extraLegoRenewFlags = mkOption {
type = types.listOf types.str;
default = if inheritDefaults then defaults.extraLegoRenewFlags else [];
description = ''
Additional flags to pass to lego renew.
'';
};
extraLegoRunFlags = mkOption {
type = types.listOf types.str;
default = if inheritDefaults then defaults.extraLegoRunFlags else [];
description = ''
Additional flags to pass to lego run.
'';
};
};
certOpts = { name, ... }: { certOpts = { name, ... }: {
options = { options = (inheritableOpts { inherit (cfg) defaults; inheritDefaults = cfg.certs."${name}".inheritDefaults; }) // {
# user option has been removed # user option has been removed
user = mkOption { user = mkOption {
visible = false; visible = false;
@ -443,40 +619,11 @@ let
default = "_mkMergedOptionModule"; default = "_mkMergedOptionModule";
}; };
enableDebugLogs = mkEnableOption "debug logging for this certificate" // { default = cfg.enableDebugLogs; }; directory = mkOption {
type = types.str;
webroot = mkOption { readOnly = true;
type = types.nullOr types.str; default = "/var/lib/acme/${name}";
default = null; description = "Directory where certificate and other state is stored.";
example = "/var/lib/acme/acme-challenge";
description = ''
Where the webroot of the HTTP vhost is located.
<filename>.well-known/acme-challenge/</filename> directory
will be created below the webroot if it doesn't exist.
<literal>http://example.org/.well-known/acme-challenge/</literal> must also
be available (notice unencrypted HTTP).
'';
};
listenHTTP = mkOption {
type = types.nullOr types.str;
default = null;
example = ":1360";
description = ''
Interface and port to listen on to solve HTTP challenges
in the form [INTERFACE]:PORT.
If you use a port other than 80, you must proxy port 80 to this port.
'';
};
server = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
ACME Directory Resource URI. Defaults to Let's Encrypt's
production endpoint,
<link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
'';
}; };
domain = mkOption { domain = mkOption {
@ -485,47 +632,6 @@ let
description = "Domain to fetch certificate for (defaults to the entry name)."; description = "Domain to fetch certificate for (defaults to the entry name).";
}; };
email = mkOption {
type = types.nullOr types.str;
default = cfg.email;
defaultText = literalExpression "config.${opt.email}";
description = "Contact email address for the CA to be able to reach you.";
};
group = mkOption {
type = types.str;
default = "acme";
description = "Group running the ACME client.";
};
reloadServices = mkOption {
type = types.listOf types.str;
default = [];
description = ''
The list of systemd services to call <code>systemctl try-reload-or-restart</code>
on.
'';
};
postRun = mkOption {
type = types.lines;
default = "";
example = "cp full.pem backup.pem";
description = ''
Commands to run after new certificates go live. Note that
these commands run as the root user.
Executed in the same directory with the new certificate.
'';
};
directory = mkOption {
type = types.str;
readOnly = true;
default = "/var/lib/acme/${name}";
description = "Directory where certificate and other state is stored.";
};
extraDomainNames = mkOption { extraDomainNames = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
@ -540,92 +646,25 @@ let
''; '';
}; };
keyType = mkOption { # This setting must be different for each configured certificate, otherwise
type = types.str; # two or more renewals may fail to bind to the address. Hence, it is not in
default = "ec256"; # the inheritableOpts.
description = '' listenHTTP = mkOption {
Key type to use for private keys.
For an up to date list of supported values check the --key-type option
at <link xlink:href="https://go-acme.github.io/lego/usage/cli/#usage"/>.
'';
};
dnsProvider = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
example = "route53"; example = ":1360";
description = '' description = ''
DNS Challenge provider. For a list of supported providers, see the "code" Interface and port to listen on to solve HTTP challenges
field of the DNS providers listed at <link xlink:href="https://go-acme.github.io/lego/dns/"/>. in the form [INTERFACE]:PORT.
If you use a port other than 80, you must proxy port 80 to this port.
''; '';
}; };
dnsResolver = mkOption { inheritDefaults = mkOption {
type = types.nullOr types.str;
default = null;
example = "1.1.1.1:53";
description = ''
Set the resolver to use for performing recursive DNS queries. Supported:
host:port. The default is to use the system resolvers, or Google's DNS
resolvers if the system's cannot be determined.
'';
};
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
<link xlink:href="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; default = true;
description = '' example = true;
Toggles lego DNS propagation check, which is used alongside DNS-01 description = "Whether to inherit values set in `security.acme.defaults` or not.";
challenge to ensure the DNS entries required are available. type = lib.types.bool;
'';
};
ocspMustStaple = mkOption {
type = types.bool;
default = false;
description = ''
Turns on the OCSP Must-Staple TLS extension.
Make sure you know what you're doing! See:
<itemizedlist>
<listitem><para><link xlink:href="https://blog.apnic.net/2019/01/15/is-the-web-ready-for-ocsp-must-staple/" /></para></listitem>
<listitem><para><link xlink:href="https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html" /></para></listitem>
</itemizedlist>
'';
};
extraLegoFlags = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Additional global flags to pass to all lego commands.
'';
};
extraLegoRenewFlags = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Additional flags to pass to lego renew.
'';
};
extraLegoRunFlags = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Additional flags to pass to lego run.
'';
}; };
}; };
}; };
@ -634,41 +673,6 @@ in {
options = { options = {
security.acme = { security.acme = {
enableDebugLogs = mkEnableOption "debug logging for all certificates by default" // { default = true; };
validMinDays = mkOption {
type = types.int;
default = 30;
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 {
type = types.str;
default = "daily";
description = ''
Systemd calendar expression when to check for renewal. See
<citerefentry><refentrytitle>systemd.time</refentrytitle>
<manvolnum>7</manvolnum></citerefentry>.
'';
};
server = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
ACME Directory Resource URI. Defaults to Let's Encrypt's
production endpoint,
<link xlink:href="https://acme-v02.api.letsencrypt.org/directory"/>, if unset.
'';
};
preliminarySelfsigned = mkOption { preliminarySelfsigned = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
@ -691,6 +695,16 @@ in {
''; '';
}; };
defaults = mkOption {
type = types.submodule ({ ... }: { options = inheritableOpts {}; });
description = ''
Default values inheritable by all configured certs. You can
use this to define options shared by all your certs. These defaults
can also be ignored on a per-cert basis using the
`security.acme.certs.''${cert}.inheritDefaults' option.
'';
};
certs = mkOption { certs = mkOption {
default = { }; default = { };
type = with types; attrsOf (submodule certOpts); type = with types; attrsOf (submodule certOpts);
@ -724,12 +738,16 @@ in {
To use the let's encrypt staging server, use security.acme.server = To use the let's encrypt staging server, use security.acme.server =
"https://acme-staging-v02.api.letsencrypt.org/directory". "https://acme-staging-v02.api.letsencrypt.org/directory".
'' '')
)
(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))) (mkChangedOptionModule [ "security" "acme" "validMin" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMin / (24 * 3600)))
(mkChangedOptionModule [ "security" "acme" "validMinDays" ] [ "security" "acme" "defaults" "validMinDays" ] (config: config.security.acme.validMinDays))
(mkChangedOptionModule [ "security" "acme" "renewInterval" ] [ "security" "acme" "defaults" "renewInterval" ] (config: config.security.acme.renewInterval))
(mkChangedOptionModule [ "security" "acme" "email" ] [ "security" "acme" "defaults" "email" ] (config: config.security.acme.email))
(mkChangedOptionModule [ "security" "acme" "server" ] [ "security" "acme" "defaults" "server" ] (config: config.security.acme.server))
(mkChangedOptionModule [ "security" "acme" "enableDebugLogs" ] [ "security" "acme" "defaults" "enableDebugLogs" ] (config: config.security.acme.enableDebugLogs))
]; ];
config = mkMerge [ config = mkMerge [

View File

@ -154,7 +154,7 @@ let
sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey; sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain; sslServerChain = if useACME then "${sslCertDir}/chain.pem" else hostOpts.sslServerChain;
acmeChallenge = optionalString useACME '' acmeChallenge = optionalString (useACME && hostOpts.acmeRoot != null) ''
Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/" Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
<Directory "${hostOpts.acmeRoot}"> <Directory "${hostOpts.acmeRoot}">
AllowOverride None AllowOverride None
@ -677,9 +677,16 @@ in
}; };
security.acme.certs = let security.acme.certs = let
acmePairs = map (hostOpts: nameValuePair hostOpts.hostName { acmePairs = map (hostOpts: let
hasRoot = hostOpts.acmeRoot != null;
in nameValuePair hostOpts.hostName {
group = mkDefault cfg.group; group = mkDefault cfg.group;
webroot = hostOpts.acmeRoot; # if acmeRoot is null inherit config.security.acme
# Since config.security.acme.certs.<cert>.webroot's own default value
# should take precedence set priority higher than mkOptionDefault
webroot = mkOverride (if hasRoot then 1000 else 2000) hostOpts.acmeRoot;
# Also nudge dnsProvider to null in case it is inherited
dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
extraDomainNames = hostOpts.serverAliases; extraDomainNames = hostOpts.serverAliases;
# Use the vhost-specific email address if provided, otherwise let # Use the vhost-specific email address if provided, otherwise let
# security.acme.email or security.acme.certs.<cert>.email be used. # security.acme.email or security.acme.certs.<cert>.email be used.

View File

@ -128,9 +128,12 @@ in
}; };
acmeRoot = mkOption { acmeRoot = mkOption {
type = types.str; type = types.nullOr types.str;
default = "/var/lib/acme/acme-challenge"; default = "/var/lib/acme/acme-challenge";
description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here"; description = ''
Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
Set to null to inherit from config.security.acme.
'';
}; };
sslServerCert = mkOption { sslServerCert = mkOption {

View File

@ -278,7 +278,7 @@ let
acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) '' acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
location /.well-known/acme-challenge { location /.well-known/acme-challenge {
${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"} ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
root ${vhost.acmeRoot}; ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"}
auth_basic off; auth_basic off;
} }
${optionalString (vhost.acmeFallbackHost != null) '' ${optionalString (vhost.acmeFallbackHost != null) ''
@ -948,9 +948,16 @@ in
}; };
security.acme.certs = let security.acme.certs = let
acmePairs = map (vhostConfig: nameValuePair vhostConfig.serverName { acmePairs = map (vhostConfig: let
hasRoot = vhostConfig.acmeRoot != null;
in nameValuePair vhostConfig.serverName {
group = mkDefault cfg.group; group = mkDefault cfg.group;
webroot = vhostConfig.acmeRoot; # if acmeRoot is null inherit config.security.acme
# Since config.security.acme.certs.<cert>.webroot's own default value
# should take precedence set priority higher than mkOptionDefault
webroot = mkOverride (if hasRoot then 1000 else 2000) vhostConfig.acmeRoot;
# Also nudge dnsProvider to null in case it is inherited
dnsProvider = mkOverride (if hasRoot then 1000 else 2000) null;
extraDomainNames = vhostConfig.serverAliases; extraDomainNames = vhostConfig.serverAliases;
# Filter for enableACME-only vhosts. Don't want to create dud certs # Filter for enableACME-only vhosts. Don't want to create dud certs
}) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts); }) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);

View File

@ -3,7 +3,7 @@
# has additional options that affect the web server as a whole, like # has additional options that affect the web server as a whole, like
# the user/group to run under.) # the user/group to run under.)
{ lib, ... }: { config, lib, ... }:
with lib; with lib;
{ {
@ -85,9 +85,12 @@ with lib;
}; };
acmeRoot = mkOption { acmeRoot = mkOption {
type = types.str; type = types.nullOr types.str;
default = "/var/lib/acme/acme-challenge"; default = "/var/lib/acme/acme-challenge";
description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here"; description = ''
Directory for the acme challenge which is PUBLIC, don't put certs or keys in here.
Set to null to inherit from config.security.acme.
'';
}; };
acmeFallbackHost = mkOption { acmeFallbackHost = mkOption {

View File

@ -1,9 +1,9 @@
let import ./make-test-python.nix ({ pkgs, lib, ... }: let
commonConfig = ./common/acme/client; commonConfig = ./common/acme/client;
dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress; dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress;
dnsScript = {pkgs, nodes}: let dnsScript = nodes: let
dnsAddress = dnsServerIP nodes; dnsAddress = dnsServerIP nodes;
in pkgs.writeShellScript "dns-hook.sh" '' in pkgs.writeShellScript "dns-hook.sh" ''
set -euo pipefail set -euo pipefail
@ -15,30 +15,137 @@ let
fi fi
''; '';
documentRoot = pkgs: pkgs.runCommand "docroot" {} '' dnsConfig = nodes: {
dnsProvider = "exec";
dnsPropagationCheck = false;
credentialsFile = pkgs.writeText "wildcard.env" ''
EXEC_PATH=${dnsScript nodes}
EXEC_POLLING_INTERVAL=1
EXEC_PROPAGATION_TIMEOUT=1
EXEC_SEQUENCE_INTERVAL=1
'';
};
documentRoot = pkgs.runCommand "docroot" {} ''
mkdir -p "$out" mkdir -p "$out"
echo hello world > "$out/index.html" echo hello world > "$out/index.html"
''; '';
vhostBase = pkgs: { vhostBase = {
forceSSL = true; forceSSL = true;
locations."/".root = documentRoot pkgs; locations."/".root = documentRoot;
}; };
in import ./make-test-python.nix ({ lib, ... }: { vhostBaseHttpd = {
forceSSL = true;
inherit documentRoot;
};
# Base specialisation config for testing general ACME features
webserverBasicConfig = {
services.nginx.enable = true;
services.nginx.virtualHosts."a.example.test" = vhostBase // {
enableACME = true;
};
};
# Generate specialisations for testing a web server
mkServerConfigs = { server, group, vhostBaseData, extraConfig ? {} }: let
baseConfig = { nodes, config, specialConfig ? {} }: lib.mkMerge [
{
security.acme = {
defaults = (dnsConfig nodes) // {
inherit group;
};
# One manual wildcard cert
certs."example.test" = {
domain = "*.example.test";
};
};
services."${server}" = {
enable = true;
virtualHosts = {
# Run-of-the-mill vhost using HTTP-01 validation
"${server}-http.example.test" = vhostBaseData // {
serverAliases = [ "${server}-http-alias.example.test" ];
enableACME = true;
};
# Another which inherits the DNS-01 config
"${server}-dns.example.test" = vhostBaseData // {
serverAliases = [ "${server}-dns-alias.example.test" ];
enableACME = true;
# Set acmeRoot to null instead of using the default of "/var/lib/acme/acme-challenge"
# webroot + dnsProvider are mutually exclusive.
acmeRoot = null;
};
# One using the wildcard certificate
"${server}-wildcard.example.test" = vhostBaseData // {
serverAliases = [ "${server}-wildcard-alias.example.test" ];
useACMEHost = "example.test";
};
};
};
# Used to determine if service reload was triggered
systemd.targets."test-renew-${server}" = {
wants = [ "acme-${server}-http.example.test.service" ];
after = [ "acme-${server}-http.example.test.service" "${server}-config-reload.service" ];
};
}
specialConfig
extraConfig
];
in {
"${server}".configuration = { nodes, config, ... }: baseConfig {
inherit nodes config;
};
# Test that server reloads when an alias is removed (and subsequently test removal works in acme)
"${server}-remove-alias".configuration = { nodes, config, ... }: baseConfig {
inherit nodes config;
specialConfig = {
# Remove an alias, but create a standalone vhost in its place for testing.
# This configuration results in certificate errors as useACMEHost does not imply
# append extraDomains, and thus we can validate the SAN is removed.
services."${server}" = {
virtualHosts."${server}-http.example.test".serverAliases = lib.mkForce [];
virtualHosts."${server}-http-alias.example.test" = vhostBaseData // {
useACMEHost = "${server}-http.example.test";
};
};
};
};
# Test that the server reloads when only the acme configuration is changed.
"${server}-change-acme-conf".configuration = { nodes, config, ... }: baseConfig {
inherit nodes config;
specialConfig = {
security.acme.certs."${server}-http.example.test" = {
keyType = "ec384";
# Also test that postRun is exec'd as root
postRun = "id | grep root";
};
};
};
};
in {
name = "acme"; name = "acme";
meta.maintainers = lib.teams.acme.members; meta.maintainers = lib.teams.acme.members;
nodes = { nodes = {
# The fake ACME server which will respond to client requests # The fake ACME server which will respond to client requests
acme = { nodes, lib, ... }: { acme = { nodes, ... }: {
imports = [ ./common/acme/server ]; imports = [ ./common/acme/server ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
}; };
# A fake DNS server which can be configured with records as desired # A fake DNS server which can be configured with records as desired
# Used to test DNS-01 challenge # Used to test DNS-01 challenge
dnsserver = { nodes, pkgs, ... }: { dnsserver = { nodes, ... }: {
networking.firewall.allowedTCPPorts = [ 8055 53 ]; networking.firewall.allowedTCPPorts = [ 8055 53 ];
networking.firewall.allowedUDPPorts = [ 53 ]; networking.firewall.allowedUDPPorts = [ 53 ];
systemd.services.pebble-challtestsrv = { systemd.services.pebble-challtestsrv = {
@ -54,7 +161,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
}; };
# A web server which will be the node requesting certs # A web server which will be the node requesting certs
webserver = { pkgs, nodes, lib, config, ... }: { webserver = { nodes, config, ... }: {
imports = [ commonConfig ]; imports = [ commonConfig ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.firewall.allowedTCPPorts = [ 80 443 ];
@ -63,138 +170,88 @@ in import ./make-test-python.nix ({ lib, ... }: {
environment.systemPackages = [ pkgs.openssl ]; environment.systemPackages = [ pkgs.openssl ];
# Set log level to info so that we can see when the service is reloaded # Set log level to info so that we can see when the service is reloaded
services.nginx.enable = true;
services.nginx.logError = "stderr info"; services.nginx.logError = "stderr info";
# First tests configure a basic cert and run a bunch of openssl checks specialisation = {
services.nginx.virtualHosts."a.example.test" = (vhostBase pkgs) // { # First derivation used to test general ACME features
enableACME = true; general.configuration = { ... }: let
}; caDomain = nodes.acme.config.test-support.acme.caDomain;
email = config.security.acme.defaults.email;
# Used to determine if service reload was triggered # Exit 99 to make it easier to track if this is the reason a renew failed
systemd.targets.test-renew-nginx = { accountCreateTester = ''
wants = [ "acme-a.example.test.service" ]; test -e accounts/${caDomain}/${email}/account.json || exit 99
after = [ "acme-a.example.test.service" "nginx-config-reload.service" ];
};
# Test that account creation is collated into one service
specialisation.account-creation.configuration = { nodes, pkgs, lib, ... }: let
email = "newhostmaster@example.test";
caDomain = nodes.acme.config.test-support.acme.caDomain;
# Exit 99 to make it easier to track if this is the reason a renew failed
testScript = ''
test -e accounts/${caDomain}/${email}/account.json || exit 99
'';
in {
security.acme.email = lib.mkForce email;
systemd.services."b.example.test".preStart = testScript;
systemd.services."c.example.test".preStart = testScript;
services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
enableACME = true;
};
services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
enableACME = true;
};
};
# Cert config changes will not cause the nginx configuration to change.
# This tests that the reload service is correctly triggered.
# It also tests that postRun is exec'd as root
specialisation.cert-change.configuration = { pkgs, ... }: {
security.acme.certs."a.example.test".keyType = "ec384";
security.acme.certs."a.example.test".postRun = ''
set -euo pipefail
touch /home/test
chown root:root /home/test
echo testing > /home/test
'';
};
# Now adding an alias to ensure that the certs are updated
specialisation.nginx-aliases.configuration = { pkgs, ... }: {
services.nginx.virtualHosts."a.example.test" = (vhostBase pkgs) // {
serverAliases = [ "b.example.test" ];
};
};
# Must be run after nginx-aliases
specialisation.remove-extra-domain.configuration = { pkgs, ... } : {
# This also validates that useACMEHost doesn't unexpectedly add the domain.
services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
useACMEHost = "a.example.test";
};
};
# Test OCSP Stapling
specialisation.ocsp-stapling.configuration = { pkgs, ... }: {
security.acme.certs."a.example.test" = {
ocspMustStaple = true;
};
services.nginx.virtualHosts."a.example.com" = {
extraConfig = ''
ssl_stapling on;
ssl_stapling_verify on;
''; '';
}; in lib.mkMerge [
}; webserverBasicConfig
{
# Used to test that account creation is collated into one service.
# These should not run until after acme-finished-a.example.test.target
systemd.services."b.example.test".preStart = accountCreateTester;
systemd.services."c.example.test".preStart = accountCreateTester;
# Test using Apache HTTPD services.nginx.virtualHosts."b.example.test" = vhostBase // {
specialisation.httpd-aliases.configuration = { pkgs, config, lib, ... }: { enableACME = true;
services.nginx.enable = lib.mkForce false; };
services.httpd.enable = true; services.nginx.virtualHosts."c.example.test" = vhostBase // {
services.httpd.adminAddr = config.security.acme.email; enableACME = true;
services.httpd.virtualHosts."c.example.test" = { };
serverAliases = [ "d.example.test" ]; }
forceSSL = true; ];
enableACME = true;
documentRoot = documentRoot pkgs;
};
# Used to determine if service reload was triggered # Test OCSP Stapling
systemd.targets.test-renew-httpd = { ocsp-stapling.configuration = { ... }: lib.mkMerge [
wants = [ "acme-c.example.test.service" ]; webserverBasicConfig
after = [ "acme-c.example.test.service" "httpd-config-reload.service" ]; {
}; security.acme.certs."a.example.test".ocspMustStaple = true;
}; services.nginx.virtualHosts."a.example.com" = {
extraConfig = ''
ssl_stapling on;
ssl_stapling_verify on;
'';
};
}
];
# Validation via DNS-01 challenge # Validate service relationships by adding a slow start service to nginx' wants.
specialisation.dns-01.configuration = { pkgs, config, nodes, ... }: { # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
security.acme.certs."example.test" = { slow-startup.configuration = { ... }: lib.mkMerge [
domain = "*.example.test"; webserverBasicConfig
group = config.services.nginx.group; {
dnsProvider = "exec"; systemd.services.my-slow-service = {
dnsPropagationCheck = false; wantedBy = [ "multi-user.target" "nginx.service" ];
credentialsFile = pkgs.writeText "wildcard.env" '' before = [ "nginx.service" ];
EXEC_PATH=${dnsScript { inherit pkgs nodes; }} preStart = "sleep 5";
''; script = "${pkgs.python3}/bin/python -m http.server";
}; };
services.nginx.virtualHosts."dns.example.test" = (vhostBase pkgs) // { services.nginx.virtualHosts."slow.example.com" = {
useACMEHost = "example.test"; forceSSL = true;
}; enableACME = true;
}; locations."/".proxyPass = "http://localhost:8000";
};
}
];
# Validate service relationships by adding a slow start service to nginx' wants. # Test compatibility with Nginx
# Reproducer for https://github.com/NixOS/nixpkgs/issues/81842 } // (mkServerConfigs {
specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ... }: { server = "nginx";
systemd.services.my-slow-service = { group = "nginx";
wantedBy = [ "multi-user.target" "nginx.service" ]; vhostBaseData = vhostBase;
before = [ "nginx.service" ]; })
preStart = "sleep 5";
script = "${pkgs.python3}/bin/python -m http.server";
};
services.nginx.virtualHosts."slow.example.com" = { # Test compatibility with Apache HTTPD
forceSSL = true; // (mkServerConfigs {
enableACME = true; server = "httpd";
locations."/".proxyPass = "http://localhost:8000"; group = "wwwrun";
}; vhostBaseData = vhostBaseHttpd;
}; extraConfig = {
services.httpd.adminAddr = config.security.acme.defaults.email;
};
});
}; };
# The client will be used to curl the webserver to validate configuration # The client will be used to curl the webserver to validate configuration
client = {nodes, lib, pkgs, ...}: { client = { nodes, ... }: {
imports = [ commonConfig ]; imports = [ commonConfig ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ]; networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
@ -203,7 +260,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
}; };
}; };
testScript = {nodes, ...}: testScript = { nodes, ... }:
let let
caDomain = nodes.acme.config.test-support.acme.caDomain; caDomain = nodes.acme.config.test-support.acme.caDomain;
newServerSystem = nodes.webserver.config.system.build.toplevel; newServerSystem = nodes.webserver.config.system.build.toplevel;
@ -212,23 +269,26 @@ in import ./make-test-python.nix ({ lib, ... }: {
# 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,
# this is because a oneshot goes from inactive => activating => inactive, and never # this is because a oneshot goes from inactive => activating => inactive, and never
# reaches the active state. Targets do not have this issue. # reaches the active state. Targets do not have this issue.
'' ''
import time import time
has_switched = False
def switch_to(node, name): def switch_to(node, name):
global has_switched # On first switch, this will create a symlink to the current system so that we can
if has_switched: # quickly switch between derivations
node.succeed( root_specs = "/tmp/specialisation"
"${switchToNewServer}" node.execute(
) f"test -e {root_specs}"
has_switched = True f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}"
)
switcher_path = f"/run/current-system/specialisation/{name}/bin/switch-to-configuration"
rc, _ = node.execute(f"test -e '{switcher_path}'")
if rc > 0:
switcher_path = f"/tmp/specialisation/{name}/bin/switch-to-configuration"
node.succeed( node.succeed(
f"/run/current-system/specialisation/{name}/bin/switch-to-configuration test" f"{switcher_path} test"
) )
@ -318,8 +378,7 @@ in import ./make-test-python.nix ({ lib, ... }: {
return download_ca_certs(node, retries - 1) return download_ca_certs(node, retries - 1)
client.start() start_all()
dnsserver.start()
dnsserver.wait_for_unit("pebble-challtestsrv.service") dnsserver.wait_for_unit("pebble-challtestsrv.service")
client.wait_for_unit("default.target") client.wait_for_unit("default.target")
@ -328,19 +387,30 @@ in import ./make-test-python.nix ({ lib, ... }: {
'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a' 'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
) )
acme.start()
webserver.start()
acme.wait_for_unit("network-online.target") acme.wait_for_unit("network-online.target")
acme.wait_for_unit("pebble.service") acme.wait_for_unit("pebble.service")
download_ca_certs(client) download_ca_certs(client)
# Perform general tests first
switch_to(webserver, "general")
with subtest("Can request certificate with HTTPS-01 challenge"): with subtest("Can request certificate with HTTPS-01 challenge"):
webserver.wait_for_unit("acme-finished-a.example.test.target") webserver.wait_for_unit("acme-finished-a.example.test.target")
check_fullchain(webserver, "a.example.test")
check_issuer(webserver, "a.example.test", "pebble")
webserver.wait_for_unit("nginx.service")
check_connection(client, "a.example.test")
with subtest("Runs 1 cert for account creation before others"):
webserver.wait_for_unit("acme-finished-b.example.test.target")
webserver.wait_for_unit("acme-finished-c.example.test.target")
check_connection(client, "b.example.test")
check_connection(client, "c.example.test")
with subtest("Certificates and accounts have safe + valid permissions"): with subtest("Certificates and accounts have safe + valid permissions"):
group = "${nodes.webserver.config.security.acme.certs."a.example.test".group}" # Nginx will set the group appropriately when enableACME is used
group = "nginx"
webserver.succeed( webserver.succeed(
f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5" f"test $(stat -L -c '%a %U %G' /var/lib/acme/a.example.test/*.pem | tee /dev/stderr | grep '640 acme {group}' | wc -l) -eq 5"
) )
@ -354,12 +424,6 @@ in import ./make-test-python.nix ({ lib, ... }: {
f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c '%a %U %G' {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0" f"test $(find /var/lib/acme/accounts -type f -exec stat -L -c '%a %U %G' {{}} \\; | tee /dev/stderr | grep -v '600 acme {group}' | wc -l) -eq 0"
) )
with subtest("Certs are accepted by web server"):
webserver.succeed("systemctl start nginx.service")
check_fullchain(webserver, "a.example.test")
check_issuer(webserver, "a.example.test", "pebble")
check_connection(client, "a.example.test")
# Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal # Selfsigned certs tests happen late so we aren't fighting the system init triggering cert renewal
with subtest("Can generate valid selfsigned certs"): with subtest("Can generate valid selfsigned certs"):
webserver.succeed("systemctl clean acme-a.example.test.service --what=state") webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
@ -373,89 +437,80 @@ in import ./make-test-python.nix ({ lib, ... }: {
# Will succeed if nginx can load the certs # Will succeed if nginx can load the certs
webserver.succeed("systemctl start nginx-config-reload.service") webserver.succeed("systemctl start nginx-config-reload.service")
with subtest("Can reload nginx when timer triggers renewal"):
webserver.succeed("systemctl start test-renew-nginx.target")
check_issuer(webserver, "a.example.test", "pebble")
check_connection(client, "a.example.test")
with subtest("Runs 1 cert for account creation before others"):
switch_to(webserver, "account-creation")
webserver.wait_for_unit("acme-finished-a.example.test.target")
check_connection(client, "a.example.test")
webserver.wait_for_unit("acme-finished-b.example.test.target")
webserver.wait_for_unit("acme-finished-c.example.test.target")
check_connection(client, "b.example.test")
check_connection(client, "c.example.test")
with subtest("Can reload web server when cert configuration changes"):
switch_to(webserver, "cert-change")
webserver.wait_for_unit("acme-finished-a.example.test.target")
check_connection_key_bits(client, "a.example.test", "384")
webserver.succeed("grep testing /home/test")
# Clean to remove the testing file (and anything else messy we did)
webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
with subtest("Correctly implements OCSP stapling"): with subtest("Correctly implements OCSP stapling"):
switch_to(webserver, "ocsp-stapling") switch_to(webserver, "ocsp-stapling")
webserver.wait_for_unit("acme-finished-a.example.test.target") webserver.wait_for_unit("acme-finished-a.example.test.target")
check_stapling(client, "a.example.test") check_stapling(client, "a.example.test")
with subtest("Can request certificate with HTTPS-01 when nginx startup is delayed"): with subtest("Can request certificate with HTTPS-01 when nginx startup is delayed"):
webserver.execute("systemctl stop nginx")
switch_to(webserver, "slow-startup") switch_to(webserver, "slow-startup")
webserver.wait_for_unit("acme-finished-slow.example.com.target") webserver.wait_for_unit("acme-finished-slow.example.com.target")
check_issuer(webserver, "slow.example.com", "pebble") check_issuer(webserver, "slow.example.com", "pebble")
webserver.wait_for_unit("nginx.service")
check_connection(client, "slow.example.com") check_connection(client, "slow.example.com")
with subtest("Can request certificate for vhost + aliases (nginx)"): domains = ["http", "dns", "wildcard"]
# Check the key hash before and after adding an alias. It should not change. for server, logsrc in [
# The previous test reverts the ed384 change ("nginx", "journalctl -n 30 -u nginx.service"),
webserver.wait_for_unit("acme-finished-a.example.test.target") ("httpd", "tail -n 30 /var/log/httpd/*.log"),
switch_to(webserver, "nginx-aliases") ]:
webserver.wait_for_unit("acme-finished-a.example.test.target") wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service")
check_issuer(webserver, "a.example.test", "pebble") with subtest(f"Works with {server}"):
check_connection(client, "a.example.test") try:
check_connection(client, "b.example.test") switch_to(webserver, server)
# Skip wildcard domain for this check ([:-1])
for domain in domains[:-1]:
webserver.wait_for_unit(
f"acme-finished-{server}-{domain}.example.test.target"
)
except Exception as err:
_, output = webserver.execute(
f"{logsrc} && ls -al /var/lib/acme/acme-challenge"
)
print(output)
raise err
with subtest("Can remove extra domains from a cert"): wait_for_server()
switch_to(webserver, "remove-extra-domain")
webserver.wait_for_unit("acme-finished-a.example.test.target")
webserver.wait_for_unit("nginx.service")
check_connection(client, "a.example.test")
rc, _ = client.execute(
"openssl s_client -CAfile /tmp/ca.crt -connect b.example.test:443"
" </dev/null 2>/dev/null | openssl x509 -noout -text"
" | grep DNS: | grep b.example.test"
)
assert rc > 0, "Removed extraDomainName was not removed from the cert"
with subtest("Can request certificates for vhost + aliases (apache-httpd)"): for domain in domains[:-1]:
try: check_issuer(webserver, f"{server}-{domain}.example.test", "pebble")
switch_to(webserver, "httpd-aliases") for domain in domains:
webserver.wait_for_unit("acme-finished-c.example.test.target") check_connection(client, f"{server}-{domain}.example.test")
except Exception as err: check_connection(client, f"{server}-{domain}-alias.example.test")
_, output = webserver.execute(
"cat /var/log/httpd/*.log && ls -al /var/lib/acme/acme-challenge" test_domain = f"{server}-{domains[0]}.example.test"
with subtest(f"Can reload {server} when timer triggers renewal"):
# Switch to selfsigned first
webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state")
webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service")
check_issuer(webserver, test_domain, "minica")
webserver.succeed(f"systemctl start {server}-config-reload.service")
webserver.succeed(f"systemctl start test-renew-{server}.target")
check_issuer(webserver, test_domain, "pebble")
check_connection(client, test_domain)
with subtest("Can remove an alias from a domain + cert is updated"):
test_alias = f"{server}-{domains[0]}-alias.example.test"
switch_to(webserver, f"{server}-remove-alias")
webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
wait_for_server()
check_connection(client, test_domain)
rc, _ = client.execute(
f"openssl s_client -CAfile /tmp/ca.crt -connect {test_alias}:443"
" </dev/null 2>/dev/null | openssl x509 -noout -text"
f" | grep DNS: | grep {test_alias}"
) )
print(output) assert rc > 0, "Removed extraDomainName was not removed from the cert"
raise err
check_issuer(webserver, "c.example.test", "pebble")
check_connection(client, "c.example.test")
check_connection(client, "d.example.test")
with subtest("Can reload httpd when timer triggers renewal"): with subtest("security.acme changes reflect on web server"):
# Switch to selfsigned first # Switch back to normal server config first, reset everything.
webserver.succeed("systemctl clean acme-c.example.test.service --what=state") switch_to(webserver, server)
webserver.succeed("systemctl start acme-selfsigned-c.example.test.service") wait_for_server()
check_issuer(webserver, "c.example.test", "minica") switch_to(webserver, f"{server}-change-acme-conf")
webserver.succeed("systemctl start httpd-config-reload.service") webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
webserver.succeed("systemctl start test-renew-httpd.target") wait_for_server()
check_issuer(webserver, "c.example.test", "pebble") check_connection_key_bits(client, test_domain, "384")
check_connection(client, "c.example.test")
with subtest("Can request wildcard certificates using DNS-01 challenge"):
switch_to(webserver, "dns-01")
webserver.wait_for_unit("acme-finished-example.test.target")
check_issuer(webserver, "example.test", "pebble")
check_connection(client, "dns.example.test")
''; '';
}) })

View File

@ -5,9 +5,11 @@ let
in { in {
security.acme = { security.acme = {
server = "https://${caDomain}/dir";
email = "hostmaster@example.test";
acceptTerms = true; acceptTerms = true;
defaults = {
server = "https://${caDomain}/dir";
email = "hostmaster@example.test";
};
}; };
security.pki.certificateFiles = [ caCert ]; security.pki.certificateFiles = [ caCert ];

View File

@ -120,6 +120,11 @@ in {
enable = true; enable = true;
description = "Pebble ACME server"; description = "Pebble ACME server";
wantedBy = [ "network.target" ]; wantedBy = [ "network.target" ];
environment = {
# We're not testing lego, we're just testing our configuration.
# No need to sleep.
PEBBLE_VA_NOSLEEP = "1";
};
serviceConfig = { serviceConfig = {
RuntimeDirectory = "pebble"; RuntimeDirectory = "pebble";