1{ runTest }:
2
3let
4 stunnelCommon = {
5 services.stunnel = {
6 enable = true;
7 user = "stunnel";
8 };
9 users.groups.stunnel = { };
10 users.users.stunnel = {
11 isSystemUser = true;
12 group = "stunnel";
13 };
14 };
15 makeCert =
16 {
17 config,
18 lib,
19 pkgs,
20 ...
21 }:
22 {
23 systemd.services.create-test-cert = {
24 wantedBy = [ "sysinit.target" ];
25 before = [
26 "sysinit.target"
27 "shutdown.target"
28 ];
29 conflicts = [ "shutdown.target" ];
30 unitConfig.DefaultDependencies = false;
31 serviceConfig.Type = "oneshot";
32 script = ''
33 ${lib.getExe pkgs.openssl} req -batch -x509 -newkey rsa -nodes -out /test-cert.pem -keyout /test-key.pem -subj /CN=${config.networking.hostName}
34 ( umask 077; cat /test-key.pem /test-cert.pem > /test-key-and-cert.pem )
35 chown stunnel /test-key.pem /test-key-and-cert.pem
36 '';
37 };
38 };
39 serverCommon =
40 { lib, pkgs, ... }:
41 {
42 networking.firewall.allowedTCPPorts = [ 443 ];
43 services.stunnel.servers.https = {
44 accept = "443";
45 connect = 80;
46 cert = "/test-key-and-cert.pem";
47 };
48 systemd.services.simple-webserver = {
49 wantedBy = [ "multi-user.target" ];
50 script = ''
51 cd /etc/webroot
52 ${lib.getExe' pkgs.python3 "python"} -m http.server 80
53 '';
54 };
55 };
56 copyCert = src: dest: filename: ''
57 from shlex import quote
58 ${src}.wait_for_file("/test-key-and-cert.pem")
59 server_cert = ${src}.succeed("cat /test-cert.pem")
60 ${dest}.succeed("echo %s > ${filename}" % quote(server_cert))
61 '';
62in
63{
64 basicServer = runTest {
65 name = "basicServer";
66
67 nodes = {
68 client = { };
69 server = {
70 imports = [
71 makeCert
72 serverCommon
73 stunnelCommon
74 ];
75 environment.etc."webroot/index.html".text = "well met";
76 };
77 };
78
79 testScript = ''
80 start_all()
81
82 ${copyCert "server" "client" "/authorized-server-cert.crt"}
83
84 server.wait_for_unit("simple-webserver")
85 server.wait_for_unit("stunnel")
86
87 client.succeed("curl --fail --cacert /authorized-server-cert.crt https://server/ > out")
88 client.succeed('[[ "$(< out)" == "well met" ]]')
89 '';
90 };
91
92 serverAndClient = runTest {
93 name = "serverAndClient";
94
95 nodes = {
96 client = {
97 imports = [ stunnelCommon ];
98 services.stunnel.clients = {
99 httpsClient = {
100 accept = "80";
101 connect = "server:443";
102 CAFile = "/authorized-server-cert.crt";
103 };
104 httpsClientWithHostVerify = {
105 accept = "81";
106 connect = "server:443";
107 CAFile = "/authorized-server-cert.crt";
108 verifyHostname = "server";
109 };
110 httpsClientWithHostVerifyFail = {
111 accept = "82";
112 connect = "server:443";
113 CAFile = "/authorized-server-cert.crt";
114 verifyHostname = "wronghostname";
115 };
116 };
117 };
118 server = {
119 imports = [
120 makeCert
121 serverCommon
122 stunnelCommon
123 ];
124 environment.etc."webroot/index.html".text = "hello there";
125 };
126 };
127
128 testScript = ''
129 start_all()
130
131 ${copyCert "server" "client" "/authorized-server-cert.crt"}
132
133 server.wait_for_unit("simple-webserver")
134 server.wait_for_unit("stunnel")
135
136 # In case stunnel came up before we got the server's cert copied over
137 client.succeed("systemctl reload-or-restart stunnel")
138
139 client.succeed("curl --fail http://localhost/ > out")
140 client.succeed('[[ "$(< out)" == "hello there" ]]')
141
142 client.succeed("curl --fail http://localhost:81/ > out")
143 client.succeed('[[ "$(< out)" == "hello there" ]]')
144
145 client.fail("curl --fail http://localhost:82/ > out")
146 client.succeed('[[ "$(< out)" == "" ]]')
147 '';
148 };
149
150 mutualAuth = runTest {
151 name = "mutualAuth";
152
153 nodes = rec {
154 client = {
155 imports = [
156 makeCert
157 stunnelCommon
158 ];
159 services.stunnel.clients.authenticated-https = {
160 accept = "80";
161 connect = "server:443";
162 verifyPeer = true;
163 CAFile = "/authorized-server-cert.crt";
164 cert = "/test-cert.pem";
165 key = "/test-key.pem";
166 };
167 };
168 wrongclient = client;
169 server = {
170 imports = [
171 makeCert
172 serverCommon
173 stunnelCommon
174 ];
175 services.stunnel.servers.https = {
176 CAFile = "/authorized-client-certs.crt";
177 verifyPeer = true;
178 };
179 environment.etc."webroot/index.html".text = "secret handshake";
180 };
181 };
182
183 testScript = ''
184 start_all()
185
186 ${copyCert "server" "client" "/authorized-server-cert.crt"}
187 ${copyCert "client" "server" "/authorized-client-certs.crt"}
188 ${copyCert "server" "wrongclient" "/authorized-server-cert.crt"}
189
190 # In case stunnel came up before we got the cross-certs in place
191 client.succeed("systemctl reload-or-restart stunnel")
192 server.succeed("systemctl reload-or-restart stunnel")
193 wrongclient.succeed("systemctl reload-or-restart stunnel")
194
195 server.wait_for_unit("simple-webserver")
196 client.fail("curl --fail --insecure https://server/ > out")
197 client.succeed('[[ "$(< out)" == "" ]]')
198 client.succeed("curl --fail http://localhost/ > out")
199 client.succeed('[[ "$(< out)" == "secret handshake" ]]')
200 wrongclient.fail("curl --fail http://localhost/ > out")
201 wrongclient.succeed('[[ "$(< out)" == "" ]]')
202 '';
203 };
204}