at 23.11-pre 7.0 kB view raw
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}