nixpkgs/nixos/tests/acme.nix
Emily d0f04c1623 nixos/tests/acme: use *.test domains
Shimming out the Let's Encrypt domain name to reuse client configuration
doesn't work properly (Pebble uses different endpoint URL formats), is
recommended against by upstream,[1] and is unnecessary now that the ACME
module supports specifying an ACME server. This commit changes the tests
to use the domain name acme.test instead, and renames the letsencrypt
node to acme to reflect that it has nothing to do with the ACME server
that Let's Encrypt runs. The imports are renamed for clarity:

* nixos/tests/common/{letsencrypt => acme}/{common.nix => client}
* nixos/tests/common/{letsencrypt => acme}/{default.nix => server}

The test's other domain names are also adjusted to use *.test for
consistency (and to avoid misuse of non-reserved domain names such
as standalone.com).

[1] https://github.com/letsencrypt/pebble/issues/283#issuecomment-545123242

Co-authored-by: Yegor Timoshenko <yegortimoshenko@riseup.net>
2020-04-18 05:15:47 +01:00

212 lines
8.3 KiB
Nix

let
commonConfig = ./common/acme/client;
dnsScript = {writeScript, dnsAddress, bash, curl}: writeScript "dns-hook.sh" ''
#!${bash}/bin/bash
set -euo pipefail
echo '[INFO]' "[$2]" 'dns-hook.sh' $*
if [ "$1" = "present" ]; then
${curl}/bin/curl --data '{"host": "'"$2"'", "value": "'"$3"'"}' http://${dnsAddress}:8055/set-txt
else
${curl}/bin/curl --data '{"host": "'"$2"'"}' http://${dnsAddress}:8055/clear-txt
fi
'';
in import ./make-test-python.nix {
name = "acme";
nodes = rec {
acme = { nodes, lib, ... }: {
imports = [ ./common/acme/server ];
networking.nameservers = lib.mkForce [
nodes.dnsserver.config.networking.primaryIPAddress
];
};
dnsserver = { nodes, pkgs, ... }: {
networking.firewall.allowedTCPPorts = [ 8055 53 ];
networking.firewall.allowedUDPPorts = [ 53 ];
systemd.services.pebble-challtestsrv = {
enable = true;
description = "Pebble ACME challenge test server";
wantedBy = [ "network.target" ];
serviceConfig = {
ExecStart = "${pkgs.pebble}/bin/pebble-challtestsrv -dns01 ':53' -defaultIPv6 '' -defaultIPv4 '${nodes.webserver.config.networking.primaryIPAddress}'";
# Required to bind on privileged ports.
User = "root";
Group = "root";
};
};
};
acmeStandalone = { nodes, lib, config, pkgs, ... }: {
imports = [ commonConfig ];
networking.nameservers = lib.mkForce [
nodes.dnsserver.config.networking.primaryIPAddress
];
networking.firewall.allowedTCPPorts = [ 80 ];
security.acme.certs."standalone.test" = {
webroot = "/var/lib/acme/acme-challenges";
};
systemd.targets."acme-finished-standalone.test" = {};
systemd.services."acme-standalone.test" = {
wants = [ "acme-finished-standalone.test.target" ];
before = [ "acme-finished-standalone.test.target" ];
};
services.nginx.enable = true;
services.nginx.virtualHosts."standalone.test" = {
locations."/.well-known/acme-challenge".root = "/var/lib/acme/acme-challenges";
};
};
webserver = { nodes, config, pkgs, lib, ... }: {
imports = [ commonConfig ];
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.nameservers = lib.mkForce [
nodes.dnsserver.config.networking.primaryIPAddress
];
# A target remains active. Use this to probe the fact that
# a service fired eventhough it is not RemainAfterExit
systemd.targets."acme-finished-a.example.test" = {};
systemd.services."acme-a.example.test" = {
wants = [ "acme-finished-a.example.test.target" ];
before = [ "acme-finished-a.example.test.target" ];
after = [ "nginx.service" ];
};
services.nginx.enable = true;
services.nginx.virtualHosts."a.example.test" = {
enableACME = true;
forceSSL = true;
locations."/".root = pkgs.runCommand "docroot" {} ''
mkdir -p "$out"
echo hello world > "$out/index.html"
'';
};
security.acme.server = "https://acme.test/dir";
specialisation.second-cert.configuration = {pkgs, ...}: {
systemd.targets."acme-finished-b.example.test" = {};
systemd.services."acme-b.example.test" = {
wants = [ "acme-finished-b.example.test.target" ];
before = [ "acme-finished-b.example.test.target" ];
after = [ "nginx.service" ];
};
services.nginx.virtualHosts."b.example.test" = {
enableACME = true;
forceSSL = true;
locations."/".root = pkgs.runCommand "docroot" {} ''
mkdir -p "$out"
echo hello world > "$out/index.html"
'';
};
};
specialisation.dns-01.configuration = {pkgs, config, nodes, lib, ...}: {
security.acme.certs."example.test" = {
domain = "*.example.test";
dnsProvider = "exec";
dnsPropagationCheck = false;
credentialsFile = with pkgs; writeText "wildcard.env" ''
EXEC_PATH=${dnsScript { inherit writeScript bash curl; dnsAddress = nodes.dnsserver.config.networking.primaryIPAddress; }}
'';
user = config.services.nginx.user;
group = config.services.nginx.group;
};
systemd.targets."acme-finished-example.test" = {};
systemd.services."acme-example.test" = {
wants = [ "acme-finished-example.test.target" ];
before = [ "acme-finished-example.test.target" "nginx.service" ];
wantedBy = [ "nginx.service" ];
};
services.nginx.virtualHosts."c.example.test" = {
forceSSL = true;
sslCertificate = config.security.acme.certs."example.test".directory + "/cert.pem";
sslTrustedCertificate = config.security.acme.certs."example.test".directory + "/full.pem";
sslCertificateKey = config.security.acme.certs."example.test".directory + "/key.pem";
locations."/".root = pkgs.runCommand "docroot" {} ''
mkdir -p "$out"
echo hello world > "$out/index.html"
'';
};
};
};
client = {nodes, lib, ...}: {
imports = [ commonConfig ];
networking.nameservers = lib.mkForce [
nodes.dnsserver.config.networking.primaryIPAddress
];
};
};
testScript = {nodes, ...}:
let
newServerSystem = nodes.webserver.config.system.build.toplevel;
switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
in
# 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. To work around this, we create some mock target units which
# get pulled in by the oneshot units. The target units linger after activation, and hence we
# can use them to probe that a oneshot fired. It is a bit ugly, but it is the best we can do
''
client.start()
dnsserver.start()
acme.wait_for_unit("default.target")
dnsserver.wait_for_unit("pebble-challtestsrv.service")
client.succeed(
'curl --data \'{"host": "acme.test", "addresses": ["${nodes.acme.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a'
)
client.succeed(
'curl --data \'{"host": "standalone.test", "addresses": ["${nodes.acmeStandalone.config.networking.primaryIPAddress}"]}\' http://${nodes.dnsserver.config.networking.primaryIPAddress}:8055/add-a'
)
acme.start()
acmeStandalone.start()
acme.wait_for_unit("default.target")
acme.wait_for_unit("pebble.service")
with subtest("can request certificate with HTTPS-01 challenge"):
acmeStandalone.wait_for_unit("default.target")
acmeStandalone.succeed("systemctl start acme-standalone.test.service")
acmeStandalone.wait_for_unit("acme-finished-standalone.test.target")
client.wait_for_unit("default.target")
client.succeed("curl https://acme.test:15000/roots/0 > /tmp/ca.crt")
client.succeed("curl https://acme.test:15000/intermediate-keys/0 >> /tmp/ca.crt")
with subtest("Can request certificate for nginx service"):
webserver.wait_for_unit("acme-finished-a.example.test.target")
client.succeed(
"curl --cacert /tmp/ca.crt https://a.example.test/ | grep -qF 'hello world'"
)
with subtest("Can add another certificate for nginx service"):
webserver.succeed(
"/run/current-system/specialisation/second-cert/bin/switch-to-configuration test"
)
webserver.wait_for_unit("acme-finished-b.example.test.target")
client.succeed(
"curl --cacert /tmp/ca.crt https://b.example.test/ | grep -qF 'hello world'"
)
with subtest("Can request wildcard certificates using DNS-01 challenge"):
webserver.succeed(
"${switchToNewServer}"
)
webserver.succeed(
"/run/current-system/specialisation/dns-01/bin/switch-to-configuration test"
)
webserver.wait_for_unit("acme-finished-example.test.target")
client.succeed(
"curl --cacert /tmp/ca.crt https://c.example.test/ | grep -qF 'hello world'"
)
'';
}