mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-11-10 09:43:30 +01:00
Currently, nginx gets restarted when adding a new ACME certificate, even when `services.nginx.enableReload = true` because of changes in the Wants/After/Before sections of `nginx.service`. This change moves these dependencies to `nginx-config-reload.service` and the respective ACME systemd units.
244 lines
9.3 KiB
Nix
244 lines
9.3 KiB
Nix
{
|
|
serverName,
|
|
group,
|
|
baseModule,
|
|
domain,
|
|
}:
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
{
|
|
name = serverName;
|
|
meta = {
|
|
maintainers = lib.teams.acme.members;
|
|
# Hard timeout in seconds. Average run time is about 100 seconds.
|
|
timeout = 300;
|
|
};
|
|
|
|
interactive.sshBackdoor.enable = true;
|
|
|
|
nodes = {
|
|
# The fake ACME server which will respond to client requests
|
|
acme =
|
|
{ nodes, ... }:
|
|
{
|
|
imports = [ ../common/acme/server ];
|
|
};
|
|
|
|
webserver =
|
|
{ nodes, ... }:
|
|
{
|
|
imports = [
|
|
../common/acme/client
|
|
baseModule
|
|
];
|
|
networking.domain = domain;
|
|
networking.firewall.allowedTCPPorts = [
|
|
80
|
|
443
|
|
];
|
|
|
|
# Resolve the vhosts the easy way
|
|
networking.hosts."127.0.0.1" = [
|
|
"proxied.${domain}"
|
|
"certchange.${domain}"
|
|
"zeroconf.${domain}"
|
|
"zeroconf2.${domain}"
|
|
"zeroconf3.${domain}"
|
|
"nullroot.${domain}"
|
|
];
|
|
|
|
# OpenSSL will be used for more thorough certificate validation
|
|
environment.systemPackages = [ pkgs.openssl ];
|
|
|
|
# Used to determine if service reload was triggered.
|
|
# This does not provide a guarantee that the webserver is finished reloading,
|
|
# to handle that there is retry logic wrapping any connectivity checks.
|
|
systemd.targets."renew-triggered" = {
|
|
wantedBy = [ "${serverName}-config-reload.service" ];
|
|
after = [ "${serverName}-config-reload.service" ];
|
|
unitConfig.RefuseManualStart = true;
|
|
};
|
|
|
|
security.acme.certs."proxied.${domain}" = {
|
|
listenHTTP = ":8080";
|
|
group = group;
|
|
};
|
|
|
|
specialisation = {
|
|
# Test that the web server is correctly reloaded when the cert changes
|
|
certchange.configuration = {
|
|
security.acme.certs."proxied.${domain}".extraDomainNames = [
|
|
"certchange.${domain}"
|
|
];
|
|
};
|
|
|
|
# A useful transitional step before other tests, and tests behaviour
|
|
# of removing an extra domain from a cert.
|
|
certundo.configuration = { };
|
|
|
|
# Tests these features:
|
|
# - enableACME behaves as expected
|
|
# - serverAliases are appended to extraDomainNames
|
|
# - Correct routing to the specific virtualHost for a cert
|
|
# Inherits previous test config
|
|
zeroconf.configuration = {
|
|
services.${serverName}.virtualHosts."zeroconf.${domain}" = {
|
|
addSSL = true;
|
|
enableACME = true;
|
|
serverAliases = [ "zeroconf2.${domain}" ];
|
|
};
|
|
};
|
|
|
|
# Test that serverAliases are correctly removed which triggers
|
|
# cert regeneration and service reload.
|
|
rmalias.configuration = {
|
|
services.${serverName}.virtualHosts."zeroconf.${domain}" = {
|
|
addSSL = true;
|
|
enableACME = true;
|
|
};
|
|
};
|
|
|
|
# Test that "acmeRoot = null" still results in
|
|
# valid cert generation by inheriting defaults.
|
|
nullroot.configuration = {
|
|
# The default.nix has the server-type dependent config statements
|
|
# to properly set up the proxying. We need a separate port here to
|
|
# avoid hostname issues with the proxy already running on :8080
|
|
security.acme.defaults.listenHTTP = ":8081";
|
|
services.${serverName}.virtualHosts."nullroot.${domain}" = {
|
|
addSSL = true;
|
|
enableACME = true;
|
|
acmeRoot = null;
|
|
};
|
|
};
|
|
|
|
# Test that a adding a second virtual host will not trigger
|
|
# other units (account and renewal service for first)
|
|
zeroconf3.configuration = {
|
|
services.${serverName}.virtualHosts = {
|
|
"zeroconf.${domain}" = {
|
|
addSSL = true;
|
|
enableACME = true;
|
|
serverAliases = [ "zeroconf2.${domain}" ];
|
|
};
|
|
"zeroconf3.${domain}" = {
|
|
addSSL = true;
|
|
enableACME = true;
|
|
};
|
|
};
|
|
# We're doing something risky with the combination of the service unit being persistent
|
|
# that could end up that the timers do not trigger properly. Show that timers have the
|
|
# desired effect.
|
|
systemd.timers."acme-renew-zeroconf3.${domain}".timerConfig = {
|
|
OnCalendar = lib.mkForce "*-*-* *:*:0/5";
|
|
AccuracySec = lib.mkForce 0;
|
|
# Skew randomly within the day, per https://letsencrypt.org/docs/integration-guide/.
|
|
RandomizedDelaySec = lib.mkForce 0;
|
|
FixedRandomDelay = lib.mkForce 0;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
testScript =
|
|
{ nodes, ... }:
|
|
''
|
|
${(import ./utils.nix).pythonUtils}
|
|
|
|
domain = "${domain}"
|
|
ca_domain = "${nodes.acme.test-support.acme.caDomain}"
|
|
fqdn = f"proxied.{domain}"
|
|
|
|
webserver.start()
|
|
webserver.wait_for_unit("${serverName}.service")
|
|
|
|
with subtest("Can run on self-signed certificates"):
|
|
check_issuer(webserver, fqdn, "minica")
|
|
# Check that the web server has picked up the selfsigned cert
|
|
check_connection(webserver, fqdn, minica=True)
|
|
|
|
acme.start()
|
|
wait_for_running(acme)
|
|
acme.wait_for_open_port(443)
|
|
|
|
with subtest("Acquire a cert through a proxied lego"):
|
|
webserver.succeed(f"systemctl start acme-order-renew-{fqdn}.service")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
download_ca_certs(webserver, ca_domain)
|
|
check_issuer(webserver, fqdn, "pebble")
|
|
check_connection(webserver, fqdn)
|
|
|
|
with subtest("security.acme changes reflect on web server part 1"):
|
|
check_connection(webserver, f"certchange.{domain}", fail=True)
|
|
switch_to(webserver, "certchange")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
check_connection(webserver, f"certchange.{domain}")
|
|
check_connection(webserver, fqdn)
|
|
|
|
with subtest("security.acme changes reflect on web server part 2"):
|
|
check_connection(webserver, f"certchange.{domain}")
|
|
switch_to(webserver, "certundo")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
check_connection(webserver, f"certchange.{domain}", fail=True)
|
|
check_connection(webserver, fqdn)
|
|
|
|
with subtest("Zero configuration SSL certificates for a vhost"):
|
|
check_connection(webserver, f"zeroconf.{domain}", fail=True)
|
|
switch_to(webserver, "zeroconf")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
check_connection(webserver, f"zeroconf.{domain}")
|
|
check_connection(webserver, f"zeroconf2.{domain}")
|
|
check_connection(webserver, fqdn)
|
|
|
|
with subtest("Removing an alias from a vhost"):
|
|
check_connection(webserver, f"zeroconf2.{domain}")
|
|
switch_to(webserver, "rmalias")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
check_connection(webserver, f"zeroconf2.{domain}", fail=True)
|
|
check_connection(webserver, f"zeroconf.{domain}")
|
|
check_connection(webserver, fqdn)
|
|
|
|
with subtest("Create cert using inherited default validation mechanism"):
|
|
check_connection(webserver, f"nullroot.{domain}", fail=True)
|
|
switch_to(webserver, "nullroot")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
check_connection(webserver, f"nullroot.{domain}")
|
|
|
|
with subtest("Ensure that adding a second vhost does not trigger first vhost acme units"):
|
|
switch_to(webserver, "zeroconf")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme")
|
|
switch_to(webserver, "zeroconf3")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
output = webserver.succeed("journalctl --cursor-file=/tmp/cursor | grep acme")
|
|
# The new certificate unit gets triggered:
|
|
t.assertIn(f"acme-zeroconf3.{domain}-start", output)
|
|
# The account generation should not be triggered again:
|
|
t.assertNotIn("acme-account-d590213ed52603e9128d.target", output)
|
|
# The other certificates should also not be triggered:
|
|
t.assertNotIn(f"acme-zeroconf.{domain}-start", output)
|
|
t.assertNotIn(f"acme-proxied.{domain}-start", output)
|
|
# Ensure the timer works, due to our shenanigans with
|
|
# RemainAfterExit=true
|
|
webserver.wait_until_succeeds(f"journalctl --cursor-file=/tmp/cursor | grep 'Starting Order (and renew) ACME certificate for zeroconf3.{domain}...'")
|
|
''
|
|
+
|
|
lib.optionalString
|
|
(config.nodes.webserver.services.nginx.enable && config.nodes.webserver.services.nginx.enableReload)
|
|
''
|
|
with subtest("Ensure that adding a second vhost does not restart nginx"):
|
|
switch_to(webserver, "zeroconf")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
webserver.succeed("journalctl --cursor-file=/tmp/cursor")
|
|
switch_to(webserver, "zeroconf3")
|
|
webserver.wait_for_unit("renew-triggered.target")
|
|
output = webserver.succeed("journalctl --cursor-file=/tmp/cursor")
|
|
t.assertNotIn("Stopping Nginx Web Server...", output)
|
|
'';
|
|
}
|