Merge pull request #147784 from m1cr0man/acme
This commit is contained in:
commit
99e8065d4c
@ -14,7 +14,17 @@
|
|||||||
</itemizedlist>
|
</itemizedlist>
|
||||||
<section xml:id="sec-release-22.05-highlights">
|
<section xml:id="sec-release-22.05-highlights">
|
||||||
<title>Highlights</title>
|
<title>Highlights</title>
|
||||||
<itemizedlist spacing="compact">
|
<itemizedlist>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
<literal>security.acme.defaults</literal> has been added to
|
||||||
|
simplify configuring settings for many certificates at once.
|
||||||
|
This also opens up the the option to use DNS-01 validation
|
||||||
|
when using <literal>enableACME</literal> on web server virtual
|
||||||
|
hosts (e.g.
|
||||||
|
<literal>services.nginx.virtualHosts.*.enableACME</literal>).
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
PHP 8.1 is now available
|
PHP 8.1 is now available
|
||||||
@ -189,6 +199,20 @@
|
|||||||
using this default will print a warning when rebuilt.
|
using this default will print a warning when rebuilt.
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
<literal>security.acme</literal> certificates will now
|
||||||
|
correctly check for CA revokation before reaching their
|
||||||
|
minimum age.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
|
<listitem>
|
||||||
|
<para>
|
||||||
|
Removing domains from
|
||||||
|
<literal>security.acme.certs._name_.extraDomainNames</literal>
|
||||||
|
will now correctly remove those domains during rebuild/renew.
|
||||||
|
</para>
|
||||||
|
</listitem>
|
||||||
<listitem>
|
<listitem>
|
||||||
<para>
|
<para>
|
||||||
The option
|
The option
|
||||||
|
@ -6,6 +6,11 @@ In addition to numerous new and upgraded packages, this release has the followin
|
|||||||
|
|
||||||
## Highlights {#sec-release-22.05-highlights}
|
## Highlights {#sec-release-22.05-highlights}
|
||||||
|
|
||||||
|
- `security.acme.defaults` has been added to simplify configuring
|
||||||
|
settings for many certificates at once. This also opens up the
|
||||||
|
the option to use DNS-01 validation when using `enableACME` on
|
||||||
|
web server virtual hosts (e.g. `services.nginx.virtualHosts.*.enableACME`).
|
||||||
|
|
||||||
- PHP 8.1 is now available
|
- PHP 8.1 is now available
|
||||||
|
|
||||||
## New Services {#sec-release-22.05-new-services}
|
## New Services {#sec-release-22.05-new-services}
|
||||||
@ -75,6 +80,12 @@ In addition to numerous new and upgraded packages, this release has the followin
|
|||||||
- The `services.unifi.openPorts` option default value of `true` is now deprecated and will be changed to `false` in 22.11.
|
- The `services.unifi.openPorts` option default value of `true` is now deprecated and will be changed to `false` in 22.11.
|
||||||
Configurations using this default will print a warning when rebuilt.
|
Configurations using this default will print a warning when rebuilt.
|
||||||
|
|
||||||
|
- `security.acme` certificates will now correctly check for CA
|
||||||
|
revokation before reaching their minimum age.
|
||||||
|
|
||||||
|
- Removing domains from `security.acme.certs._name_.extraDomainNames`
|
||||||
|
will now correctly remove those domains during rebuild/renew.
|
||||||
|
|
||||||
- The option
|
- The option
|
||||||
[services.ssh.enableAskPassword](#opt-services.ssh.enableAskPassword) was
|
[services.ssh.enableAskPassword](#opt-services.ssh.enableAskPassword) was
|
||||||
added, decoupling the setting of `SSH_ASKPASS` from
|
added, decoupling the setting of `SSH_ASKPASS` from
|
||||||
|
@ -3,6 +3,7 @@ with lib;
|
|||||||
let
|
let
|
||||||
cfg = config.security.acme;
|
cfg = config.security.acme;
|
||||||
opt = options.security.acme;
|
opt = options.security.acme;
|
||||||
|
user = if cfg.useRoot then "root" else "acme";
|
||||||
|
|
||||||
# Used to calculate timer accuracy for coalescing
|
# Used to calculate timer accuracy for coalescing
|
||||||
numCerts = length (builtins.attrNames cfg.certs);
|
numCerts = length (builtins.attrNames cfg.certs);
|
||||||
@ -23,7 +24,7 @@ let
|
|||||||
# security.acme.certs.<cert>.group on some of the services.
|
# security.acme.certs.<cert>.group on some of the services.
|
||||||
commonServiceConfig = {
|
commonServiceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
User = "acme";
|
User = user;
|
||||||
Group = mkDefault "acme";
|
Group = mkDefault "acme";
|
||||||
UMask = 0022;
|
UMask = 0022;
|
||||||
StateDirectoryMode = 750;
|
StateDirectoryMode = 750;
|
||||||
@ -101,12 +102,12 @@ let
|
|||||||
# is configurable on a per-cert basis.
|
# is configurable on a per-cert basis.
|
||||||
userMigrationService = let
|
userMigrationService = let
|
||||||
script = with builtins; ''
|
script = with builtins; ''
|
||||||
chown -R acme .lego/accounts
|
chown -R ${user} .lego/accounts
|
||||||
'' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
|
'' + (concatStringsSep "\n" (mapAttrsToList (cert: data: ''
|
||||||
for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
|
for fixpath in ${escapeShellArg cert} .lego/${escapeShellArg cert}; do
|
||||||
if [ -d "$fixpath" ]; then
|
if [ -d "$fixpath" ]; then
|
||||||
chmod -R u=rwX,g=rX,o= "$fixpath"
|
chmod -R u=rwX,g=rX,o= "$fixpath"
|
||||||
chown -R acme:${data.group} "$fixpath"
|
chown -R ${user}:${data.group} "$fixpath"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
'') certConfigs));
|
'') certConfigs));
|
||||||
@ -128,7 +129,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" ];
|
||||||
@ -156,6 +157,7 @@ let
|
|||||||
${toString data.ocspMustStaple} ${data.keyType}
|
${toString data.ocspMustStaple} ${data.keyType}
|
||||||
'';
|
'';
|
||||||
certDir = mkHash hashData;
|
certDir = mkHash hashData;
|
||||||
|
# TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
|
||||||
domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
|
domainHash = mkHash "${concatStringsSep " " extraDomains} ${data.domain}";
|
||||||
accountHash = (mkAccountHash acmeServer data);
|
accountHash = (mkAccountHash acmeServer data);
|
||||||
accountDir = accountDirRoot + accountHash;
|
accountDir = accountDirRoot + accountHash;
|
||||||
@ -210,7 +212,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";
|
||||||
|
|
||||||
@ -267,7 +269,7 @@ let
|
|||||||
cat key.pem fullchain.pem > full.pem
|
cat key.pem fullchain.pem > full.pem
|
||||||
|
|
||||||
# Group might change between runs, re-apply it
|
# Group might change between runs, re-apply it
|
||||||
chown 'acme:${data.group}' *
|
chown '${user}:${data.group}' *
|
||||||
|
|
||||||
# Default permissions make the files unreadable by group + anon
|
# Default permissions make the files unreadable by group + anon
|
||||||
# Need to be readable by group
|
# Need to be readable by group
|
||||||
@ -322,7 +324,7 @@ let
|
|||||||
fi
|
fi
|
||||||
'');
|
'');
|
||||||
} // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) {
|
} // optionalAttrs (data.listenHTTP != null && toInt (elemAt (splitString ":" data.listenHTTP) 1) < 1024) {
|
||||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Working directory will be /tmp
|
# Working directory will be /tmp
|
||||||
@ -355,7 +357,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) ''
|
||||||
@ -372,37 +374,40 @@ let
|
|||||||
|
|
||||||
echo '${domainHash}' > domainhash.txt
|
echo '${domainHash}' > domainhash.txt
|
||||||
|
|
||||||
# Check if we can renew
|
# Check if we can renew.
|
||||||
if [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
|
# We can only renew if the list of domains has not changed.
|
||||||
|
if cmp -s domainhash.txt certificates/domainhash.txt && [ -e 'certificates/${keyName}.key' -a -e 'certificates/${keyName}.crt' -a -n "$(ls -1 accounts)" ]; then
|
||||||
|
|
||||||
# When domains are updated, there's no need to do a full
|
# Even if a cert is not expired, it may be revoked by the CA.
|
||||||
# Lego run, but it's likely renew won't work if days is too low.
|
# Try to renew, and silently fail if the cert is not expired.
|
||||||
if [ -e certificates/domainhash.txt ] && cmp -s domainhash.txt certificates/domainhash.txt; then
|
# Avoids #85794 and resolves #129838
|
||||||
|
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: skipping 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
|
||||||
echo 1>&2 "nixos-acme: renewing now, because certificate expires within the configured ${toString cfg.validMinDays} days"
|
# High number to avoid Systemd reserved codes.
|
||||||
lego ${renewOpts} --days ${toString cfg.validMinDays}
|
exit 11
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
echo 1>&2 "certificate domain(s) have changed; will renew now"
|
|
||||||
# Any number > 90 works, but this one is over 9000 ;-)
|
|
||||||
lego ${renewOpts} --days 9001
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Otherwise do a full run
|
# Otherwise do a full run
|
||||||
else
|
elif ! lego ${runOpts}; then
|
||||||
lego ${runOpts}
|
# Produce a nice error for those doing their first nixos-rebuild with these certs
|
||||||
|
echo Failed to fetch certificates. \
|
||||||
|
This may mean your DNS records are set up incorrectly. \
|
||||||
|
${optionalString (cfg.preliminarySelfsigned) "Selfsigned certs are in place and dependant services will still start."}
|
||||||
|
# Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
|
||||||
|
# High number to avoid Systemd reserved codes.
|
||||||
|
exit 10
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv domainhash.txt certificates/
|
mv domainhash.txt certificates/
|
||||||
|
|
||||||
# Group might change between runs, re-apply it
|
# Group might change between runs, re-apply it
|
||||||
chown 'acme:${data.group}' certificates/*
|
chown '${user}:${data.group}' certificates/*
|
||||||
|
|
||||||
# Copy all certs to the "real" certs directory
|
# Copy all certs to the "real" certs directory
|
||||||
CERT='certificates/${keyName}.crt'
|
if ! cmp -s 'certificates/${keyName}.crt' out/fullchain.pem; then
|
||||||
if [ -e "$CERT" ] && ! cmp -s "$CERT" out/fullchain.pem; then
|
|
||||||
touch out/renewed
|
touch out/renewed
|
||||||
echo Installing new certificate
|
echo Installing new certificate
|
||||||
cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
|
cp -vp 'certificates/${keyName}.crt' out/fullchain.pem
|
||||||
@ -421,7 +426,194 @@ let
|
|||||||
|
|
||||||
certConfigs = mapAttrs certToConfig cfg.certs;
|
certConfigs = mapAttrs certToConfig cfg.certs;
|
||||||
|
|
||||||
certOpts = { name, ... }: {
|
# These options can be specified within
|
||||||
|
# security.acme.defaults or security.acme.certs.<name>
|
||||||
|
inheritableModule = isDefaults: { config, ... }: let
|
||||||
|
defaultAndText = name: default: {
|
||||||
|
# When ! isDefaults then this is the option declaration for the
|
||||||
|
# security.acme.certs.<name> path, which has the extra inheritDefaults
|
||||||
|
# option, which if disabled means that we can't inherit it
|
||||||
|
default = if isDefaults || ! config.inheritDefaults then default else cfg.defaults.${name};
|
||||||
|
# The docs however don't need to depend on inheritDefaults, they should
|
||||||
|
# stay constant. Though notably it wouldn't matter much, because to get
|
||||||
|
# the option information, a submodule with name `<name>` is evaluated
|
||||||
|
# without any definitions.
|
||||||
|
defaultText = if isDefaults then default else literalExpression "config.security.acme.defaults.${name}";
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
options = {
|
||||||
|
validMinDays = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
inherit (defaultAndText "validMinDays" 30) default defaultText;
|
||||||
|
description = "Minimum remaining validity before renewal in days.";
|
||||||
|
};
|
||||||
|
|
||||||
|
renewInterval = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
inherit (defaultAndText "renewInterval" "daily") default defaultText;
|
||||||
|
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" // {
|
||||||
|
inherit (defaultAndText "enableDebugLogs" true) default defaultText;
|
||||||
|
};
|
||||||
|
|
||||||
|
webroot = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
inherit (defaultAndText "webroot" null) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "server" null) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "email" null) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "group" "acme") default defaultText;
|
||||||
|
description = "Group running the ACME client.";
|
||||||
|
};
|
||||||
|
|
||||||
|
reloadServices = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
inherit (defaultAndText "reloadServices" []) default defaultText;
|
||||||
|
description = ''
|
||||||
|
The list of systemd services to call <code>systemctl try-reload-or-restart</code>
|
||||||
|
on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
postRun = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
inherit (defaultAndText "postRun" "") default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "keyType" "ec256") default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "dnsProvider" null) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "dnsResolver" null) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "credentialsFile" null) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "dnsPropagationCheck" true) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "ocspMustStaple" false) default defaultText;
|
||||||
|
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;
|
||||||
|
inherit (defaultAndText "extraLegoFlags" []) default defaultText;
|
||||||
|
description = ''
|
||||||
|
Additional global flags to pass to all lego commands.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraLegoRenewFlags = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
inherit (defaultAndText "extraLegoRenewFlags" []) default defaultText;
|
||||||
|
description = ''
|
||||||
|
Additional flags to pass to lego renew.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraLegoRunFlags = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
inherit (defaultAndText "extraLegoRunFlags" []) default defaultText;
|
||||||
|
description = ''
|
||||||
|
Additional flags to pass to lego run.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
certOpts = { name, config, ... }: {
|
||||||
options = {
|
options = {
|
||||||
# user option has been removed
|
# user option has been removed
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
@ -441,40 +633,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 {
|
||||||
@ -483,47 +646,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 = [];
|
||||||
@ -538,92 +660,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.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -632,41 +687,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;
|
||||||
@ -689,9 +709,31 @@ in {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useRoot = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to use the root user when generating certs. This is not recommended
|
||||||
|
for security + compatiblity reasons. If a service requires root owned certificates
|
||||||
|
consider following the guide on "Using ACME with services demanding root
|
||||||
|
owned certificates" in the NixOS manual, and only using this as a fallback
|
||||||
|
or for testing.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
defaults = mkOption {
|
||||||
|
type = types.submodule (inheritableModule true);
|
||||||
|
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 [ (inheritableModule false) certOpts ]);
|
||||||
description = ''
|
description = ''
|
||||||
Attribute set of certificates to get signed and renewed. Creates
|
Attribute set of certificates to get signed and renewed. Creates
|
||||||
<literal>acme-''${cert}.{service,timer}</literal> systemd units for
|
<literal>acme-''${cert}.{service,timer}</literal> systemd units for
|
||||||
@ -722,12 +764,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 [
|
||||||
@ -842,8 +888,8 @@ in {
|
|||||||
# Create some targets which can be depended on to be "active" after cert renewals
|
# Create some targets which can be depended on to be "active" after cert renewals
|
||||||
finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
|
finishedTargets = mapAttrs' (cert: conf: nameValuePair "acme-finished-${cert}" {
|
||||||
wantedBy = [ "default.target" ];
|
wantedBy = [ "default.target" ];
|
||||||
requires = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
|
requires = [ "acme-${cert}.service" ];
|
||||||
after = [ "acme-${cert}.service" ] ++ conf.selfsignedDeps;
|
after = [ "acme-${cert}.service" ];
|
||||||
}) certConfigs;
|
}) certConfigs;
|
||||||
|
|
||||||
# Create targets to limit the number of simultaneous account creations
|
# Create targets to limit the number of simultaneous account creations
|
||||||
|
@ -7,8 +7,9 @@
|
|||||||
<para>
|
<para>
|
||||||
NixOS supports automatic domain validation & certificate retrieval and
|
NixOS supports automatic domain validation & certificate retrieval and
|
||||||
renewal using the ACME protocol. Any provider can be used, but by default
|
renewal using the ACME protocol. Any provider can be used, but by default
|
||||||
NixOS uses Let's Encrypt. The alternative ACME client <literal>lego</literal>
|
NixOS uses Let's Encrypt. The alternative ACME client
|
||||||
is used under the hood.
|
<link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under
|
||||||
|
the hood.
|
||||||
</para>
|
</para>
|
||||||
<para>
|
<para>
|
||||||
Automatic cert validation and configuration for Apache and Nginx virtual
|
Automatic cert validation and configuration for Apache and Nginx virtual
|
||||||
@ -29,7 +30,7 @@
|
|||||||
<para>
|
<para>
|
||||||
You must also set an email address to be used when creating accounts with
|
You must also set an email address to be used when creating accounts with
|
||||||
Let's Encrypt. You can set this for all certs with
|
Let's Encrypt. You can set this for all certs with
|
||||||
<literal><xref linkend="opt-security.acme.email" /></literal>
|
<literal><xref linkend="opt-security.acme.defaults.email" /></literal>
|
||||||
and/or on a per-cert basis with
|
and/or on a per-cert basis with
|
||||||
<literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
|
<literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
|
||||||
This address is only used for registration and renewal reminders,
|
This address is only used for registration and renewal reminders,
|
||||||
@ -38,7 +39,7 @@
|
|||||||
|
|
||||||
<para>
|
<para>
|
||||||
Alternatively, you can use a different ACME server by changing the
|
Alternatively, you can use a different ACME server by changing the
|
||||||
<literal><xref linkend="opt-security.acme.server" /></literal> option
|
<literal><xref linkend="opt-security.acme.defaults.server" /></literal> option
|
||||||
to a provider of your choosing, or just change the server for one cert with
|
to a provider of your choosing, or just change the server for one cert with
|
||||||
<literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
|
<literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
|
||||||
</para>
|
</para>
|
||||||
@ -60,12 +61,12 @@
|
|||||||
= true;</literal> in a virtualHost config. We first create self-signed
|
= true;</literal> in a virtualHost config. We first create self-signed
|
||||||
placeholder certificates in place of the real ACME certs. The placeholder
|
placeholder certificates in place of the real ACME certs. The placeholder
|
||||||
certs are overwritten when the ACME certs arrive. For
|
certs are overwritten when the ACME certs arrive. For
|
||||||
<literal>foo.example.com</literal> the config would look like.
|
<literal>foo.example.com</literal> the config would look like this:
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<programlisting>
|
<programlisting>
|
||||||
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
||||||
<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
|
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
<link linkend="opt-services.nginx.enable">enable</link> = true;
|
<link linkend="opt-services.nginx.enable">enable</link> = true;
|
||||||
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
|
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
|
||||||
@ -114,7 +115,7 @@ services.nginx = {
|
|||||||
|
|
||||||
<programlisting>
|
<programlisting>
|
||||||
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
||||||
<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
|
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
|
||||||
|
|
||||||
# /var/lib/acme/.challenges must be writable by the ACME user
|
# /var/lib/acme/.challenges must be writable by the ACME user
|
||||||
# and readable by the Nginx user. The easiest way to achieve
|
# and readable by the Nginx user. The easiest way to achieve
|
||||||
@ -218,7 +219,7 @@ services.bind = {
|
|||||||
|
|
||||||
# Now we can configure ACME
|
# Now we can configure ACME
|
||||||
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
||||||
<xref linkend="opt-security.acme.email" /> = "admin+acme@example.com";
|
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
|
||||||
<xref linkend="opt-security.acme.certs" />."example.com" = {
|
<xref linkend="opt-security.acme.certs" />."example.com" = {
|
||||||
<link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
|
<link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
|
||||||
<link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
|
<link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
|
||||||
@ -231,25 +232,39 @@ services.bind = {
|
|||||||
<para>
|
<para>
|
||||||
The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
|
The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
|
||||||
must be kept secure and thus you should not keep their contents in your
|
must be kept secure and thus you should not keep their contents in your
|
||||||
Nix config. Instead, generate them one time with these commands:
|
Nix config. Instead, generate them one time with a systemd service:
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<programlisting>
|
<programlisting>
|
||||||
mkdir -p /var/lib/secrets
|
systemd.services.dns-rfc2136-conf = {
|
||||||
tsig-keygen rfc2136key.example.com > /var/lib/secrets/dnskeys.conf
|
requiredBy = ["acme-example.com.service", "bind.service"];
|
||||||
chown named:root /var/lib/secrets/dnskeys.conf
|
before = ["acme-example.com.service", "bind.service"];
|
||||||
chmod 400 /var/lib/secrets/dnskeys.conf
|
unitConfig = {
|
||||||
|
ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
UMask = 0077;
|
||||||
|
};
|
||||||
|
path = [ pkgs.bind ];
|
||||||
|
script = ''
|
||||||
|
mkdir -p /var/lib/secrets
|
||||||
|
tsig-keygen rfc2136key.example.com > /var/lib/secrets/dnskeys.conf
|
||||||
|
chown named:root /var/lib/secrets/dnskeys.conf
|
||||||
|
chmod 400 /var/lib/secrets/dnskeys.conf
|
||||||
|
|
||||||
# Copy the secret value from the dnskeys.conf, and put it in
|
# Copy the secret value from the dnskeys.conf, and put it in
|
||||||
# RFC2136_TSIG_SECRET below
|
# RFC2136_TSIG_SECRET below
|
||||||
|
|
||||||
cat > /var/lib/secrets/certs.secret << EOF
|
cat > /var/lib/secrets/certs.secret << EOF
|
||||||
RFC2136_NAMESERVER='127.0.0.1:53'
|
RFC2136_NAMESERVER='127.0.0.1:53'
|
||||||
RFC2136_TSIG_ALGORITHM='hmac-sha256.'
|
RFC2136_TSIG_ALGORITHM='hmac-sha256.'
|
||||||
RFC2136_TSIG_KEY='rfc2136key.example.com'
|
RFC2136_TSIG_KEY='rfc2136key.example.com'
|
||||||
RFC2136_TSIG_SECRET='your secret key'
|
RFC2136_TSIG_SECRET='your secret key'
|
||||||
EOF
|
EOF
|
||||||
chmod 400 /var/lib/secrets/certs.secret
|
chmod 400 /var/lib/secrets/certs.secret
|
||||||
|
'';
|
||||||
|
};
|
||||||
</programlisting>
|
</programlisting>
|
||||||
|
|
||||||
<para>
|
<para>
|
||||||
@ -258,6 +273,106 @@ chmod 400 /var/lib/secrets/certs.secret
|
|||||||
journalctl -fu acme-example.com.service</literal> and watching its log output.
|
journalctl -fu acme-example.com.service</literal> and watching its log output.
|
||||||
</para>
|
</para>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section xml:id="module-security-acme-config-dns-with-vhosts">
|
||||||
|
<title>Using DNS validation with web server virtual hosts</title>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
It is possible to use DNS-01 validation with all certificates,
|
||||||
|
including those automatically configured via the Nginx/Apache
|
||||||
|
<literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal>
|
||||||
|
option. This configuration pattern is fully
|
||||||
|
supported and part of the module's test suite for Nginx + Apache.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
You must follow the guide above on configuring DNS-01 validation
|
||||||
|
first, however instead of setting the options for one certificate
|
||||||
|
(e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />)
|
||||||
|
you will set them as defaults
|
||||||
|
(e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />).
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<programlisting>
|
||||||
|
# Configure ACME appropriately
|
||||||
|
<xref linkend="opt-security.acme.acceptTerms" /> = true;
|
||||||
|
<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
|
||||||
|
<xref linkend="opt-security.acme.defaults" /> = {
|
||||||
|
<link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136";
|
||||||
|
<link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
|
||||||
|
# We don't need to wait for propagation since this is a local DNS server
|
||||||
|
<link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
# For each virtual host you would like to use DNS-01 validation with,
|
||||||
|
# set acmeRoot = null
|
||||||
|
services.nginx = {
|
||||||
|
<link linkend="opt-services.nginx.enable">enable</link> = true;
|
||||||
|
<link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
|
||||||
|
"foo.example.com" = {
|
||||||
|
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
|
||||||
|
<link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</programlisting>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
And that's it! Next time your configuration is rebuilt, or when
|
||||||
|
you add a new virtualHost, it will be DNS-01 validated.
|
||||||
|
</para>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section xml:id="module-security-acme-root-owned">
|
||||||
|
<title>Using ACME with services demanding root owned certificates</title>
|
||||||
|
|
||||||
|
<para>
|
||||||
|
Some services refuse to start if the configured certificate files
|
||||||
|
are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
|
||||||
|
There is no way to change the user the ACME module uses (it will always be
|
||||||
|
<literal>acme</literal>), however you can use systemd's
|
||||||
|
<literal>LoadCredential</literal> feature to resolve this elegantly.
|
||||||
|
Below is an example configuration for OpenSMTPD, but this pattern
|
||||||
|
can be applied to any service.
|
||||||
|
</para>
|
||||||
|
|
||||||
|
<programlisting>
|
||||||
|
# Configure ACME however you like (DNS or HTTP validation), adding
|
||||||
|
# the following configuration for the relevant certificate.
|
||||||
|
# Note: You cannot use `systemctl reload` here as that would mean
|
||||||
|
# the LoadCredential configuration below would be skipped and
|
||||||
|
# the service would continue to use old certificates.
|
||||||
|
security.acme.certs."mail.example.com".postRun = ''
|
||||||
|
systemctl restart opensmtpd
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Now you must augment OpenSMTPD's systemd service to load
|
||||||
|
# the certificate files.
|
||||||
|
<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"];
|
||||||
|
<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let
|
||||||
|
certDir = config.security.acme.certs."mail.example.com".directory;
|
||||||
|
in [
|
||||||
|
"cert.pem:${certDir}/cert.pem"
|
||||||
|
"key.pem:${certDir}/key.pem"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Finally, configure OpenSMTPD to use these certs.
|
||||||
|
services.opensmtpd = let
|
||||||
|
credsDir = "/run/credentials/opensmtpd.service";
|
||||||
|
in {
|
||||||
|
enable = true;
|
||||||
|
setSendmail = false;
|
||||||
|
serverConfiguration = ''
|
||||||
|
pki mail.example.com cert "${credsDir}/cert.pem"
|
||||||
|
pki mail.example.com key "${credsDir}/key.pem"
|
||||||
|
listen on localhost tls pki mail.example.com
|
||||||
|
action act1 relay host smtp://127.0.0.1:10027
|
||||||
|
match for local action act1
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
</programlisting>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section xml:id="module-security-acme-regenerate">
|
<section xml:id="module-security-acme-regenerate">
|
||||||
<title>Regenerating certificates</title>
|
<title>Regenerating certificates</title>
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ services.prosody = {
|
|||||||
a TLS certificate for the three endponits:
|
a TLS certificate for the three endponits:
|
||||||
<programlisting>
|
<programlisting>
|
||||||
security.acme = {
|
security.acme = {
|
||||||
<link linkend="opt-security.acme.email">email</link> = "root@example.org";
|
<link linkend="opt-security.acme.defaults.email">email</link> = "root@example.org";
|
||||||
<link linkend="opt-security.acme.acceptTerms">acceptTerms</link> = true;
|
<link linkend="opt-security.acme.acceptTerms">acceptTerms</link> = true;
|
||||||
<link linkend="opt-security.acme.certs">certs</link> = {
|
<link linkend="opt-security.acme.certs">certs</link> = {
|
||||||
"example.org" = {
|
"example.org" = {
|
||||||
|
@ -25,7 +25,7 @@ services.discourse = {
|
|||||||
};
|
};
|
||||||
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
<link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
|
||||||
};
|
};
|
||||||
<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
|
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
|
||||||
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
||||||
</programlisting>
|
</programlisting>
|
||||||
</para>
|
</para>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
};
|
};
|
||||||
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
|
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
|
||||||
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
||||||
<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
|
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
|
||||||
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
||||||
}</programlisting>
|
}</programlisting>
|
||||||
</para>
|
</para>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
};
|
};
|
||||||
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
|
<link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
|
||||||
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
|
||||||
<link linkend="opt-security.acme.email">security.acme.email</link> = "me@example.com";
|
<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
|
||||||
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
|
||||||
}</programlisting>
|
}</programlisting>
|
||||||
</para>
|
</para>
|
||||||
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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,113 +170,53 @@ 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
|
||||||
};
|
|
||||||
|
|
||||||
# Used to determine if service reload was triggered
|
|
||||||
systemd.targets.test-renew-nginx = {
|
|
||||||
wants = [ "acme-a.example.test.service" ];
|
|
||||||
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;
|
caDomain = nodes.acme.config.test-support.acme.caDomain;
|
||||||
|
email = config.security.acme.defaults.email;
|
||||||
# Exit 99 to make it easier to track if this is the reason a renew failed
|
# Exit 99 to make it easier to track if this is the reason a renew failed
|
||||||
testScript = ''
|
accountCreateTester = ''
|
||||||
test -e accounts/${caDomain}/${email}/account.json || exit 99
|
test -e accounts/${caDomain}/${email}/account.json || exit 99
|
||||||
'';
|
'';
|
||||||
in {
|
in lib.mkMerge [
|
||||||
security.acme.email = lib.mkForce email;
|
webserverBasicConfig
|
||||||
systemd.services."b.example.test".preStart = testScript;
|
{
|
||||||
systemd.services."c.example.test".preStart = testScript;
|
# 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;
|
||||||
|
|
||||||
services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
|
services.nginx.virtualHosts."b.example.test" = vhostBase // {
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
};
|
};
|
||||||
services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
|
services.nginx.virtualHosts."c.example.test" = vhostBase // {
|
||||||
enableACME = true;
|
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" = {
|
|
||||||
serverAliases = [ "b.example.test" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Test OCSP Stapling
|
# Test OCSP Stapling
|
||||||
specialisation.ocsp-stapling.configuration = { pkgs, ... }: {
|
ocsp-stapling.configuration = { ... }: lib.mkMerge [
|
||||||
security.acme.certs."a.example.test" = {
|
webserverBasicConfig
|
||||||
ocspMustStaple = true;
|
{
|
||||||
};
|
security.acme.certs."a.example.test".ocspMustStaple = true;
|
||||||
services.nginx.virtualHosts."a.example.com" = {
|
services.nginx.virtualHosts."a.example.test" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
ssl_stapling on;
|
ssl_stapling on;
|
||||||
ssl_stapling_verify on;
|
ssl_stapling_verify on;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
];
|
||||||
# Test using Apache HTTPD
|
|
||||||
specialisation.httpd-aliases.configuration = { pkgs, config, lib, ... }: {
|
|
||||||
services.nginx.enable = lib.mkForce false;
|
|
||||||
services.httpd.enable = true;
|
|
||||||
services.httpd.adminAddr = config.security.acme.email;
|
|
||||||
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
|
|
||||||
systemd.targets.test-renew-httpd = {
|
|
||||||
wants = [ "acme-c.example.test.service" ];
|
|
||||||
after = [ "acme-c.example.test.service" "httpd-config-reload.service" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Validation via DNS-01 challenge
|
|
||||||
specialisation.dns-01.configuration = { pkgs, config, nodes, ... }: {
|
|
||||||
security.acme.certs."example.test" = {
|
|
||||||
domain = "*.example.test";
|
|
||||||
group = config.services.nginx.group;
|
|
||||||
dnsProvider = "exec";
|
|
||||||
dnsPropagationCheck = false;
|
|
||||||
credentialsFile = pkgs.writeText "wildcard.env" ''
|
|
||||||
EXEC_PATH=${dnsScript { inherit pkgs nodes; }}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
services.nginx.virtualHosts."dns.example.test" = (vhostBase pkgs) // {
|
|
||||||
useACMEHost = "example.test";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Validate service relationships by adding a slow start service to nginx' wants.
|
# Validate service relationships by adding a slow start service to nginx' wants.
|
||||||
# Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
|
# Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
|
||||||
specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ... }: {
|
slow-startup.configuration = { ... }: lib.mkMerge [
|
||||||
|
webserverBasicConfig
|
||||||
|
{
|
||||||
systemd.services.my-slow-service = {
|
systemd.services.my-slow-service = {
|
||||||
wantedBy = [ "multi-user.target" "nginx.service" ];
|
wantedBy = [ "multi-user.target" "nginx.service" ];
|
||||||
before = [ "nginx.service" ];
|
before = [ "nginx.service" ];
|
||||||
@ -177,16 +224,88 @@ in import ./make-test-python.nix ({ lib, ... }: {
|
|||||||
script = "${pkgs.python3}/bin/python -m http.server";
|
script = "${pkgs.python3}/bin/python -m http.server";
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx.virtualHosts."slow.example.com" = {
|
services.nginx.virtualHosts."slow.example.test" = {
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
locations."/".proxyPass = "http://localhost:8000";
|
locations."/".proxyPass = "http://localhost:8000";
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# Test lego internal server (listenHTTP option)
|
||||||
|
# Also tests useRoot option
|
||||||
|
lego-server.configuration = { ... }: {
|
||||||
|
security.acme.useRoot = true;
|
||||||
|
security.acme.certs."lego.example.test" = {
|
||||||
|
listenHTTP = ":80";
|
||||||
|
group = "nginx";
|
||||||
|
};
|
||||||
|
services.nginx.enable = true;
|
||||||
|
services.nginx.virtualHosts."lego.example.test" = {
|
||||||
|
useACMEHost = "lego.example.test";
|
||||||
|
onlySSL = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Test compatiblity with Caddy
|
||||||
|
# It only supports useACMEHost, hence not using mkServerConfigs
|
||||||
|
} // (let
|
||||||
|
baseCaddyConfig = { nodes, config, ... }: {
|
||||||
|
security.acme = {
|
||||||
|
defaults = (dnsConfig nodes) // {
|
||||||
|
group = config.services.caddy.group;
|
||||||
|
};
|
||||||
|
# One manual wildcard cert
|
||||||
|
certs."example.test" = {
|
||||||
|
domain = "*.example.test";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.caddy = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts."a.exmaple.test" = {
|
||||||
|
useACMEHost = "example.test";
|
||||||
|
extraConfig = ''
|
||||||
|
root * ${documentRoot}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
caddy.configuration = baseCaddyConfig;
|
||||||
|
|
||||||
|
# Test that the server reloads when only the acme configuration is changed.
|
||||||
|
"caddy-change-acme-conf".configuration = { nodes, config, ... }: lib.mkMerge [
|
||||||
|
(baseCaddyConfig {
|
||||||
|
inherit nodes config;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
security.acme.certs."example.test" = {
|
||||||
|
keyType = "ec384";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# Test compatibility with Nginx
|
||||||
|
}) // (mkServerConfigs {
|
||||||
|
server = "nginx";
|
||||||
|
group = "nginx";
|
||||||
|
vhostBaseData = vhostBase;
|
||||||
|
})
|
||||||
|
|
||||||
|
# Test compatibility with Apache HTTPD
|
||||||
|
// (mkServerConfigs {
|
||||||
|
server = "httpd";
|
||||||
|
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) ];
|
||||||
|
|
||||||
@ -195,7 +314,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;
|
||||||
@ -204,23 +323,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}"
|
||||||
|
f" || ln -s $(readlink /run/current-system)/specialisation {root_specs}"
|
||||||
)
|
)
|
||||||
has_switched = True
|
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -310,8 +432,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")
|
||||||
@ -320,19 +441,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)
|
||||||
|
|
||||||
with subtest("Can request certificate with HTTPS-01 challenge"):
|
# Perform general tests first
|
||||||
|
switch_to(webserver, "general")
|
||||||
|
|
||||||
|
with subtest("Can request certificate with HTTP-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"
|
||||||
)
|
)
|
||||||
@ -346,12 +478,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")
|
||||||
@ -365,77 +491,107 @@ 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 HTTP-01 using lego's internal web server"):
|
||||||
switch_to(webserver, "slow-startup")
|
switch_to(webserver, "lego-server")
|
||||||
webserver.wait_for_unit("acme-finished-slow.example.com.target")
|
webserver.wait_for_unit("acme-finished-lego.example.test.target")
|
||||||
check_issuer(webserver, "slow.example.com", "pebble")
|
webserver.wait_for_unit("nginx.service")
|
||||||
check_connection(client, "slow.example.com")
|
webserver.succeed("echo HENLO && systemctl cat nginx.service")
|
||||||
|
webserver.succeed("test \"$(stat -c '%U' /var/lib/acme/* | uniq)\" = \"root\"")
|
||||||
with subtest("Can request certificate for vhost + aliases (nginx)"):
|
|
||||||
# Check the key hash before and after adding an alias. It should not change.
|
|
||||||
# The previous test reverts the ed384 change
|
|
||||||
webserver.wait_for_unit("acme-finished-a.example.test.target")
|
|
||||||
switch_to(webserver, "nginx-aliases")
|
|
||||||
webserver.wait_for_unit("acme-finished-a.example.test.target")
|
|
||||||
check_issuer(webserver, "a.example.test", "pebble")
|
|
||||||
check_connection(client, "a.example.test")
|
check_connection(client, "a.example.test")
|
||||||
check_connection(client, "b.example.test")
|
check_connection(client, "lego.example.test")
|
||||||
|
|
||||||
with subtest("Can request certificates for vhost + aliases (apache-httpd)"):
|
with subtest("Can request certificate with HTTP-01 when nginx startup is delayed"):
|
||||||
|
webserver.execute("systemctl stop nginx")
|
||||||
|
switch_to(webserver, "slow-startup")
|
||||||
|
webserver.wait_for_unit("acme-finished-slow.example.test.target")
|
||||||
|
check_issuer(webserver, "slow.example.test", "pebble")
|
||||||
|
webserver.wait_for_unit("nginx.service")
|
||||||
|
check_connection(client, "slow.example.test")
|
||||||
|
|
||||||
|
with subtest("Works with caddy"):
|
||||||
|
switch_to(webserver, "caddy")
|
||||||
|
webserver.wait_for_unit("acme-finished-example.test.target")
|
||||||
|
webserver.wait_for_unit("caddy.service")
|
||||||
|
# FIXME reloading caddy is not sufficient to load new certs.
|
||||||
|
# Restart it manually until this is fixed.
|
||||||
|
webserver.succeed("systemctl restart caddy.service")
|
||||||
|
check_connection(client, "a.example.test")
|
||||||
|
|
||||||
|
with subtest("security.acme changes reflect on caddy"):
|
||||||
|
switch_to(webserver, "caddy-change-acme-conf")
|
||||||
|
webserver.wait_for_unit("acme-finished-example.test.target")
|
||||||
|
webserver.wait_for_unit("caddy.service")
|
||||||
|
# FIXME reloading caddy is not sufficient to load new certs.
|
||||||
|
# Restart it manually until this is fixed.
|
||||||
|
webserver.succeed("systemctl restart caddy.service")
|
||||||
|
check_connection_key_bits(client, "a.example.test", "384")
|
||||||
|
|
||||||
|
domains = ["http", "dns", "wildcard"]
|
||||||
|
for server, logsrc in [
|
||||||
|
("nginx", "journalctl -n 30 -u nginx.service"),
|
||||||
|
("httpd", "tail -n 30 /var/log/httpd/*.log"),
|
||||||
|
]:
|
||||||
|
wait_for_server = lambda: webserver.wait_for_unit(f"{server}.service")
|
||||||
|
with subtest(f"Works with {server}"):
|
||||||
try:
|
try:
|
||||||
switch_to(webserver, "httpd-aliases")
|
switch_to(webserver, server)
|
||||||
webserver.wait_for_unit("acme-finished-c.example.test.target")
|
# 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:
|
except Exception as err:
|
||||||
_, output = webserver.execute(
|
_, output = webserver.execute(
|
||||||
"cat /var/log/httpd/*.log && ls -al /var/lib/acme/acme-challenge"
|
f"{logsrc} && ls -al /var/lib/acme/acme-challenge"
|
||||||
)
|
)
|
||||||
print(output)
|
print(output)
|
||||||
raise err
|
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"):
|
wait_for_server()
|
||||||
|
|
||||||
|
for domain in domains[:-1]:
|
||||||
|
check_issuer(webserver, f"{server}-{domain}.example.test", "pebble")
|
||||||
|
for domain in domains:
|
||||||
|
check_connection(client, f"{server}-{domain}.example.test")
|
||||||
|
check_connection(client, f"{server}-{domain}-alias.example.test")
|
||||||
|
|
||||||
|
test_domain = f"{server}-{domains[0]}.example.test"
|
||||||
|
|
||||||
|
with subtest(f"Can reload {server} when timer triggers renewal"):
|
||||||
# Switch to selfsigned first
|
# Switch to selfsigned first
|
||||||
webserver.succeed("systemctl clean acme-c.example.test.service --what=state")
|
webserver.succeed(f"systemctl clean acme-{test_domain}.service --what=state")
|
||||||
webserver.succeed("systemctl start acme-selfsigned-c.example.test.service")
|
webserver.succeed(f"systemctl start acme-selfsigned-{test_domain}.service")
|
||||||
check_issuer(webserver, "c.example.test", "minica")
|
check_issuer(webserver, test_domain, "minica")
|
||||||
webserver.succeed("systemctl start httpd-config-reload.service")
|
webserver.succeed(f"systemctl start {server}-config-reload.service")
|
||||||
webserver.succeed("systemctl start test-renew-httpd.target")
|
webserver.succeed(f"systemctl start test-renew-{server}.target")
|
||||||
check_issuer(webserver, "c.example.test", "pebble")
|
check_issuer(webserver, test_domain, "pebble")
|
||||||
check_connection(client, "c.example.test")
|
check_connection(client, test_domain)
|
||||||
|
|
||||||
with subtest("Can request wildcard certificates using DNS-01 challenge"):
|
with subtest("Can remove an alias from a domain + cert is updated"):
|
||||||
switch_to(webserver, "dns-01")
|
test_alias = f"{server}-{domains[0]}-alias.example.test"
|
||||||
webserver.wait_for_unit("acme-finished-example.test.target")
|
switch_to(webserver, f"{server}-remove-alias")
|
||||||
check_issuer(webserver, "example.test", "pebble")
|
webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
|
||||||
check_connection(client, "dns.example.test")
|
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}"
|
||||||
|
)
|
||||||
|
assert rc > 0, "Removed extraDomainName was not removed from the cert"
|
||||||
|
|
||||||
|
with subtest("security.acme changes reflect on web server"):
|
||||||
|
# Switch back to normal server config first, reset everything.
|
||||||
|
switch_to(webserver, server)
|
||||||
|
wait_for_server()
|
||||||
|
switch_to(webserver, f"{server}-change-acme-conf")
|
||||||
|
webserver.wait_for_unit(f"acme-finished-{test_domain}.target")
|
||||||
|
wait_for_server()
|
||||||
|
check_connection_key_bits(client, test_domain, "384")
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
@ -5,9 +5,11 @@ let
|
|||||||
|
|
||||||
in {
|
in {
|
||||||
security.acme = {
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
|
defaults = {
|
||||||
server = "https://${caDomain}/dir";
|
server = "https://${caDomain}/dir";
|
||||||
email = "hostmaster@example.test";
|
email = "hostmaster@example.test";
|
||||||
acceptTerms = true;
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
security.pki.certificateFiles = [ caCert ];
|
security.pki.certificateFiles = [ caCert ];
|
||||||
|
@ -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";
|
||||||
|
Loading…
Reference in New Issue
Block a user