nixVersions.stable: 2.15 -> 2.17
[NixPkgs.git] / nixos / tests / systemd-confinement.nix
blob428888d41a205a35f817e9a13d215bc8dcfaef66
1 import ./make-test-python.nix {
2   name = "systemd-confinement";
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     '';
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     '';
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       };
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" ];
41       __testSteps = lib.mkOrder num (''
42         machine.succeed("echo ${toString num} > /teststep")
43       '' + testScript);
44     };
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     ];
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     };
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     });
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   };
178   testScript = { nodes, ... }: ''
179     def assert_eq(a, b):
180         assert a == b, f"{a} != {b}"
182     machine.wait_for_unit("multi-user.target")
183   '' + nodes.machine.config.__testSteps;