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