import ./make-test.nix { name = "systemd-chroot"; machine = { pkgs, lib, ... }: let testServer = pkgs.writeScript "testserver.sh" '' #!${pkgs.stdenv.shell} export PATH=${lib.escapeShellArg "${pkgs.coreutils}/bin"} ${lib.escapeShellArg pkgs.stdenv.shell} 2>&1 echo "exit-status:$?" ''; testClient = pkgs.writeScriptBin "chroot-exec" '' #!${pkgs.stdenv.shell} -e output="$(echo "$@" | nc -NU "/run/test$(< /teststep).sock")" ret="$(echo "$output" | sed -nre '$s/^exit-status:([0-9]+)$/\1/p')" echo "$output" | head -n -1 exit "''${ret:-1}" ''; mkTestStep = num: { description, config ? {}, testScript }: { systemd.sockets."test${toString num}" = { description = "Socket for Test Service ${toString num}"; wantedBy = [ "sockets.target" ]; socketConfig.ListenStream = "/run/test${toString num}.sock"; socketConfig.Accept = true; }; systemd.services."test${toString num}@" = { description = "Chrooted Test Service ${toString num}"; chroot = (config.chroot or {}) // { enable = true; }; serviceConfig = (config.serviceConfig or {}) // { ExecStart = testServer; StandardInput = "socket"; }; } // removeAttrs config [ "chroot" "serviceConfig" ]; __testSteps = lib.mkOrder num '' subtest '${lib.escape ["\\" "'"] description}', sub { $machine->succeed('echo ${toString num} > /teststep'); ${testScript} }; ''; }; in { imports = lib.imap1 mkTestStep [ { description = "chroot-only confinement"; config.chroot.confinement = "chroot-only"; testScript = '' $machine->succeed( 'test "$(chroot-exec ls -1 / | paste -sd,)" = bin,nix', 'test "$(chroot-exec id -u)" = 0', 'chroot-exec chown 65534 /bin', ); ''; } { description = "full confinement with APIVFS"; testScript = '' $machine->fail( 'chroot-exec ls -l /etc', 'chroot-exec ls -l /run', 'chroot-exec chown 65534 /bin', ); $machine->succeed( 'test "$(chroot-exec id -u)" = 0', 'chroot-exec chown 0 /bin', ); ''; } { description = "check existence of bind-mounted /etc"; config.serviceConfig.BindReadOnlyPaths = [ "/etc" ]; testScript = '' $machine->succeed('test -n "$(chroot-exec cat /etc/passwd)"'); ''; } { description = "check if User/Group really runs as non-root"; config.serviceConfig.User = "chroot-testuser"; config.serviceConfig.Group = "chroot-testgroup"; testScript = '' $machine->succeed('chroot-exec ls -l /dev'); $machine->succeed('test "$(chroot-exec id -u)" != 0'); $machine->fail('chroot-exec touch /bin/test'); ''; } (let symlink = pkgs.runCommand "symlink" { target = pkgs.writeText "symlink-target" "got me\n"; } "ln -s \"$target\" \"$out\""; in { description = "check if symlinks are properly bind-mounted"; config.chroot.packages = lib.singleton symlink; testScript = '' $machine->fail('chroot-exec test -e /etc'); $machine->succeed('chroot-exec cat ${symlink} >&2'); $machine->succeed('test "$(chroot-exec cat ${symlink})" = "got me"'); ''; }) { description = "check if StateDirectory works"; config.serviceConfig.User = "chroot-testuser"; config.serviceConfig.Group = "chroot-testgroup"; config.serviceConfig.StateDirectory = "testme"; testScript = '' $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'); ''; } ]; options.__testSteps = lib.mkOption { type = lib.types.lines; description = "All of the test steps combined as a single script."; }; config.environment.systemPackages = lib.singleton testClient; config.users.groups.chroot-testgroup = {}; config.users.users.chroot-testuser = { description = "Chroot Test User"; group = "chroot-testgroup"; }; }; testScript = { nodes, ... }: '' $machine->waitForUnit('multi-user.target'); ${nodes.machine.config.__testSteps} ''; }