summaryrefslogtreecommitdiffstats
path: root/nixos
diff options
context:
space:
mode:
authorLucas Savva <lucas@m1cr0man.com>2021-11-28 17:03:31 +0000
committerLucas Savva <lucas@m1cr0man.com>2021-12-26 16:44:10 +0000
commit377c6bcefce8e8ccd471892a1b24621d5a909457 (patch)
tree4914c740b10fb82ff157ef2071929ba2c3f8822a /nixos
parenta7f00013280416ce889d841e675526b8cb96a0ee (diff)
nixos/acme: Add defaults and inheritDefaults option
Allows configuring many default settings for certificates, all of which can still be overridden on a per-cert basis. Some options have been moved into .defaults from security.acme, namely email, server, validMinDays and renewInterval. These changes will not break existing configurations thanks to mkChangedOptionModule. With this, it is also now possible to configure DNS-01 with web servers whose virtualHosts utilise enableACME. The only requirement is you set `acmeRoot = null` for each vhost. The test suite has been revamped to cover these additions and also to generally make it easier to maintain. Test config for apache and nginx has been fully standardised, and it is now much easier to add a new web server if it follows the same configuration patterns as those two. I have also optimised the use of switch-to-configuration which should speed up testing.
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/security/acme.nix228
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix13
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/vhost-options.nix7
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix13
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix9
-rw-r--r--nixos/tests/acme.nix515
-rw-r--r--nixos/tests/common/acme/client/default.nix6
-rw-r--r--nixos/tests/common/acme/server/default.nix5
8 files changed, 448 insertions, 348 deletions
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 9242a12fd35f..c39653d174e5 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -128,7 +128,7 @@ let
};
certToConfig = cert: data: let
- acmeServer = if data.server != null then data.server else cfg.server;
+ acmeServer = data.server;
useDns = data.dnsProvider != null;
destPath = "/var/lib/acme/${cert}";
selfsignedDeps = optionals (cfg.preliminarySelfsigned) [ "acme-selfsigned-${cert}.service" ];
@@ -211,7 +211,7 @@ let
description = "Renew ACME Certificate for ${cert}";
wantedBy = [ "timers.target" ];
timerConfig = {
- OnCalendar = cfg.renewInterval;
+ OnCalendar = data.renewInterval;
Unit = "acme-${cert}.service";
Persistent = "yes";
@@ -356,7 +356,7 @@ let
expiration_s=$[expiration_date - now]
expiration_days=$[expiration_s / (3600 * 24)] # rounds down
- [[ $expiration_days -gt ${toString cfg.validMinDays} ]]
+ [[ $expiration_days -gt ${toString data.validMinDays} ]]
}
${optionalString (data.webroot != null) ''
@@ -380,11 +380,12 @@ let
# Even if a cert is not expired, it may be revoked by the CA.
# Try to renew, and silently fail if the cert is not expired.
# Avoids #85794 and resolves #129838
- if ! lego ${renewOpts} --days ${toString cfg.validMinDays}; then
+ if ! lego ${renewOpts} --days ${toString data.validMinDays}; then
if is_expiration_skippable out/full.pem; then
- echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString cfg.validMinDays} days"
+ echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't within the coming ${toString data.validMinDays} days"
else
- exit 3
+ # High number to avoid Systemd reserved codes.
+ exit 11
fi
fi
@@ -394,8 +395,9 @@ let
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 2 so that users can potentially amend SuccessExitStatus to ignore this error.
- exit 2
+ # Exit 10 so that users can potentially amend SuccessExitStatus to ignore this error.
+ # High number to avoid Systemd reserved codes.
+ exit 10
fi
mv domainhash.txt certificates/
@@ -423,31 +425,33 @@ let
certConfigs = mapAttrs certToConfig cfg.certs;
- certOpts = { name, ... }: {
- options = {
- # user option has been removed
- user = mkOption {
- visible = false;
- default = "_mkRemovedOptionModule";
+ # These options can be specified within
+ # security.acme or security.acme.certs.<name>
+ inheritableOpts =
+ { inheritDefaults ? false, defaults ? null }: {
+ validMinDays = mkOption {
+ type = types.int;
+ default = if inheritDefaults then defaults.validMinDays else 30;
+ description = "Minimum remaining validity before renewal in days.";
};
- # allowKeysForGroup option has been removed
- allowKeysForGroup = mkOption {
- visible = false;
- default = "_mkRemovedOptionModule";
+ renewInterval = mkOption {
+ type = types.str;
+ default = if inheritDefaults then defaults.renewInterval else "daily";
+ description = ''
+ Systemd calendar expression when to check for renewal. See
+ <citerefentry><refentrytitle>systemd.time</refentrytitle>
+ <manvolnum>7</manvolnum></citerefentry>.
+ '';
};
- # extraDomains was replaced with extraDomainNames
- extraDomains = mkOption {
- visible = false;
- default = "_mkMergedOptionModule";
+ enableDebugLogs = mkEnableOption "debug logging for this certificate" // {
+ default = if inheritDefaults then defaults.enableDebugLogs else true;
};
- enableDebugLogs = mkEnableOption "debug logging for this certificate" // { default = cfg.enableDebugLogs; };
-
webroot = mkOption {
type = types.nullOr types.str;
- default = null;
+ default = if inheritDefaults then defaults.webroot else null;
example = "/var/lib/acme/acme-challenge";
description = ''
Where the webroot of the HTTP vhost is located.
@@ -458,20 +462,9 @@ let
'';
};
- 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;
+ default = if inheritDefaults then defaults.server else null;
description = ''
ACME Directory Resource URI. Defaults to Let's Encrypt's
production endpoint,
@@ -479,28 +472,25 @@ let
'';
};
- domain = mkOption {
- type = types.str;
- default = 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.";
+ type = types.str;
+ default = if inheritDefaults then defaults.email else null;
+ description = ''
+ Email address for account creation and correspondence from the CA.
+ It is recommended to use the same email for all certs to avoid account
+ creation limits.
+ '';
};
group = mkOption {
type = types.str;
- default = "acme";
+ default = if inheritDefaults then defaults.group else "acme";
description = "Group running the ACME client.";
};
reloadServices = mkOption {
type = types.listOf types.str;
- default = [];
+ default = if inheritDefaults then defaults.reloadServices else [];
description = ''
The list of systemd services to call <code>systemctl try-reload-or-restart</code>
on.
@@ -509,7 +499,7 @@ let
postRun = mkOption {
type = types.lines;
- default = "";
+ default = if inheritDefaults then defaults.postRun else "";
example = "cp full.pem backup.pem";
description = ''
Commands to run after new certificates go live. Note that
@@ -519,30 +509,9 @@ let
'';
};
- directory = mkOption {
- type = types.str;
- readOnly = true;
- default = "/var/lib/acme/${name}";
- description = "Directory where certificate and other state is stored.";
- };
-
- extraDomainNames = mkOption {
- type = types.listOf types.str;
- default = [];
- example = literalExpression ''
- [
- "example.org"
- "mydomain.org"
- ]
- '';
- description = ''
- A list of extra domain names, which are included in the one certificate to be issued.
- '';
- };
-
keyType = mkOption {
type = types.str;
- default = "ec256";
+ default = if inheritDefaults then defaults.keyType else "ec256";
description = ''
Key type to use for private keys.
For an up to date list of supported values check the --key-type option
@@ -552,7 +521,7 @@ let
dnsProvider = mkOption {
type = types.nullOr types.str;
- default = null;
+ default = if inheritDefaults then defaults.dnsProvider else null;
example = "route53";
description = ''
DNS Challenge provider. For a list of supported providers, see the "code"
@@ -562,7 +531,7 @@ let
dnsResolver = mkOption {
type = types.nullOr types.str;
- default = null;
+ default = if inheritDefaults then defaults.dnsResolver else null;
example = "1.1.1.1:53";
description = ''
Set the resolver to use for performing recursive DNS queries. Supported:
@@ -573,6 +542,7 @@ let
credentialsFile = mkOption {
type = types.path;
+ default = if inheritDefaults then defaults.credentialsFile else null;
description = ''
Path to an EnvironmentFile for the cert's service containing any required and
optional environment variables for your selected dnsProvider.
@@ -584,7 +554,7 @@ let
dnsPropagationCheck = mkOption {
type = types.bool;
- default = true;
+ default = if inheritDefaults then defaults.dnsPropagationCheck else true;
description = ''
Toggles lego DNS propagation check, which is used alongside DNS-01
challenge to ensure the DNS entries required are available.
@@ -593,7 +563,7 @@ let
ocspMustStaple = mkOption {
type = types.bool;
- default = false;
+ default = if inheritDefaults then defaults.ocspMustStaple else false;
description = ''
Turns on the OCSP Must-Staple TLS extension.
Make sure you know what you're doing! See:
@@ -606,7 +576,7 @@ let
extraLegoFlags = mkOption {
type = types.listOf types.str;
- default = [];
+ default = if inheritDefaults then defaults.extraLegoFlags else [];
description = ''
Additional global flags to pass to all lego commands.
'';
@@ -614,7 +584,7 @@ let
extraLegoRenewFlags = mkOption {
type = types.listOf types.str;
- default = [];
+ default = if inheritDefaults then defaults.extraLegoRenewFlags else [];
description = ''
Additional flags to pass to lego renew.
'';
@@ -622,53 +592,87 @@ let
extraLegoRunFlags = mkOption {
type = types.listOf types.str;
- default = [];
+ default = if inheritDefaults then defaults.extraLegoRunFlags else [];
description = ''
Additional flags to pass to lego run.
'';
};
};
- };
-
-in {
- options = {
- security.acme = {
+ certOpts = { name, ... }: {
+ options = (inheritableOpts { inherit (cfg) defaults; inheritDefaults = cfg.certs."${name}".inheritDefaults; }) // {
+ # user option has been removed
+ user = mkOption {
+ visible = false;
+ default = "_mkRemovedOptionModule";
+ };
- enableDebugLogs = mkEnableOption "debug logging for all certificates by default" // { default = true; };
+ # allowKeysForGroup option has been removed
+ allowKeysForGroup = mkOption {
+ visible = false;
+ default = "_mkRemovedOptionModule";
+ };
- validMinDays = mkOption {
- type = types.int;
- default = 30;
- description = "Minimum remaining validity before renewal in days.";
+ # extraDomains was replaced with extraDomainNames
+ extraDomains = mkOption {
+ visible = false;
+ default = "_mkMergedOptionModule";
};
- email = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = "Contact email address for the CA to be able to reach you.";
+ directory = mkOption {
+ type = types.str;
+ readOnly = true;
+ default = "/var/lib/acme/${name}";
+ description = "Directory where certificate and other state is stored.";
};
- renewInterval = mkOption {
+ domain = mkOption {
type = types.str;
- default = "daily";
+ default = name;
+ description = "Domain to fetch certificate for (defaults to the entry name).";
+ };
+
+ extraDomainNames = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ example = literalExpression ''
+ [
+ "example.org"
+ "mydomain.org"
+ ]
+ '';
description = ''
- Systemd calendar expression when to check for renewal. See
- <citerefentry><refentrytitle>systemd.time</refentrytitle>
- <manvolnum>7</manvolnum></citerefentry>.
+ A list of extra domain names, which are included in the one certificate to be issued.
'';
};
- server = mkOption {
+ # This setting must be different for each configured certificate, otherwise
+ # two or more renewals may fail to bind to the address. Hence, it is not in
+ # the inheritableOpts.
+ listenHTTP = mkOption {
type = types.nullOr types.str;
default = null;
+ example = ":1360";
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.
+ 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.
'';
};
+ inheritDefaults = mkOption {
+ default = true;
+ example = true;
+ description = "Whether to inherit values set in `security.acme.defaults` or not.";
+ type = lib.types.bool;
+ };
+ };
+ };
+
+in {
+
+ options = {
+ security.acme = {
preliminarySelfsigned = mkOption {
type = types.bool;
default = true;
@@ -691,6 +695,16 @@ in {
'';
};
+ defaults = mkOption {
+ type = types.submodule ({ ... }: { options = inheritableOpts {}; });
+ description = ''
+ Default values inheritable by all configured certs. You can
+ use this to define options shared by all your certs. These defaults
+ can also be ignored on a per-cert basis using the
+ `security.acme.certs.''${cert}.inheritDefaults' option.
+ '';
+ };
+
certs = mkOption {
default = { };
type = with types; attrsOf (submodule certOpts);
@@ -724,12 +738,16 @@ in {
To use the let's encrypt staging server, use security.acme.server =
"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" "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")
- (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 [
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 992a58875e43..1a49b4ca15c7 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -154,7 +154,7 @@ let
sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
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/"
<Directory "${hostOpts.acmeRoot}">
AllowOverride None
@@ -677,9 +677,16 @@ in
};
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;
- 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;
# Use the vhost-specific email address if provided, otherwise let
# security.acme.email or security.acme.certs.<cert>.email be used.
diff --git a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
index 8bb7e91ec9cd..c52ab2c596e0 100644
--- a/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/vhost-options.nix
@@ -128,9 +128,12 @@ in
};
acmeRoot = mkOption {
- type = types.str;
+ type = types.nullOr types.str;
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 {
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index ba8e874f2ded..05b7870fc3a1 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -278,7 +278,7 @@ let
acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) ''
location /.well-known/acme-challenge {
${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"}
- root ${vhost.acmeRoot};
+ ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"}
auth_basic off;
}
${optionalString (vhost.acmeFallbackHost != null) ''
@@ -948,9 +948,16 @@ in
};
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;
- 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;
# Filter for enableACME-only vhosts. Don't want to create dud certs
}) (filter (vhostConfig: vhostConfig.useACMEHost == null) acmeEnabledVhosts);
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 7f49ce9586ca..c4e8285dc48b 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -3,7 +3,7 @@
# has additional options that affect the web server as a whole, like
# the user/group to run under.)
-{ lib, ... }:
+{ config, lib, ... }:
with lib;
{
@@ -85,9 +85,12 @@ with lib;
};
acmeRoot = mkOption {
- type = types.str;
+ type = types.nullOr types.str;
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 {
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 80b85502d4e8..549fa9e64eea 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -1,9 +1,9 @@
-let
+import ./make-test-python.nix ({ pkgs, lib, ... }: let
commonConfig = ./common/acme/client;
dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress;
- dnsScript = {pkgs, nodes}: let
+ dnsScript = nodes: let
dnsAddress = dnsServerIP nodes;
in pkgs.writeShellScript "dns-hook.sh" ''
set -euo pipefail
@@ -15,30 +15,137 @@ let
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"
echo hello world > "$out/index.html"
'';
- vhostBase = pkgs: {
+ vhostBase = {
forceSSL = true;
- locations."/".root = documentRoot pkgs;
+ locations."/".root = documentRoot;
+ };
+
+ 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 import ./make-test-python.nix ({ lib, ... }: {
+in {
name = "acme";
meta.maintainers = lib.teams.acme.members;
nodes = {
# The fake ACME server which will respond to client requests
- acme = { nodes, lib, ... }: {
+ acme = { nodes, ... }: {
imports = [ ./common/acme/server ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
};
# A fake DNS server which can be configured with records as desired
# Used to test DNS-01 challenge
- dnsserver = { nodes, pkgs, ... }: {
+ dnsserver = { nodes, ... }: {
networking.firewall.allowedTCPPorts = [ 8055 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
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
- webserver = { pkgs, nodes, lib, config, ... }: {
+ webserver = { nodes, config, ... }: {
imports = [ commonConfig ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
networking.firewall.allowedTCPPorts = [ 80 443 ];
@@ -63,138 +170,88 @@ in import ./make-test-python.nix ({ lib, ... }: {
environment.systemPackages = [ pkgs.openssl ];
# Set log level to info so that we can see when the service is reloaded
- services.nginx.enable = true;
services.nginx.logError = "stderr info";
- # First tests configure a basic cert and run a bunch of openssl checks
- services.nginx.virtualHosts."a.example.test" = (vhostBase pkgs) // {
- enableACME = true;
- };
-
- # 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;
- # Exit 99 to make it easier to track if this is the reason a renew failed
- testScript = ''
- test -e accounts/${caDomain}/${email}/account.json || exit 99
- '';
- in {
- security.acme.email = lib.mkForce email;
- systemd.services."b.example.test".preStart = testScript;
- systemd.services."c.example.test".preStart = testScript;
-
- services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
- enableACME = true;
- };
- services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
- enableACME = true;
- };
- };
-
- # Cert config changes will not cause the nginx configuration to change.
- # This tests that the reload service is correctly triggered.
- # It also tests that postRun is exec'd as root
- specialisation.cert-change.configuration = { pkgs, ... }: {
- security.acme.certs."a.example.test".keyType = "ec384";
- security.acme.certs."a.example.test".postRun = ''
- set -euo pipefail
- touch /home/test
- chown root:root /home/test
- echo testing > /home/test
- '';
- };
-
- # Now adding an alias to ensure that the certs are updated
- specialisation.nginx-aliases.configuration = { pkgs, ... }: {
- services.nginx.virtualHosts."a.example.test" = (vhostBase pkgs) // {
- serverAliases = [ "b.example.test" ];
- };
- };
-
- # Must be run after nginx-aliases
- specialisation.remove-extra-domain.configuration = { pkgs, ... } : {
- # This also validates that useACMEHost doesn't unexpectedly add the domain.
- services.nginx.virtualHosts."b.example.test" = (vhostBase pkgs) // {
- useACMEHost = "a.example.test";
- };
- };
-
- # Test OCSP Stapling
- specialisation.ocsp-stapling.configuration = { pkgs, ... }: {
- security.acme.certs."a.example.test" = {
- ocspMustStaple = true;
- };
- services.nginx.virtualHosts."a.example.com" = {
- extraConfig = ''
- ssl_stapling on;
- ssl_stapling_verify on;
- '';
- };
- };
-
- # 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; }}
+ specialisation = {
+ # First derivation used to test general ACME features
+ general.configuration = { ... }: let
+ 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
+ accountCreateTester = ''
+ test -e accounts/${caDomain}/${email}/account.json || exit 99
'';
- };
-
- services.nginx.virtualHosts."dns.example.test" = (vhostBase pkgs) // {
- useACMEHost = "example.test";
- };
- };
-
- # Validate service relationships by adding a slow start service to nginx' wants.
- # Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
- specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ... }: {