1{ pkgs, ... }:
2
3let
4 inherit (import ./ssh-keys.nix pkgs)
5 snakeOilPrivateKey
6 snakeOilPublicKey
7 snakeOilEd25519PrivateKey
8 snakeOilEd25519PublicKey
9 ;
10in
11{
12 name = "openssh";
13 meta = with pkgs.lib.maintainers; {
14 maintainers = [ aszlig ];
15 };
16
17 nodes = {
18
19 server =
20 { ... }:
21
22 {
23 services.openssh.enable = true;
24 security.pam.services.sshd.limits = [
25 {
26 domain = "*";
27 item = "memlock";
28 type = "-";
29 value = 1024;
30 }
31 ];
32 users.users.root.openssh.authorizedKeys.keys = [
33 snakeOilPublicKey
34 ];
35 };
36
37 server-allowed-users =
38 { ... }:
39
40 {
41 services.openssh = {
42 enable = true;
43 settings.AllowUsers = [
44 "alice"
45 "bob"
46 ];
47 };
48 users.groups = {
49 alice = { };
50 bob = { };
51 carol = { };
52 };
53 users.users = {
54 alice = {
55 isNormalUser = true;
56 group = "alice";
57 openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
58 };
59 bob = {
60 isNormalUser = true;
61 group = "bob";
62 openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
63 };
64 carol = {
65 isNormalUser = true;
66 group = "carol";
67 openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
68 };
69 };
70 };
71
72 server-lazy =
73 { ... }:
74
75 {
76 services.openssh = {
77 enable = true;
78 startWhenNeeded = true;
79 };
80 security.pam.services.sshd.limits = [
81 {
82 domain = "*";
83 item = "memlock";
84 type = "-";
85 value = 1024;
86 }
87 ];
88 users.users.root.openssh.authorizedKeys.keys = [
89 snakeOilPublicKey
90 ];
91 };
92
93 server-lazy-socket = {
94 virtualisation.vlans = [
95 1
96 2
97 ];
98 services.openssh = {
99 enable = true;
100 startWhenNeeded = true;
101 ports = [ 2222 ];
102 listenAddresses = [ { addr = "0.0.0.0"; } ];
103 };
104 users.users.root.openssh.authorizedKeys.keys = [
105 snakeOilPublicKey
106 ];
107 };
108
109 server-localhost-only =
110 { ... }:
111
112 {
113 services.openssh = {
114 enable = true;
115 listenAddresses = [
116 {
117 addr = "127.0.0.1";
118 port = 22;
119 }
120 ];
121 };
122 };
123
124 server-localhost-only-lazy =
125 { ... }:
126
127 {
128 services.openssh = {
129 enable = true;
130 startWhenNeeded = true;
131 listenAddresses = [
132 {
133 addr = "127.0.0.1";
134 port = 22;
135 }
136 ];
137 };
138 };
139
140 server-match-rule =
141 { ... }:
142
143 {
144 services.openssh = {
145 enable = true;
146 listenAddresses = [
147 {
148 addr = "127.0.0.1";
149 port = 22;
150 }
151 {
152 addr = "[::]";
153 port = 22;
154 }
155 ];
156 extraConfig = ''
157 # Combined test for two (predictable) Match criterias
158 Match LocalAddress 127.0.0.1 LocalPort 22
159 PermitRootLogin yes
160
161 # Separate tests for Match criterias
162 Match User root
163 PermitRootLogin yes
164 Match Group root
165 PermitRootLogin yes
166 Match Host nohost.example
167 PermitRootLogin yes
168 Match LocalAddress 127.0.0.1
169 PermitRootLogin yes
170 Match LocalPort 22
171 PermitRootLogin yes
172 Match RDomain nohost.example
173 PermitRootLogin yes
174 Match Address 127.0.0.1
175 PermitRootLogin yes
176 '';
177 };
178 };
179
180 server-no-openssl =
181 { ... }:
182 {
183 services.openssh = {
184 enable = true;
185 package = pkgs.opensshPackages.openssh.override {
186 linkOpenssl = false;
187 };
188 hostKeys = [
189 {
190 type = "ed25519";
191 path = "/etc/ssh/ssh_host_ed25519_key";
192 }
193 ];
194 settings = {
195 # Since this test is against an OpenSSH-without-OpenSSL,
196 # we have to override NixOS's defaults ciphers (which require OpenSSL)
197 # and instead set these to null, which will mean OpenSSH uses its defaults.
198 # Expectedly, OpenSSH's defaults don't require OpenSSL when it's compiled
199 # without OpenSSL.
200 Ciphers = null;
201 KexAlgorithms = null;
202 Macs = null;
203 };
204 };
205 users.users.root.openssh.authorizedKeys.keys = [
206 snakeOilEd25519PublicKey
207 ];
208 };
209
210 server-no-pam =
211 { pkgs, ... }:
212 {
213 services.openssh = {
214 enable = true;
215 package = pkgs.opensshPackages.openssh.override {
216 withPAM = false;
217 };
218 settings = {
219 UsePAM = false;
220 };
221 };
222 users.users.root.openssh.authorizedKeys.keys = [
223 snakeOilPublicKey
224 ];
225 };
226
227 server-sftp =
228 { pkgs, ... }:
229 {
230 services.openssh = {
231 enable = true;
232 extraConfig = ''
233 Match Group sftponly
234 ChrootDirectory /srv/sftp
235 ForceCommand internal-sftp
236 '';
237 };
238
239 users.groups = {
240 sftponly = { };
241 };
242 users.users = {
243 alice = {
244 isNormalUser = true;
245 createHome = false;
246 group = "sftponly";
247 shell = "/run/current-system/sw/bin/nologin";
248 openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
249 };
250 };
251 };
252
253 client =
254 { ... }:
255 {
256 virtualisation.vlans = [
257 1
258 2
259 ];
260 };
261
262 };
263
264 testScript = ''
265 start_all()
266
267 server.wait_for_unit("sshd", timeout=30)
268 server_allowed_users.wait_for_unit("sshd", timeout=30)
269 server_localhost_only.wait_for_unit("sshd", timeout=30)
270 server_match_rule.wait_for_unit("sshd", timeout=30)
271 server_no_openssl.wait_for_unit("sshd", timeout=30)
272 server_no_pam.wait_for_unit("sshd", timeout=30)
273 server_sftp.wait_for_unit("sshd", timeout=30)
274
275 server_lazy.wait_for_unit("sshd.socket", timeout=30)
276 server_localhost_only_lazy.wait_for_unit("sshd.socket", timeout=30)
277 server_lazy_socket.wait_for_unit("sshd.socket", timeout=30)
278
279 with subtest("manual-authkey"):
280 client.succeed(
281 '${pkgs.openssh}/bin/ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""'
282 )
283 public_key = client.succeed(
284 "${pkgs.openssh}/bin/ssh-keygen -y -f /root/.ssh/id_ed25519"
285 )
286 public_key = public_key.strip()
287 client.succeed("chmod 600 /root/.ssh/id_ed25519")
288
289 server.succeed("echo '{}' > /root/.ssh/authorized_keys".format(public_key))
290 server_lazy.succeed("echo '{}' > /root/.ssh/authorized_keys".format(public_key))
291
292 client.wait_for_unit("network.target")
293 client.succeed(
294 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'echo hello world' >&2",
295 timeout=30
296 )
297 client.succeed(
298 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'ulimit -l' | grep 1024",
299 timeout=30
300 )
301
302 client.succeed(
303 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server-lazy 'echo hello world' >&2",
304 timeout=30
305 )
306 client.succeed(
307 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server-lazy 'ulimit -l' | grep 1024",
308 timeout=30
309 )
310
311 with subtest("socket activation on a non-standard port"):
312 client.succeed(
313 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
314 )
315 client.succeed("chmod 600 privkey.snakeoil")
316 # The final segment in this IP is allocated according to the alphabetical order of machines in this test.
317 client.succeed(
318 "ssh -p 2222 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil root@192.168.2.5 true",
319 timeout=30
320 )
321
322 with subtest("configured-authkey"):
323 client.succeed(
324 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
325 )
326 client.succeed("chmod 600 privkey.snakeoil")
327 client.succeed(
328 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server true",
329 timeout=30
330 )
331 client.succeed(
332 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server-lazy true",
333 timeout=30
334 )
335
336 with subtest("localhost-only"):
337 server_localhost_only.succeed("ss -nlt | grep '127.0.0.1:22'")
338 server_localhost_only_lazy.succeed("ss -nlt | grep '127.0.0.1:22'")
339
340 with subtest("match-rules"):
341 server_match_rule.succeed("ss -nlt | grep '127.0.0.1:22'")
342
343 with subtest("allowed-users"):
344 client.succeed(
345 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
346 )
347 client.succeed("chmod 600 privkey.snakeoil")
348 client.succeed(
349 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil alice@server-allowed-users true",
350 timeout=30
351 )
352 client.succeed(
353 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil bob@server-allowed-users true",
354 timeout=30
355 )
356 client.fail(
357 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil carol@server-allowed-users true",
358 timeout=30
359 )
360
361 with subtest("no-openssl"):
362 client.succeed(
363 "cat ${snakeOilEd25519PrivateKey} > privkey.snakeoil"
364 )
365 client.succeed("chmod 600 privkey.snakeoil")
366 client.succeed(
367 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server-no-openssl true",
368 timeout=30
369 )
370
371 with subtest("no-pam"):
372 client.succeed(
373 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
374 )
375 client.succeed("chmod 600 privkey.snakeoil")
376 client.succeed(
377 "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil server-no-pam true",
378 timeout=30
379 )
380
381 with subtest("sftp"):
382 server_sftp.succeed(
383 "mkdir -p /srv/sftp/uploads"
384 )
385 server_sftp.succeed(
386 "chown alice:sftponly /srv/sftp/uploads"
387 )
388 server_sftp.succeed(
389 "chmod 0755 /srv/sftp/uploads"
390 )
391
392 client.succeed(
393 "cat ${snakeOilPrivateKey} > privkey.snakeoil"
394 )
395 client.succeed("chmod 600 privkey.snakeoil")
396
397 client.succeed(
398 "echo 'hello-sftp-world' > test-file"
399 )
400 client.succeed(
401 "echo 'put test-file uploads/' > put-batch-file"
402 )
403
404 client.succeed(
405 "sftp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil -b put-batch-file alice@server-sftp",
406 timeout=30
407 )
408
409 server_sftp.wait_for_file("/srv/sftp/uploads/test-file")
410
411 # None of the per-connection units should have failed.
412 server_lazy.fail("systemctl is-failed 'sshd@*.service'")
413 '';
414}