mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Isolate rootless qualification cgroups
This commit is contained in:
@@ -7313,7 +7313,11 @@ must pre-create their root-owned, mode-`0700` destination before the wrapper
|
||||
injects the exact packet into a stopped disposable container. The wrapper
|
||||
uses the supported `slirp4netns` rootless Docker driver only inside that outer
|
||||
`--network none` boundary; selecting the unsupported `host` RootlessKit driver
|
||||
or granting an outer default route is invalid qualification. The wrapper
|
||||
or granting an outer default route is invalid qualification. The outer host
|
||||
uses a private cgroup namespace with no host cgroup bind mount. Each dedicated
|
||||
runtime identity must have an active delegated systemd user manager before its
|
||||
daemon starts, and cleanup must stop that manager and remove its linger state.
|
||||
The wrapper
|
||||
exercises each runtime in an isolated state root and emits the
|
||||
standalone `secure-runtime-rootless-v1` receipt only after exact socket
|
||||
ownership, daemon rootless attestation, installer pinning, direct telemetry,
|
||||
|
||||
@@ -264,7 +264,10 @@ must pre-create its root-owned, mode-`0700` packet destination before the
|
||||
wrapper injects artifacts into each stopped disposable container.
|
||||
Rootless Docker uses its supported `slirp4netns` driver inside the outer
|
||||
`--network none` container only; an unsupported `host` RootlessKit driver or
|
||||
an outer default route invalidates the proof.
|
||||
an outer default route invalidates the proof. The disposable host uses a
|
||||
private cgroup namespace without a host cgroup bind mount, creates an active
|
||||
delegated systemd user manager for the dedicated runtime identity, and removes
|
||||
that manager's linger state during cleanup.
|
||||
Its standalone `secure-runtime-rootless-v1` receipt must bind the exact
|
||||
qualification, collector, helper, installer, source-manifest, socket,
|
||||
fresh-install, legacy-migration, restart, fallback, recovery, ambiguity,
|
||||
|
||||
@@ -160,7 +160,7 @@ func TestSecureRuntimeRootlessQualification(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
daemon := rootlessQualPrepareDaemons(t, runtimeKind)
|
||||
defer rootlessQualBestEffortStop(daemon.rootlessUnit, daemon.rootfulUnit, rootlessQualOtherUnit(runtimeKind))
|
||||
defer rootlessQualBestEffortStop(daemon.rootlessUnit, daemon.rootfulUnit, rootlessQualOtherUnit(runtimeKind), fmt.Sprintf("user@%d.service", daemon.uid))
|
||||
rootlessQualStartRootful(t, daemon)
|
||||
rootlessQualStartRootless(t, daemon)
|
||||
identity := rootlessQualReadIdentityRecord(t, daemon)
|
||||
@@ -414,6 +414,7 @@ func TestSecureRuntimeRootlessQualification(t *testing.T) {
|
||||
rootlessQualUninstallPulse(t, installerPath, server.URL)
|
||||
rootlessQualStopUnit(t, daemon.rootlessUnit)
|
||||
rootlessQualStopUnit(t, daemon.rootfulUnit)
|
||||
rootlessQualStopUserManager(t, daemon)
|
||||
rootlessQualRemoveRuntimeState(t, daemon)
|
||||
rootlessQualAssertPulseRemoved(t)
|
||||
for _, socket := range []string{daemon.rootlessSock, daemon.rootfulSock} {
|
||||
@@ -539,6 +540,16 @@ func rootlessQualPrepareDaemons(t *testing.T, runtimeKind string) rootlessQualDa
|
||||
for _, path := range paths {
|
||||
rootlessQualCommand(t, 10*time.Second, "install", "-d", "-o", "pulse-agent", "-g", "pulse-agent", "-m", "0700", path)
|
||||
}
|
||||
rootlessQualCommand(t, 10*time.Second, "loginctl", "enable-linger", "pulse-agent")
|
||||
userUnit := fmt.Sprintf("user@%d.service", uid)
|
||||
rootlessQualCommand(t, 20*time.Second, "systemctl", "start", userUnit)
|
||||
active := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", userUnit, "--property=ActiveState", "--value")
|
||||
delegated := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", userUnit, "--property=Delegate", "--value")
|
||||
controlGroup := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", userUnit, "--property=ControlGroup", "--value")
|
||||
expectedControlGroup := fmt.Sprintf("/user.slice/user-%d.slice/user@%d.service", uid, uid)
|
||||
if active != "active" || delegated != "yes" || controlGroup != expectedControlGroup {
|
||||
t.Fatalf("rootless user manager is not exactly delegated: active=%q delegate=%q control_group=%q", active, delegated, controlGroup)
|
||||
}
|
||||
containerfile := "FROM scratch\nCOPY busybox /busybox\nENTRYPOINT [\"/busybox\"]\n"
|
||||
if err := os.WriteFile(filepath.Join(home, "fixture", "Containerfile"), []byte(containerfile), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -1030,8 +1041,14 @@ func rootlessQualRemoveRuntimeState(t *testing.T, d rootlessQualDaemon) {
|
||||
}
|
||||
}
|
||||
|
||||
func rootlessQualStopUserManager(t *testing.T, d rootlessQualDaemon) {
|
||||
t.Helper()
|
||||
rootlessQualCommand(t, 10*time.Second, "loginctl", "disable-linger", "pulse-agent")
|
||||
rootlessQualStopUnit(t, fmt.Sprintf("user@%d.service", d.uid))
|
||||
}
|
||||
|
||||
func rootlessQualUserStateClean(d rootlessQualDaemon) bool {
|
||||
for _, path := range []string{d.home, filepath.Join("/run/user", strconv.Itoa(d.uid)), "/var/lib/pulse-rootful-docker", "/var/lib/containers/storage"} {
|
||||
for _, path := range []string{d.home, filepath.Join("/run/user", strconv.Itoa(d.uid)), "/var/lib/pulse-rootful-docker", "/var/lib/containers/storage", "/var/lib/systemd/linger/pulse-agent"} {
|
||||
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
@@ -1360,7 +1377,7 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) {
|
||||
}
|
||||
script := string(raw)
|
||||
for _, required := range []string{
|
||||
`PULSE_ROOTLESS_UBUNTU_IMAGE`, `^ubuntu@sha256:`, `--network none`, `--tmpfs /run`,
|
||||
`PULSE_ROOTLESS_UBUNTU_IMAGE`, `^ubuntu@sha256:`, `--network none`, `--cgroupns=private`, `--tmpfs /run`,
|
||||
`PULSE_SECURE_RUNTIME_ROOTLESS_QUALIFICATION=disposable-v1`, `dockeragent.test`,
|
||||
`run_runtime docker`, `run_runtime podman`, `PULSE_ROOTLESS_RUNTIME=${runtime_name}`, `--privileged`,
|
||||
`pulse-secure-runtime-rootless-qualification`, `qualification result != \"passed\"`,
|
||||
@@ -1373,7 +1390,7 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) {
|
||||
t.Fatalf("rootless qualification wrapper missing %q", required)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"/var/run/docker.sock:", "/run/podman/podman.sock:", "-v $", "--volume"} {
|
||||
for _, forbidden := range []string{"/var/run/docker.sock:", "/run/podman/podman.sock:", "/sys/fs/cgroup:/sys/fs/cgroup", "--cgroupns=host", "-v $", "--volume"} {
|
||||
if strings.Contains(script, forbidden) {
|
||||
t.Fatalf("rootless qualification wrapper contains forbidden host-runtime mount marker %q", forbidden)
|
||||
}
|
||||
|
||||
@@ -225,9 +225,9 @@ run_runtime() {
|
||||
|
||||
container_id="$(docker create --name "${container_name}" --hostname "pulse-rootless-${runtime_name}" \
|
||||
--label "${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}" \
|
||||
--privileged --network none --cgroupns=host \
|
||||
--privileged --network none --cgroupns=private \
|
||||
--tmpfs /run:rw,nosuid,nodev,mode=755 --tmpfs /run/lock:rw,nosuid,nodev,mode=755 \
|
||||
-v /sys/fs/cgroup:/sys/fs/cgroup:rw "${IMAGE_TAG}")"
|
||||
"${IMAGE_TAG}")"
|
||||
CONTAINER_IDS+=("${container_id}")
|
||||
docker cp "${PACKET_DIR}/." "${container_id}:/opt/pulse/packet"
|
||||
docker start "${container_id}" >/dev/null
|
||||
|
||||
Reference in New Issue
Block a user