1import ./make-test-python.nix {
2 name = "systemd-confinement";
3
4 machine = { pkgs, lib, ... }: let
5 testServer = pkgs.writeScript "testserver.sh" ''
6 #!${pkgs.runtimeShell}
7 export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"}
8 ${lib.escapeShellArg pkgs.runtimeShell} 2>&1
9 echo "exit-status:$?"
10 '';
11
12 testClient = pkgs.writeScriptBin "chroot-exec" ''
13 #!${pkgs.runtimeShell} -e
14 output="$(echo "$@" | nc -NU "/run/test$(< /teststep).sock")"
15 ret="$(echo "$output" | sed -nre '$s/^exit-status:([0-9]+)$/\1/p')"
16 echo "$output" | head -n -1
17 exit "''${ret:-1}"
18 '';
19
20 mkTestStep = num: { config ? {}, testScript }: {
21 systemd.sockets."test${toString num}" = {
22 description = "Socket for Test Service ${toString num}";
23 wantedBy = [ "sockets.target" ];
24 socketConfig.ListenStream = "/run/test${toString num}.sock";
25 socketConfig.Accept = true;
26 };
27
28 systemd.services."test${toString num}@" = {
29 description = "Confined Test Service ${toString num}";
30 confinement = (config.confinement or {}) // { enable = true; };
31 serviceConfig = (config.serviceConfig or {}) // {
32 ExecStart = testServer;
33 StandardInput = "socket";
34 };
35 } // removeAttrs config [ "confinement" "serviceConfig" ];
36
37 __testSteps = lib.mkOrder num (''
38 machine.succeed("echo ${toString num} > /teststep")
39 '' + testScript);
40 };
41
42 in {
43 imports = lib.imap1 mkTestStep [
44 { config.confinement.mode = "chroot-only";
45 testScript = ''
46 with subtest("chroot-only confinement"):
47 paths = machine.succeed('chroot-exec ls -1 / | paste -sd,').strip()
48 assert_eq(paths, "bin,nix,run")
49 uid = machine.succeed('chroot-exec id -u').strip()
50 assert_eq(uid, "0")
51 machine.succeed("chroot-exec chown 65534 /bin")
52 '';
53 }
54 { testScript = ''
55 with subtest("full confinement with APIVFS"):
56 machine.fail("chroot-exec ls -l /etc")
57 machine.fail("chroot-exec chown 65534 /bin")
58 assert_eq(machine.succeed('chroot-exec id -u').strip(), "0")
59 machine.succeed("chroot-exec chown 0 /bin")
60 '';
61 }
62 { config.serviceConfig.BindReadOnlyPaths = [ "/etc" ];
63 testScript = ''
64 with subtest("check existence of bind-mounted /etc"):
65 passwd = machine.succeed('chroot-exec cat /etc/passwd').strip()
66 assert len(passwd) > 0, "/etc/passwd must not be empty"
67 '';
68 }
69 { config.serviceConfig.User = "chroot-testuser";
70 config.serviceConfig.Group = "chroot-testgroup";
71 testScript = ''
72 with subtest("check if User/Group really runs as non-root"):
73 machine.succeed("chroot-exec ls -l /dev")
74 uid = machine.succeed('chroot-exec id -u').strip()
75 assert uid != "0", "UID of chroot-testuser shouldn't be 0"
76 machine.fail("chroot-exec touch /bin/test")
77 '';
78 }
79 (let
80 symlink = pkgs.runCommand "symlink" {
81 target = pkgs.writeText "symlink-target" "got me\n";
82 } "ln -s \"$target\" \"$out\"";
83 in {
84 config.confinement.packages = lib.singleton symlink;
85 testScript = ''
86 with subtest("check if symlinks are properly bind-mounted"):
87 machine.fail("chroot-exec test -e /etc")
88 text = machine.succeed('chroot-exec cat ${symlink}').strip()
89 assert_eq(text, "got me")
90 '';
91 })
92 { config.serviceConfig.User = "chroot-testuser";
93 config.serviceConfig.Group = "chroot-testgroup";
94 config.serviceConfig.StateDirectory = "testme";
95 testScript = ''
96 with subtest("check if StateDirectory works"):
97 machine.succeed("chroot-exec touch /tmp/canary")
98 machine.succeed('chroot-exec "echo works > /var/lib/testme/foo"')
99 machine.succeed('test "$(< /var/lib/testme/foo)" = works')
100 machine.succeed("test ! -e /tmp/canary")
101 '';
102 }
103 { testScript = ''
104 with subtest("check if /bin/sh works"):
105 machine.succeed(
106 "chroot-exec test -e /bin/sh",
107 'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar',
108 )
109 '';
110 }
111 { config.confinement.binSh = null;
112 testScript = ''
113 with subtest("check if suppressing /bin/sh works"):
114 machine.succeed("chroot-exec test ! -e /bin/sh")
115 machine.succeed('test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo')
116 '';
117 }
118 { config.confinement.binSh = "${pkgs.hello}/bin/hello";
119 testScript = ''
120 with subtest("check if we can set /bin/sh to something different"):
121 machine.succeed("chroot-exec test -e /bin/sh")
122 machine.succeed('test "$(chroot-exec /bin/sh -g foo)" = foo')
123 '';
124 }
125 { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n";
126 testScript = ''
127 with subtest("check if only Exec* dependencies are included"):
128 machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek')
129 '';
130 }
131 { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n";
132 config.confinement.fullUnit = true;
133 testScript = ''
134 with subtest("check if all unit dependencies are included"):
135 machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek')
136 '';
137 }
138 ];
139
140 options.__testSteps = lib.mkOption {
141 type = lib.types.lines;
142 description = "All of the test steps combined as a single script.";
143 };
144
145 config.environment.systemPackages = lib.singleton testClient;
146
147 config.users.groups.chroot-testgroup = {};
148 config.users.users.chroot-testuser = {
149 isSystemUser = true;
150 description = "Chroot Test User";
151 group = "chroot-testgroup";
152 };
153 };
154
155 testScript = { nodes, ... }: ''
156 def assert_eq(a, b):
157 assert a == b, f"{a} != {b}"
158
159 machine.wait_for_unit("multi-user.target")
160 '' + nodes.machine.config.__testSteps;
161}