I’m writing a test using pkgs.testers.runNixOSTest for my own server where I have a fixture ACME server to provide certificates. I use the certificates in Caddy like so:
{
services.caddy.virtualHosts.${cfg.domain} = ''
tls "${config.security.acme.certs.${cfg.domain}.directory}/fullchain.pem" "${config.security.acme.certs.${cfg.domain}.directory}/key.pem"
reverse_proxy unix/${some-socket}
handle /.well-known/acme-challenge* {
root * ${cfg.webroot}
file_server
}
'';
security.acme.certs.${cfg.domain} = {
inherit (cfg) domain webroot;
inherit group;
reloadServices = ["caddy.service"];
};
}
In order for the testing server to be issued certificates, I added a fake ACME and fake DNS servers, and the "${inputs.nixpkgs}/nixos/tests/common/acme/client" module to the server so it trusts the root certificate coming from acme:
# nodes
let
dnsServerIP = nodes: nodes.dnsserver.networking.primaryIPAddress;
in
{
dnsserver = {nodes, ...}: {
networking = {
firewall.allowedTCPPorts = [
8055
53
];
firewall.allowedUDPPorts = [53];
# nixos/lib/testing/network.nix will provide name resolution via /etc/hosts
# for all nodes based on their host names and domain
hostName = "dnsserver";
domain = "test";
};
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.server.networking.primaryIPAddress}'";
# Required to bind on privileged ports.
AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
};
};
};
acme = {
nodes,
config,
modulesPath,
pkgs,
lib,
inputs,
...
}: {
imports = [
"${inputs.nixpkgs}/nixos/tests/common/acme/server"
(modulesPath + "/profiles/qemu-guest.nix")
];
networking.nameservers = lib.mkForce [(dnsServerIP nodes)];
};
# Web interface node
server = {
nodes,
modulesPath,
config,
pkgs,
lib,
inputs,
...
}: {
imports = [
"${inputs.nixpkgs}/nixos/tests/common/acme/client"
(modulesPath + "/profiles/qemu-guest.nix")
];
networking.nameservers = lib.mkForce [(dnsServerIP nodes)];
system.production = lib.mkForce true;
services = {
caddy.acmeCA = config.security.acme.defaults.server;
};
};
};
}
After server starts running, I ran openssl s_client -showcerts -connect subdomain.example.com:443 to check that the snakeoil ACME server produced trusted certificates, but the result is
CONNECTED(00000003)
---
Certificate chain
0 s:
i:CN=Pebble Intermediate CA 1c7953
a:PKEY: EC, (prime256v1); sigalg: sha256WithRSAEncryption
v:NotBefore: Mar 15 07:00:28 2026 GMT; NotAfter: Mar 15 07:00:27 2031 GMT
-----BEGIN CERTIFICATE-----
MIICr..... (deleted because its too long)
-----END CERTIFICATE-----
1 s:CN=Pebble Intermediate CA 1c7953
i:CN=Pebble Root CA 4b1bb2
a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: Mar 15 07:00:00 2026 GMT; NotAfter: Mar 15 07:00:00 2056 GMT
-----BEGIN CERTIFICATE-----
MIIDU... (deleted because its too long)
-----END CERTIFICATE-----
---
Server certificate
subject=
issuer=CN=Pebble Intermediate CA 1c7953
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ecdsa_secp256r1_sha256
Negotiated TLS1.3 group: X25519MLKEM768
---
SSL handshake has read 2994 bytes and written 1606 bytes
Verification error: unable to get local issuer certificate
The most notable thing here is that no certificate in this chain of certificates matches the snakeoil in nixos/tests/common/acme/server. All acme-order-renew-... services on server exited gracefully, which would not happen had acme not exist.
server # [ 26.760672] systemd[1]: acme-order-renew-subdomain.example.com.service: Deactivated successfully.
Why does the certificate chain not contain the trusted snakeoil?
See here for a MWE.
On letsencrypt discourse, a commenter said that pebble generates a new cert every time it starts up. This poses a problem for my tests since I have another service that validates the SSL certificate generated by an ACME server.