1{
2 serverName,
3 group,
4 baseModule,
5 domain ? "example.test",
6}:
7{
8 config,
9 lib,
10 pkgs,
11 ...
12}:
13{
14 name = serverName;
15 meta = {
16 maintainers = lib.teams.acme.members;
17 # Hard timeout in seconds. Average run time is about 100 seconds.
18 timeout = 300;
19 };
20
21 nodes = {
22 # The fake ACME server which will respond to client requests
23 acme =
24 { nodes, ... }:
25 {
26 imports = [ ../common/acme/server ];
27 };
28
29 webserver =
30 { nodes, ... }:
31 {
32 imports = [
33 ../common/acme/client
34 baseModule
35 ];
36 networking.domain = domain;
37 networking.firewall.allowedTCPPorts = [
38 80
39 443
40 ];
41
42 # Resolve the vhosts the easy way
43 networking.hosts."127.0.0.1" = [
44 "proxied.${domain}"
45 "certchange.${domain}"
46 "zeroconf.${domain}"
47 "zeroconf2.${domain}"
48 "nullroot.${domain}"
49 ];
50
51 # OpenSSL will be used for more thorough certificate validation
52 environment.systemPackages = [ pkgs.openssl ];
53
54 # Used to determine if service reload was triggered.
55 # This does not provide a guarantee that the webserver is finished reloading,
56 # to handle that there is retry logic wrapping any connectivity checks.
57 systemd.targets."renew-triggered" = {
58 wantedBy = [ "${serverName}-config-reload.service" ];
59 after = [ "${serverName}-config-reload.service" ];
60 };
61
62 security.acme.certs."proxied.${domain}" = {
63 listenHTTP = ":8080";
64 group = group;
65 };
66
67 specialisation = {
68 # Test that the web server is correctly reloaded when the cert changes
69 certchange.configuration = {
70 security.acme.certs."proxied.${domain}".extraDomainNames = [
71 "certchange.${domain}"
72 ];
73 };
74
75 # A useful transitional step before other tests, and tests behaviour
76 # of removing an extra domain from a cert.
77 certundo.configuration = { };
78
79 # Tests these features:
80 # - enableACME behaves as expected
81 # - serverAliases are appended to extraDomainNames
82 # - Correct routing to the specific virtualHost for a cert
83 # Inherits previous test config
84 zeroconf.configuration = {
85 services.${serverName}.virtualHosts."zeroconf.${domain}" = {
86 addSSL = true;
87 enableACME = true;
88 serverAliases = [ "zeroconf2.${domain}" ];
89 };
90 };
91
92 # Test that serverAliases are correctly removed which triggers
93 # cert regeneration and service reload.
94 rmalias.configuration = {
95 services.${serverName}.virtualHosts."zeroconf.${domain}" = {
96 addSSL = true;
97 enableACME = true;
98 };
99 };
100
101 # Test that "acmeRoot = null" still results in
102 # valid cert generation by inheriting defaults.
103 nullroot.configuration = {
104 security.acme.defaults.listenHTTP = ":8080";
105 services.${serverName}.virtualHosts."nullroot.${domain}" = {
106 onlySSL = true;
107 enableACME = true;
108 acmeRoot = null;
109 };
110 };
111 };
112 };
113 };
114
115 testScript =
116 { nodes, ... }:
117 ''
118 ${(import ./utils.nix).pythonUtils}
119
120 domain = "${domain}"
121 ca_domain = "${nodes.acme.test-support.acme.caDomain}"
122 fqdn = f"proxied.{domain}"
123
124 acme.start()
125 wait_for_running(acme)
126 acme.wait_for_open_port(443)
127
128 with subtest("Acquire a cert through a proxied lego"):
129 webserver.start()
130 webserver.succeed("systemctl is-system-running --wait")
131 wait_for_running(webserver)
132 download_ca_certs(webserver, ca_domain)
133 check_connection(webserver, fqdn)
134
135 with subtest("Can run on selfsigned certificates"):
136 # Switch to selfsigned first
137 webserver.succeed(f"systemctl clean acme-{fqdn}.service --what=state")
138 webserver.succeed(f"systemctl start acme-selfsigned-{fqdn}.service")
139 check_issuer(webserver, fqdn, "minica")
140 webserver.succeed("systemctl restart ${serverName}-config-reload.service")
141 # Check that the web server has picked up the selfsigned cert
142 check_connection(webserver, fqdn, minica=True)
143 webserver.succeed("systemctl stop renew-triggered.target")
144 webserver.succeed(f"systemctl start acme-{fqdn}.service")
145 webserver.wait_for_unit("renew-triggered.target")
146 check_issuer(webserver, fqdn, "pebble")
147 check_connection(webserver, fqdn)
148
149 with subtest("security.acme changes reflect on web server part 1"):
150 check_connection(webserver, f"certchange.{domain}", fail=True)
151 switch_to(webserver, "certchange")
152 webserver.wait_for_unit("renew-triggered.target")
153 check_connection(webserver, f"certchange.{domain}")
154 check_connection(webserver, fqdn)
155
156 with subtest("security.acme changes reflect on web server part 2"):
157 check_connection(webserver, f"certchange.{domain}")
158 switch_to(webserver, "certundo")
159 webserver.wait_for_unit("renew-triggered.target")
160 check_connection(webserver, f"certchange.{domain}", fail=True)
161 check_connection(webserver, fqdn)
162
163 with subtest("Zero configuration SSL certificates for a vhost"):
164 check_connection(webserver, f"zeroconf.{domain}", fail=True)
165 switch_to(webserver, "zeroconf")
166 webserver.wait_for_unit("renew-triggered.target")
167 check_connection(webserver, f"zeroconf.{domain}")
168 check_connection(webserver, f"zeroconf2.{domain}")
169 check_connection(webserver, fqdn)
170
171 with subtest("Removing an alias from a vhost"):
172 check_connection(webserver, f"zeroconf2.{domain}")
173 switch_to(webserver, "rmalias")
174 webserver.wait_for_unit("renew-triggered.target")
175 check_connection(webserver, f"zeroconf2.{domain}", fail=True)
176 check_connection(webserver, f"zeroconf.{domain}")
177 check_connection(webserver, fqdn)
178
179 with subtest("Create cert using inherited default validation mechanism"):
180 check_connection(webserver, f"nullroot.{domain}", fail=True)
181 switch_to(webserver, "nullroot")
182 webserver.wait_for_unit("renew-triggered.target")
183 check_connection(webserver, f"nullroot.{domain}")
184 '';
185}