···
commonConfig = ./common/acme/client;
dnsServerIP = nodes: nodes.dnsserver.config.networking.primaryIPAddress;
-
dnsScript = {pkgs, nodes}: let
dnsAddress = dnsServerIP nodes;
in pkgs.writeShellScript "dns-hook.sh" ''
···
-
documentRoot = pkgs: pkgs.runCommand "docroot" {} ''
echo hello world > "$out/index.html"
-
locations."/".root = documentRoot pkgs;
-
in import ./make-test-python.nix ({ lib, ... }: {
meta.maintainers = lib.teams.acme.members;
# The fake ACME server which will respond to client requests
-
acme = { nodes, lib, ... }: {
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, ... }: {
networking.firewall.allowedTCPPorts = [ 8055 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
systemd.services.pebble-challtestsrv = {
···
# A web server which will be the node requesting certs
-
webserver = { pkgs, nodes, lib, config, ... }: {
imports = [ commonConfig ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
networking.firewall.allowedTCPPorts = [ 80 443 ];
···
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) // {
-
# 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
-
test -e accounts/${caDomain}/${email}/account.json || exit 99
-
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) // {
-
services.nginx.virtualHosts."c.example.test" = (vhostBase pkgs) // {
-
# 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 = ''
-
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";
-
specialisation.ocsp-stapling.configuration = { pkgs, ... }: {
-
security.acme.certs."a.example.test" = {
-
services.nginx.virtualHosts."a.example.com" = {
-
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" ];
-
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;
-
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.
-
# Reproducer for https://github.com/NixOS/nixpkgs/issues/81842
-
specialisation.slow-startup.configuration = { pkgs, config, nodes, lib, ... }: {
-
systemd.services.my-slow-service = {
-
wantedBy = [ "multi-user.target" "nginx.service" ];
-
before = [ "nginx.service" ];
-
script = "${pkgs.python3}/bin/python -m http.server";
-
services.nginx.virtualHosts."slow.example.com" = {
-
locations."/".proxyPass = "http://localhost:8000";
# The client will be used to curl the webserver to validate configuration
-
client = {nodes, lib, pkgs, ...}: {
imports = [ commonConfig ];
networking.nameservers = lib.mkForce [ (dnsServerIP nodes) ];
···
-
testScript = {nodes, ...}:
caDomain = nodes.acme.config.test-support.acme.caDomain;
newServerSystem = nodes.webserver.config.system.build.toplevel;
···
# 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
# reaches the active state. Targets do not have this issue.
-
def switch_to(node, name):
-
f"/run/current-system/specialisation/{name}/bin/switch-to-configuration test"
···
return download_ca_certs(node, retries - 1)
dnsserver.wait_for_unit("pebble-challtestsrv.service")
client.wait_for_unit("default.target")
···
'curl --data \'{"host": "${caDomain}", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${dnsServerIP nodes}:8055/add-a'
acme.wait_for_unit("network-online.target")
acme.wait_for_unit("pebble.service")
download_ca_certs(client)
with subtest("Can request certificate with HTTPS-01 challenge"):
webserver.wait_for_unit("acme-finished-a.example.test.target")
with subtest("Certificates and accounts have safe + valid permissions"):
-
group = "${nodes.webserver.config.security.acme.certs."a.example.test".group}"
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 $(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
with subtest("Can generate valid selfsigned certs"):
webserver.succeed("systemctl clean acme-a.example.test.service --what=state")
···
# Will succeed if nginx can load the certs
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"):
switch_to(webserver, "ocsp-stapling")
webserver.wait_for_unit("acme-finished-a.example.test.target")
check_stapling(client, "a.example.test")
with subtest("Can request certificate with HTTPS-01 when nginx startup is delayed"):
switch_to(webserver, "slow-startup")
webserver.wait_for_unit("acme-finished-slow.example.com.target")
check_issuer(webserver, "slow.example.com", "pebble")
check_connection(client, "slow.example.com")
-
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, "b.example.test")
-
with subtest("Can remove extra domains from a cert"):
-
switch_to(webserver, "remove-extra-domain")
-
webserver.wait_for_unit("acme-finished-a.example.test.target")
-
webserver.wait_for_unit("nginx.service")
-
check_connection(client, "a.example.test")
-
rc, _ = client.execute(
-
"openssl s_client -CAfile /tmp/ca.crt -connect b.example.test:443"
-
" </dev/null 2>/dev/null | openssl x509 -noout -text"
-
" | grep DNS: | grep b.example.test"
-
assert rc > 0, "Removed extraDomainName was not removed from the cert"
-
with subtest("Can request certificates for vhost + aliases (apache-httpd)"):
-
switch_to(webserver, "httpd-aliases")
-
webserver.wait_for_unit("acme-finished-c.example.test.target")
-
except Exception as err:
-
_, output = webserver.execute(
-
"cat /var/log/httpd/*.log && ls -al /var/lib/acme/acme-challenge"
-
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"):
-
# Switch to selfsigned first
-
webserver.succeed("systemctl clean acme-c.example.test.service --what=state")
-
webserver.succeed("systemctl start acme-selfsigned-c.example.test.service")
-
check_issuer(webserver, "c.example.test", "minica")
-
webserver.succeed("systemctl start httpd-config-reload.service")
-
webserver.succeed("systemctl start test-renew-httpd.target")
-
check_issuer(webserver, "c.example.test", "pebble")
-
check_connection(client, "c.example.test")
-
with subtest("Can request wildcard certificates using DNS-01 challenge"):
-
switch_to(webserver, "dns-01")
-
webserver.wait_for_unit("acme-finished-example.test.target")
-
check_issuer(webserver, "example.test", "pebble")
-
check_connection(client, "dns.example.test")