diff --git a/nixos/tests/systemd-confinement.nix b/nixos/tests/systemd-confinement.nix index a4a0bf57f1cf..184c05b79274 100644 --- a/nixos/tests/systemd-confinement.nix +++ b/nixos/tests/systemd-confinement.nix @@ -18,6 +18,7 @@ import ./make-test-python.nix { ''; mkTestStep = num: { + description, testScript, config ? {}, serviceName ? "test${toString num}", @@ -38,192 +39,198 @@ import ./make-test-python.nix { }; } // removeAttrs config [ "confinement" "serviceConfig" ]; - __testSteps = lib.mkOrder num ('' - machine.succeed("echo ${toString num} > /teststep") - '' + testScript); + __testSteps = lib.mkOrder num '' + with subtest('${lib.escape ["'" "\\"] description}'): + machine.succeed("echo ${toString num} > /teststep") + ${lib.replaceStrings ["\n"] ["\n "] testScript} + ''; }; in { imports = lib.imap1 mkTestStep [ - { config.confinement.mode = "chroot-only"; + { description = "chroot-only confinement"; + config.confinement.mode = "chroot-only"; testScript = '' - with subtest("chroot-only confinement"): - # chroot-exec starts a socket-activated service, - # but, upon starting, a systemd system service - # calls setup_namespace() which calls base_filesystem_create() - # which creates some usual top level directories. - # In chroot-only mode, without additional BindPaths= or the like, - # they must be empty and thus removable by rmdir. - paths = machine.succeed('chroot-exec rmdir /dev /etc /proc /root /sys /usr /var "&&" ls -Am /').strip() - assert_eq(paths, "bin, nix, run") - uid = machine.succeed('chroot-exec id -u').strip() - assert_eq(uid, "0") - machine.succeed("chroot-exec chown 65534 /bin") + # chroot-exec starts a socket-activated service, + # but, upon starting, a systemd system service + # calls setup_namespace() which calls base_filesystem_create() + # which creates some usual top level directories. + # In chroot-only mode, without additional BindPaths= or the like, + # they must be empty and thus removable by rmdir. + paths = machine.succeed('chroot-exec rmdir /dev /etc /proc /root /sys /usr /var "&&" ls -Am /').strip() + assert_eq(paths, "bin, nix, run") + uid = machine.succeed('chroot-exec id -u').strip() + assert_eq(uid, "0") + machine.succeed("chroot-exec chown 65534 /bin") ''; } - { testScript = '' - with subtest("full confinement with APIVFS"): - machine.succeed('chroot-exec rmdir /etc') - machine.fail("chroot-exec chown 65534 /bin") - assert_eq(machine.succeed('chroot-exec id -u').strip(), "0") - machine.succeed("chroot-exec chown 0 /bin") - ''; - } - { config.serviceConfig.BindReadOnlyPaths = [ "/etc" ]; + { description = "full confinement with APIVFS"; testScript = '' - with subtest("check existence of bind-mounted /etc"): - passwd = machine.succeed('chroot-exec cat /etc/passwd').strip() - assert len(passwd) > 0, "/etc/passwd must not be empty" + machine.succeed('chroot-exec rmdir /etc') + machine.fail("chroot-exec chown 65534 /bin") + assert_eq(machine.succeed('chroot-exec id -u').strip(), "0") + machine.succeed("chroot-exec chown 0 /bin") ''; } - { config.serviceConfig.User = "chroot-testuser"; + { description = "check existence of bind-mounted /etc"; + config.serviceConfig.BindReadOnlyPaths = [ "/etc" ]; + testScript = '' + passwd = machine.succeed('chroot-exec cat /etc/passwd').strip() + assert len(passwd) > 0, "/etc/passwd must not be empty" + ''; + } + { description = "check if User/Group really runs as non-root"; + config.serviceConfig.User = "chroot-testuser"; config.serviceConfig.Group = "chroot-testgroup"; testScript = '' - with subtest("check if User/Group really runs as non-root"): - machine.succeed("chroot-exec ls -l /dev") - uid = machine.succeed('chroot-exec id -u').strip() - assert uid != "0", "UID of chroot-testuser shouldn't be 0" - machine.fail("chroot-exec touch /bin/test") + machine.succeed("chroot-exec ls -l /dev") + uid = machine.succeed('chroot-exec id -u').strip() + assert uid != "0", "UID of chroot-testuser shouldn't be 0" + machine.fail("chroot-exec touch /bin/test") ''; } - { config.confinement.mode = "full-apivfs"; + { description = "check if DynamicUser is working in full-apivfs mode"; + config.confinement.mode = "full-apivfs"; config.serviceConfig.DynamicUser = true; testScript = '' - with subtest("check if DynamicUser is working in full-apivfs mode"): - machine.succeed("chroot-exec ls -l /dev") - paths = machine.succeed('chroot-exec find / -path /dev/"\\*" -prune -o -path /nix/"\\*" -prune -o -path /proc/"\\*" -prune -o -path /sys/"\\*" -prune -o -print || test $? = 1') - assert_eq( - '\n'.join(sorted(paths.split('\n'))), - """ - / - /bin - /bin/sh - /dev - /etc - /nix - /proc - /root - /run - /run/host - /run/host/.os-release-stage - /run/host/.os-release-stage/os-release - /run/host/os-release - /run/systemd - /run/systemd/incoming - /sys - /tmp - /usr - /var - /var/tmp - find: '/root': Permission denied - find: '/run/systemd/incoming': Permission denied""" - ) - uid = machine.succeed('chroot-exec id -u').strip() - assert uid != "0", "UID of a DynamicUser shouldn't be 0" - machine.fail("chroot-exec touch /bin/test") - # DynamicUser=true implies ProtectSystem=strict - machine.fail("chroot-exec touch /etc/test") + machine.succeed("chroot-exec ls -l /dev") + paths = machine.succeed('chroot-exec find / -path /dev/"\\*" -prune -o -path /nix/"\\*" -prune -o -path /proc/"\\*" -prune -o -path /sys/"\\*" -prune -o -print || test $? = 1') + assert_eq( + '\n'.join(sorted(paths.split('\n'))), + dedent(""" + / + /bin + /bin/sh + /dev + /etc + /nix + /proc + /root + /run + /run/host + /run/host/.os-release-stage + /run/host/.os-release-stage/os-release + /run/host/os-release + /run/systemd + /run/systemd/incoming + /sys + /tmp + /usr + /var + /var/tmp + find: '/root': Permission denied + find: '/run/systemd/incoming': Permission denied + """).rstrip() + ) + uid = machine.succeed('chroot-exec id -u').strip() + assert uid != "0", "UID of a DynamicUser shouldn't be 0" + machine.fail("chroot-exec touch /bin/test") + # DynamicUser=true implies ProtectSystem=strict + machine.fail("chroot-exec touch /etc/test") ''; } - { config.confinement.mode = "full-apivfs"; + { description = "check if DynamicUser and PrivateTmp=false are working in full-apivfs mode"; + config.confinement.mode = "full-apivfs"; config.serviceConfig.DynamicUser = true; config.serviceConfig.PrivateTmp = false; testScript = '' - with subtest("check if DynamicUser and PrivateTmp=false are working in full-apivfs mode"): - machine.succeed("chroot-exec ls -l /dev") - paths = machine.succeed('chroot-exec find / -path /dev/"\\*" -prune -o -path /nix/"\\*" -prune -o -path /proc/"\\*" -prune -o -path /sys/"\\*" -prune -o -print || test $? = 1') - assert_eq( - '\n'.join(sorted(paths.split('\n'))), - """ - / - /bin - /bin/sh - /dev - /etc - /nix - /proc - /root - /run - /run/host - /run/host/.os-release-stage - /run/host/.os-release-stage/os-release - /run/host/os-release - /run/systemd - /run/systemd/incoming - /sys - /usr - /var - find: '/root': Permission denied - find: '/run/systemd/incoming': Permission denied""" - ) - uid = machine.succeed('chroot-exec id -u').strip() - assert uid != "0", "UID of a DynamicUser shouldn't be 0" - machine.fail("chroot-exec touch /bin/test") - # DynamicUser=true implies ProtectSystem=strict - machine.fail("chroot-exec touch /etc/test") + machine.succeed("chroot-exec ls -l /dev") + paths = machine.succeed('chroot-exec find / -path /dev/"\\*" -prune -o -path /nix/"\\*" -prune -o -path /proc/"\\*" -prune -o -path /sys/"\\*" -prune -o -print || test $? = 1') + assert_eq( + '\n'.join(sorted(paths.split('\n'))), + dedent(""" + / + /bin + /bin/sh + /dev + /etc + /nix + /proc + /root + /run + /run/host + /run/host/.os-release-stage + /run/host/.os-release-stage/os-release + /run/host/os-release + /run/systemd + /run/systemd/incoming + /sys + /usr + /var + find: '/root': Permission denied + find: '/run/systemd/incoming': Permission denied + """).rstrip() + ) + uid = machine.succeed('chroot-exec id -u').strip() + assert uid != "0", "UID of a DynamicUser shouldn't be 0" + machine.fail("chroot-exec touch /bin/test") + # DynamicUser=true implies ProtectSystem=strict + machine.fail("chroot-exec touch /etc/test") ''; } - { config.confinement.mode = "chroot-only"; + { description = "check if DynamicUser is working in chroot-only mode"; + config.confinement.mode = "chroot-only"; config.serviceConfig.DynamicUser = true; testScript = '' - with subtest("check if DynamicUser is working in chroot-only mode"): - paths = machine.succeed('chroot-exec find / -path /nix/"\\*" -prune -o -print || test $? = 1') - assert_eq( - '\n'.join(sorted(paths.split('\n'))), - """ - / - /bin - /bin/sh - /dev - /etc - /nix - /proc - /root - /run - /run/systemd - /run/systemd/incoming - /sys - /usr - /var - find: '/root': Permission denied - find: '/run/systemd/incoming': Permission denied""" - ) - uid = machine.succeed('chroot-exec id -u').strip() - assert uid != "0", "UID of a DynamicUser shouldn't be 0" - machine.fail("chroot-exec touch /bin/test") + paths = machine.succeed('chroot-exec find / -path /nix/"\\*" -prune -o -print || test $? = 1') + assert_eq( + '\n'.join(sorted(paths.split('\n'))), + dedent(""" + / + /bin + /bin/sh + /dev + /etc + /nix + /proc + /root + /run + /run/systemd + /run/systemd/incoming + /sys + /usr + /var + find: '/root': Permission denied + find: '/run/systemd/incoming': Permission denied + """).rstrip() + ) + uid = machine.succeed('chroot-exec id -u').strip() + assert uid != "0", "UID of a DynamicUser shouldn't be 0" + machine.fail("chroot-exec touch /bin/test") ''; } - { config.confinement.mode = "chroot-only"; + { description = "check if DynamicUser and PrivateTmp=true are working in chroot-only mode"; + config.confinement.mode = "chroot-only"; config.serviceConfig.DynamicUser = true; config.serviceConfig.PrivateTmp = true; testScript = '' - with subtest("check if DynamicUser and PrivateTmp=true are working in chroot-only mode"): - paths = machine.succeed('chroot-exec find / -path /nix/"\\*" -prune -o -print || test $? = 1') - assert_eq( - '\n'.join(sorted(paths.split('\n'))), - """ - / - /bin - /bin/sh - /dev - /etc - /nix - /proc - /root - /run - /run/systemd - /run/systemd/incoming - /sys - /tmp - /usr - /var - /var/tmp - find: '/root': Permission denied - find: '/run/systemd/incoming': Permission denied""" - ) - uid = machine.succeed('chroot-exec id -u').strip() - assert uid != "0", "UID of a DynamicUser shouldn't be 0" - machine.fail("chroot-exec touch /bin/test") + paths = machine.succeed('chroot-exec find / -path /nix/"\\*" -prune -o -print || test $? = 1') + assert_eq( + '\n'.join(sorted(paths.split('\n'))), + dedent(""" + / + /bin + /bin/sh + /dev + /etc + /nix + /proc + /root + /run + /run/systemd + /run/systemd/incoming + /sys + /tmp + /usr + /var + /var/tmp + find: '/root': Permission denied + find: '/run/systemd/incoming': Permission denied + """).rstrip() + ) + uid = machine.succeed('chroot-exec id -u').strip() + assert uid != "0", "UID of a DynamicUser shouldn't be 0" + machine.fail("chroot-exec touch /bin/test") ''; } (let @@ -231,68 +238,70 @@ import ./make-test-python.nix { target = pkgs.writeText "symlink-target" "got me\n"; } "ln -s \"$target\" \"$out\""; in { + description = "check if symlinks are properly bind-mounted"; config.confinement.packages = lib.singleton symlink; testScript = '' - with subtest("check if symlinks are properly bind-mounted"): - machine.succeed("chroot-exec rmdir /etc") - text = machine.succeed('chroot-exec cat ${symlink}').strip() - assert_eq(text, "got me") + machine.succeed("chroot-exec rmdir /etc") + text = machine.succeed('chroot-exec cat ${symlink}').strip() + assert_eq(text, "got me") ''; }) - { config.serviceConfig.User = "chroot-testuser"; + { description = "check if StateDirectory works"; + config.serviceConfig.User = "chroot-testuser"; config.serviceConfig.Group = "chroot-testgroup"; config.serviceConfig.StateDirectory = "testme"; testScript = '' - with subtest("check if StateDirectory works"): - machine.succeed("chroot-exec touch /tmp/canary") - machine.succeed('chroot-exec "echo works > /var/lib/testme/foo"') - machine.succeed('test "$(< /var/lib/testme/foo)" = works') - machine.succeed("test ! -e /tmp/canary") + machine.succeed("chroot-exec touch /tmp/canary") + machine.succeed('chroot-exec "echo works > /var/lib/testme/foo"') + machine.succeed('test "$(< /var/lib/testme/foo)" = works') + machine.succeed("test ! -e /tmp/canary") ''; } - { testScript = '' - with subtest("check if /bin/sh works"): - machine.succeed( - "chroot-exec test -e /bin/sh", - 'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar', - ) - ''; - } - { config.confinement.binSh = null; + { description = "check if /bin/sh works"; testScript = '' - with subtest("check if suppressing /bin/sh works"): - machine.succeed("chroot-exec test ! -e /bin/sh") - machine.succeed('test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo') + machine.succeed( + "chroot-exec test -e /bin/sh", + 'test "$(chroot-exec \'/bin/sh -c "echo bar"\')" = bar', + ) ''; } - { config.confinement.binSh = "${pkgs.hello}/bin/hello"; + { description = "check if suppressing /bin/sh works"; + config.confinement.binSh = null; testScript = '' - with subtest("check if we can set /bin/sh to something different"): - machine.succeed("chroot-exec test -e /bin/sh") - machine.succeed('test "$(chroot-exec /bin/sh -g foo)" = foo') + machine.succeed( + 'chroot-exec test ! -e /bin/sh', + 'test "$(chroot-exec \'/bin/sh -c "echo foo"\')" != foo', + ) ''; } - { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; + { description = "check if we can set /bin/sh to something different"; + config.confinement.binSh = "${pkgs.hello}/bin/hello"; testScript = '' - with subtest("check if only Exec* dependencies are included"): - machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek') + machine.succeed("chroot-exec test -e /bin/sh") + machine.succeed('test "$(chroot-exec /bin/sh -g foo)" = foo') ''; } - { config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; + { description = "check if only Exec* dependencies are included"; + config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; + testScript = '' + machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" != eek') + ''; + } + { description = "check if all unit dependencies are included"; + config.environment.FOOBAR = pkgs.writeText "foobar" "eek\n"; config.confinement.fullUnit = true; testScript = '' - with subtest("check if all unit dependencies are included"): - machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek') + machine.succeed('test "$(chroot-exec \'cat "$FOOBAR"\')" = eek') ''; } - { serviceName = "shipped-unitfile"; + { description = "check if shipped unit file still works"; + serviceName = "shipped-unitfile"; config.confinement.mode = "chroot-only"; testScript = '' - with subtest("check if shipped unit file still works"): - machine.succeed( - 'chroot-exec \'kill -9 $$ 2>&1 || :\' | ' - 'grep -q "Too many levels of symbolic links"' - ) + machine.succeed( + 'chroot-exec \'kill -9 $$ 2>&1 || :\' | ' + 'grep -q "Too many levels of symbolic links"' + ) ''; } ]; @@ -322,12 +331,14 @@ import ./make-test-python.nix { }; testScript = { nodes, ... }: '' + from textwrap import dedent import difflib + def assert_eq(got, expected): - if got != expected: - diff = difflib.unified_diff(got.splitlines(keepends=True), expected.splitlines(keepends=True)) - print("".join(diff)) - assert got == expected, f"{got} != {expected}" + if got != expected: + diff = difflib.unified_diff(got.splitlines(keepends=True), expected.splitlines(keepends=True)) + print("".join(diff)) + assert got == expected, f"{got} != {expected}" machine.wait_for_unit("multi-user.target") '' + nodes.machine.__testSteps;