Persist rootless qualification receipts

This commit is contained in:
rcourtman
2026-09-01 19:47:15 +01:00
parent e89eaac9d4
commit 861f0f07bc
4 changed files with 32 additions and 9 deletions
@@ -7338,6 +7338,9 @@ standalone `secure-runtime-rootless-v1` receipt only after exact socket
ownership, daemon rootless attestation, installer pinning, direct telemetry,
same-family typed-helper fallback, recovery without collector restart,
ambiguity refusal, authority isolation, and cleanup are recorded.
Receipt output uses a dedicated root-owned, mode-`0700` `/opt/pulse/result`
boundary instead of volatile `/run` state, so systemd user-manager teardown
cannot erase a passing result before the wrapper retains it.
The wrapper must compile every Go artifact with mandatory VCS stamping so a
missing revision or unavailable clean-worktree proof fails before live evidence
can be emitted; the receipt and validator retain the exact artifact hashes and
@@ -261,7 +261,10 @@ network-isolated Ubuntu/systemd hosts for real rootless Docker and Podman,
never mounts a host daemon socket, and records each host identity separately.
The exact qualification packet remains outside the image layers; the image
must pre-create its root-owned, mode-`0700` packet destination before the
wrapper injects artifacts into each stopped disposable container.
wrapper injects artifacts into each stopped disposable container. The image
must also pre-create the separate root-owned, mode-`0700` `/opt/pulse/result`
output boundary; receipts are written there instead of volatile `/run` state so
systemd user-manager cleanup cannot erase completed qualification evidence.
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. The disposable host uses a
@@ -38,6 +38,8 @@ const (
rootlessQualOptIn = "PULSE_SECURE_RUNTIME_ROOTLESS_QUALIFICATION"
rootlessQualOptInValue = "disposable-v1"
rootlessQualMarker = "/etc/pulse-secure-runtime-rootless-qualification"
rootlessQualReceiptPath = "/opt/pulse/result/rootless-receipt.json"
rootlessQualResultDir = "/opt/pulse/result"
rootlessQualFixture = "pulse-rootless-qualification-fixture:v1"
rootlessQualRunningName = "pulse-rootless-running"
rootlessQualExitedName = "pulse-rootless-exited"
@@ -520,8 +522,16 @@ func rootlessQualRequireDisposableHost(t *testing.T, runtimeKind, receiptPath st
if err != nil || strings.TrimSpace(string(marker)) != rootlessQualOptInValue {
t.Fatalf("disposable marker is absent or invalid: %v", err)
}
if !filepath.IsAbs(receiptPath) || filepath.Clean(receiptPath) != receiptPath {
t.Fatalf("PULSE_ROOTLESS_RECEIPT must be an exact absolute path: %q", receiptPath)
if receiptPath != rootlessQualReceiptPath {
t.Fatalf("PULSE_ROOTLESS_RECEIPT must use the dedicated durable output path %q: %q", rootlessQualReceiptPath, receiptPath)
}
resultInfo, err := os.Lstat(rootlessQualResultDir)
if err != nil {
t.Fatalf("inspect rootless qualification result directory: %v", err)
}
resultStat, ok := resultInfo.Sys().(*syscall.Stat_t)
if !ok || !resultInfo.IsDir() || resultInfo.Mode()&os.ModeSymlink != 0 || resultInfo.Mode().Perm() != 0o700 || resultStat.Uid != 0 {
t.Fatalf("rootless qualification result directory must be a root-owned, non-symlink directory with mode 0700: mode=%s stat=%#v", resultInfo.Mode(), resultInfo.Sys())
}
osRelease := string(rootlessQualReadFile(t, "/etc/os-release"))
if !strings.Contains(osRelease, "VERSION_ID=\"24.04\"") && !strings.Contains(osRelease, "VERSION_ID=24.04") {
@@ -1597,7 +1607,9 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) {
`run_runtime docker`, `run_runtime podman`, `PULSE_ROOTLESS_RUNTIME=${runtime_name}`, `--privileged`,
`pulse-secure-runtime-rootless-qualification`, `qualification result != \"passed\"`,
`openssl pkeyutl -sign -rawin -inkey`, `qualification output directory must have exact mode 0700`,
`install -d -m 0700 /opt/pulse/packet`,
`install -d -m 0700 /opt/pulse/packet /opt/pulse/result`,
`PULSE_ROOTLESS_RECEIPT=/opt/pulse/result/rootless-receipt.json`,
`docker cp "${container_id}:/opt/pulse/result/rootless-receipt.json"`,
`capture_qualification_container_diagnostics`, `journalctl --no-pager -n 2000`,
`302a300506032b6570032100`, `len(spki) != len(prefix) + 32`,
} {
@@ -1608,7 +1620,7 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) {
if count := strings.Count(script, `-buildvcs=true`); count != 3 {
t.Fatalf("rootless qualification wrapper must require VCS metadata for all three Go artifacts: got %d", count)
}
for _, forbidden := range []string{"/var/run/docker.sock:", "/run/podman/podman.sock:", "/sys/fs/cgroup:/sys/fs/cgroup", "--cgroupns=host", "-v $", "--volume"} {
for _, forbidden := range []string{"/var/run/docker.sock:", "/run/podman/podman.sock:", "/sys/fs/cgroup:/sys/fs/cgroup", "/run/rootless-receipt.json", "--cgroupns=host", "-v $", "--volume"} {
if strings.Contains(script, forbidden) {
t.Fatalf("rootless qualification wrapper contains forbidden host-runtime mount marker %q", forbidden)
}
@@ -1621,7 +1633,7 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) {
if createIndex < 0 || trackIndex < 0 || trackIndex < createIndex {
t.Fatal("rootless wrapper must track the exact container ID only after docker create succeeds")
}
packetDirectoryIndex := strings.Index(script, `install -d -m 0700 /opt/pulse/packet`)
packetDirectoryIndex := strings.Index(script, `install -d -m 0700 /opt/pulse/packet /opt/pulse/result`)
packetCopyIndex := strings.Index(script, `docker cp "${PACKET_DIR}/." "${container_id}:/opt/pulse/packet"`)
if packetDirectoryIndex < 0 || packetCopyIndex < 0 || packetCopyIndex < packetDirectoryIndex {
t.Fatal("rootless wrapper must create the private packet destination in the image before artifact injection")
@@ -198,7 +198,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ln -sf /dev/null /etc/systemd/system/docker.socket && \
ln -sf /dev/null /etc/systemd/system/podman.service && \
ln -sf /dev/null /etc/systemd/system/podman.socket && \
install -d -m 0700 /opt/pulse/packet && \
install -d -m 0700 /opt/pulse/packet /opt/pulse/result && \
printf '%s\n' disposable-v1 >/etc/pulse-secure-runtime-rootless-qualification && \
rm -f /etc/machine-id && touch /etc/machine-id && \
systemctl set-default multi-user.target
@@ -253,7 +253,7 @@ run_runtime() {
if ! docker exec \
-e PULSE_SECURE_RUNTIME_ROOTLESS_QUALIFICATION=disposable-v1 \
-e "PULSE_ROOTLESS_RUNTIME=${runtime_name}" \
-e PULSE_ROOTLESS_RECEIPT=/run/rootless-receipt.json \
-e PULSE_ROOTLESS_RECEIPT=/opt/pulse/result/rootless-receipt.json \
-e PULSE_ROOTLESS_SOURCE_HASHES=/opt/pulse/packet/source-hashes.json \
-e "PULSE_ROOTLESS_SOURCE_COMMIT=${SOURCE_COMMIT}" \
-e PULSE_SECURE_RUNTIME_COLLECTOR=/opt/pulse/packet/pulse-agent \
@@ -268,7 +268,12 @@ run_runtime() {
return 1
fi
docker cp "${container_id}:/run/rootless-receipt.json" "${local_receipt}"
if ! docker exec "${container_id}" test -f /opt/pulse/result/rootless-receipt.json; then
capture_qualification_container_diagnostics "${runtime_name}" "${container_id}"
echo "ERROR: ${runtime_name} qualification did not retain its durable receipt" >&2
return 1
fi
docker cp "${container_id}:/opt/pulse/result/rootless-receipt.json" "${local_receipt}"
capture_qualification_container_diagnostics "${runtime_name}" "${container_id}"
chmod 0600 "${local_receipt}" "${OUTPUT_DIR}/${runtime_name}-test.log"
remove_qualification_container_strict "${container_id}"