nixos/tests/confinement: Re-add description attr

The reason why I originally used the "description" attribute was that it
can be easily used to parametrise the tests so that we can specify
common constraints and apply it across a number of different
configurations.

When porting the tests to Python, the description attribute was replaced
by inlining it into the Python code, most probably because it was easier
to do in bulk since using Nix to generate the subtest parts would be
very complicated to do since we also had to please Black (a Python code
formatter that we no longer use in test scripts).

Since we now also want to support DynamicUser in systemd-confinement,
the need to parametrise the tests became apparent again because it's now
easier to refactor our subtests to run both with *and* without
DynamicUser set to true.

Signed-off-by: aszlig <aszlig@nix.build>
This commit is contained in:
aszlig
2024-05-13 00:40:32 +02:00
parent 0a5542c766
commit ba31b3753e
+209 -198
View File
@@ -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;