Files
pulse/scripts/installtests/secure_runtime_rootless_qualification_test.go
T
2026-09-01 19:37:23 +01:00

1842 lines
85 KiB
Go

//go:build !windows
package installtests
// This file is a deliberately separate qualification packet. The live test is
// never part of ordinary `go test`: it mutates users, systemd units, and local
// container-runtime state and therefore requires an exact disposable-host
// marker plus an explicit opt-in. The host-side wrapper creates the isolated
// Ubuntu 24.04 systemd containers and never mounts the host runtime socket.
import (
"context"
"crypto/sha256"
"debug/buildinfo"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"syscall"
"testing"
"time"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
)
const (
rootlessQualOptIn = "PULSE_SECURE_RUNTIME_ROOTLESS_QUALIFICATION"
rootlessQualOptInValue = "disposable-v1"
rootlessQualMarker = "/etc/pulse-secure-runtime-rootless-qualification"
rootlessQualFixture = "pulse-rootless-qualification-fixture:v1"
rootlessQualRunningName = "pulse-rootless-running"
rootlessQualExitedName = "pulse-rootless-exited"
)
var rootlessQualScenarioOrder = []string{
"fresh_install",
"legacy_migration",
"collector_restart",
"daemon_restart",
"socket_loss_helper_fallback",
"direct_recovery",
"dual_socket_ambiguity_refusal",
"exact_pin_recovery",
"telemetry_parity",
"authority_isolation",
"cleanup",
}
type rootlessQualReceipt struct {
SchemaVersion int `json:"schema_version"`
Kind string `json:"kind"`
Result string `json:"result"`
SourceCommit string `json:"source_commit"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at"`
SourceHashes map[string]string `json:"source_hashes"`
Artifacts rootlessQualArtifacts `json:"artifacts"`
Runs []rootlessQualRun `json:"runs"`
}
type rootlessQualHost struct {
MachineID string `json:"machine_id"`
Architecture string `json:"architecture"`
Kernel string `json:"kernel"`
SystemdVersion string `json:"systemd_version"`
}
type rootlessQualArtifact struct {
PathBasename string `json:"path_basename"`
SHA256 string `json:"sha256"`
Package string `json:"package"`
GoVersion string `json:"go_version"`
VCSRevision string `json:"vcs_revision"`
VCSModified bool `json:"vcs_modified"`
}
type rootlessQualInstallerArtifact struct {
PathBasename string `json:"path_basename"`
SHA256 string `json:"sha256"`
}
type rootlessQualArtifacts struct {
QualificationTest rootlessQualArtifact `json:"qualification_test"`
Collector rootlessQualArtifact `json:"collector"`
Helper rootlessQualArtifact `json:"helper"`
Installer rootlessQualInstallerArtifact `json:"installer"`
}
type rootlessQualRun struct {
Host rootlessQualHost `json:"host"`
Runtime rootlessQualRuntime `json:"runtime"`
Scenarios []rootlessQualScenario `json:"scenarios"`
}
type rootlessQualRuntime struct {
Runtime string `json:"runtime"`
RuntimeVersion string `json:"runtime_version"`
DaemonID string `json:"daemon_id"`
CollectorUID int `json:"collector_uid"`
SocketPath string `json:"socket_path"`
SocketUID int `json:"socket_uid"`
SocketGID int `json:"socket_gid"`
SocketMode string `json:"socket_mode"`
DaemonRootless bool `json:"daemon_rootless"`
SocketType string `json:"socket_type"`
SocketSymlink bool `json:"socket_symlink"`
}
type rootlessQualScenario struct {
Name string `json:"name"`
Result string `json:"result"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at"`
ReportSequence *uint64 `json:"report_sequence"`
ReportStreamID *string `json:"report_stream_id"`
Evidence map[string]any `json:"evidence"`
}
type rootlessQualDaemon struct {
runtime string
rootlessUnit string
rootfulUnit string
rootlessSock string
rootfulSock string
uid int
home string
}
func TestSecureRuntimeRootlessQualification(t *testing.T) {
if os.Getenv(rootlessQualOptIn) != rootlessQualOptInValue {
t.Skip("run through scripts/run-secure-runtime-rootless-qualification.sh inside its disposable systemd container")
}
runtimeKind := strings.TrimSpace(os.Getenv("PULSE_ROOTLESS_RUNTIME"))
receiptPath := strings.TrimSpace(os.Getenv("PULSE_ROOTLESS_RECEIPT"))
rootlessQualRequireDisposableHost(t, runtimeKind, receiptPath)
collector := secureRuntimeReadArtifact(t, "PULSE_SECURE_RUNTIME_COLLECTOR")
collectorSignature := secureRuntimeReadSignature(t, "PULSE_SECURE_RUNTIME_COLLECTOR_SIGNATURE")
helper := secureRuntimeReadArtifact(t, "PULSE_SECURE_RUNTIME_HELPER")
collectorVersion := secureRuntimeArtifactVersion(t, "PULSE_SECURE_RUNTIME_COLLECTOR")
installerPath := strings.TrimSpace(os.Getenv("PULSE_SECURE_RUNTIME_INSTALLER"))
if !filepath.IsAbs(installerPath) {
t.Fatalf("PULSE_SECURE_RUNTIME_INSTALLER must be absolute: %q", installerPath)
}
started := time.Now().UTC()
fixture := newSecureRuntimeLabFixture(collector, collectorSignature, helper, nil, collectorVersion)
defer fixture.actionServer.Shutdown()
server := httptest.NewServer(fixture)
defer server.Close()
collectorCredential := secureRuntimeLabToken
daemon := rootlessQualPrepareDaemons(t, 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)
if err := os.Remove(daemon.rootlessSock + ".qualification-identity"); err != nil {
t.Fatalf("remove transient runtime identity sidecar: %v", err)
}
rootlessQualCreateFixture(t, daemon, false)
rootlessQualCreateFixture(t, daemon, true)
daemonRootlessObserved := rootlessQualDaemonRootless(t, daemon)
if !daemonRootlessObserved {
t.Fatalf("%s daemon did not independently report rootless operation", runtimeKind)
}
rootfulBaseline := rootlessQualRuntimeBaseline(t, daemon, false)
rootlessBaseline := rootlessQualRuntimeBaseline(t, daemon, true)
if rootfulBaseline.SemanticDigest != rootlessBaseline.SemanticDigest || rootfulBaseline.Count != 2 {
t.Fatalf("separate same-family baselines differ: rootful=%+v rootless=%+v", rootfulBaseline, rootlessBaseline)
}
var scenarios []rootlessQualScenario
appendScenario := func(name string, began time.Time, report *agentsdocker.Report, evidence map[string]any) {
scenario := rootlessQualScenario{Name: name, Result: "passed", StartedAt: began.Format(time.RFC3339Nano), CompletedAt: time.Now().UTC().Format(time.RFC3339Nano), Evidence: evidence}
if report != nil {
stream, sequence, ok := agentshost.ParseReportSequenceID(report.SequenceID)
if !ok {
t.Fatalf("scenario %s received invalid sequence ID %q", name, report.SequenceID)
}
scenario.ReportSequence = &sequence
scenario.ReportStreamID = &stream
}
scenarios = append(scenarios, scenario)
}
// Prove a fresh safe-profile install first, then remove it before creating
// the legacy root profile. Both paths therefore begin without installed
// Pulse files; the surrounding outer container itself is also fresh.
freshStarted := time.Now().UTC()
secureRuntimeRunInstaller(t, installerPath, server.URL,
"--least-privilege", "--enable-privileged-helper", "--enable-docker")
freshReport := rootlessQualWaitDirect(t, fixture, freshStarted, runtimeKind, rootlessBaseline.SemanticDigest, 75*time.Second)
secureRuntimeAssertSafeProfile(t)
secureRuntimeAssertHelperProtocol(t)
freshDigest := rootlessQualDigestReport(freshReport.Report)
freshPID := secureRuntimeCollectorMainPID(t)
daemonIDBefore := rootlessQualDaemonID(t, daemon, true)
appendScenario("fresh_install", freshStarted, &freshReport.Report,
rootlessQualDirectEvidence(daemon, identity, freshPID, daemonIDBefore, daemonRootlessObserved, freshDigest))
rootlessQualUninstallPulse(t, installerPath, server.URL, collectorCredential)
rootlessQualAssertPulseRemoved(t)
if registered, revoked, uninstalls := fixture.collectorLifecycleSnapshot(); registered || !revoked || uninstalls != 1 {
t.Fatalf("fresh collector uninstall was not durably modeled: registered=%t revoked=%t uninstalls=%d", registered, revoked, uninstalls)
}
fixture.replaceCollectorCredential(secureRuntimeLabTokenV2, secureRuntimeCollectorBindingV2)
collectorCredential = secureRuntimeLabTokenV2
migrationStarted := time.Now().UTC()
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, server.URL, collectorCredential,
"--enable-commands", "--command-authority", "command-capable", "--enable-docker")
legacyReport := rootlessQualWaitReport(t, fixture, migrationStarted, 75*time.Second, func(report agentsdocker.Report) bool {
return rootlessQualComplete(report) && report.Host.CollectionMode == "" && report.Host.Runtime == runtimeKind
})
secureRuntimeAssertRootCommandProfile(t)
legacyPID := secureRuntimeCollectorMainPID(t)
legacyDigest := rootlessQualSemanticDigest(legacyReport.Report)
if legacyDigest != rootfulBaseline.SemanticDigest {
t.Fatalf("legacy root report differs from the separate rootful runtime baseline: report=%s baseline=%s", legacyDigest, rootfulBaseline.SemanticDigest)
}
applyStarted := time.Now().UTC()
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, server.URL, collectorCredential, "--safe-profile-apply")
migratedReport := rootlessQualWaitDirect(t, fixture, applyStarted, runtimeKind, rootlessBaseline.SemanticDigest, 75*time.Second)
secureRuntimeAssertSafeProfile(t)
secureRuntimeAssertHelperProtocol(t)
migratedDigest := rootlessQualDigestReport(migratedReport.Report)
if freshDigest.SemanticDigest != rootlessBaseline.SemanticDigest || migratedDigest.SemanticDigest != rootlessBaseline.SemanticDigest {
t.Fatalf("direct report semantic parity failed: baseline=%s fresh=%s migration=%s", rootlessBaseline.SemanticDigest, freshDigest.SemanticDigest, migratedDigest.SemanticDigest)
}
migratedPID := secureRuntimeCollectorMainPID(t)
if migratedPID == legacyPID {
t.Fatalf("safe-profile migration did not replace legacy collector PID %d", legacyPID)
}
migrationEvidence := rootlessQualDirectEvidence(daemon, identity, migratedPID, daemonIDBefore, daemonRootlessObserved, migratedDigest)
migrationEvidence["legacy_profile"] = "root-command-capable"
migrationEvidence["target_profile"] = "typed-helper-monitoring-only"
migrationEvidence["authority_reduced"] = true
migrationEvidence["legacy_collector_pid"] = legacyPID
appendScenario("legacy_migration", migrationStarted, &migratedReport.Report, migrationEvidence)
restartStarted := time.Now().UTC()
collectorPIDBefore := migratedPID
streamBefore, _, _ := agentshost.ParseReportSequenceID(migratedReport.Report.SequenceID)
secureRuntimeCommand(t, 20*time.Second, "systemctl", "restart", "pulse-agent.service")
collectorPIDAfter := secureRuntimeCollectorMainPID(t)
restartedCollectorReport := rootlessQualWaitDirect(t, fixture, restartStarted, runtimeKind, rootlessBaseline.SemanticDigest, 75*time.Second)
streamAfter, _, _ := agentshost.ParseReportSequenceID(restartedCollectorReport.Report.SequenceID)
restartedCollectorDigest := rootlessQualDigestReport(restartedCollectorReport.Report)
if collectorPIDAfter == collectorPIDBefore || streamAfter == streamBefore || restartedCollectorDigest.SemanticDigest != rootlessBaseline.SemanticDigest {
t.Fatalf("collector restart did not replace process/stream with parity: pid=%d/%d stream=%s/%s", collectorPIDBefore, collectorPIDAfter, streamBefore, streamAfter)
}
restartEvidence := rootlessQualDirectEvidence(daemon, identity, collectorPIDAfter, daemonIDBefore, daemonRootlessObserved, restartedCollectorDigest)
restartEvidence["previous_collector_pid"] = collectorPIDBefore
restartEvidence["previous_report_stream_id"] = streamBefore
appendScenario("collector_restart", restartStarted, &restartedCollectorReport.Report, restartEvidence)
daemonRestartStarted := time.Now().UTC()
daemonPIDBefore, invocationBefore := rootlessQualUnitIdentity(t, daemon.rootlessUnit)
rootlessQualStopUnit(t, daemon.rootlessUnit)
rootlessQualStartRootless(t, daemon)
daemonPIDAfter, invocationAfter := rootlessQualUnitIdentity(t, daemon.rootlessUnit)
daemonIDAfter := rootlessQualDaemonID(t, daemon, true)
daemonRootlessAfterRestart := rootlessQualDaemonRootless(t, daemon)
daemonRestartReport := rootlessQualWaitDirect(t, fixture, daemonRestartStarted, runtimeKind, rootlessBaseline.SemanticDigest, 75*time.Second)
daemonRestartDigest := rootlessQualDigestReport(daemonRestartReport.Report)
if daemonPIDBefore == daemonPIDAfter || invocationBefore == invocationAfter || daemonIDBefore != daemonIDAfter || daemonRestartDigest.SemanticDigest != rootlessBaseline.SemanticDigest {
t.Fatalf("daemon restart identity/parity mismatch: pid=%d/%d invocation=%s/%s daemon=%s/%s", daemonPIDBefore, daemonPIDAfter, invocationBefore, invocationAfter, daemonIDBefore, daemonIDAfter)
}
daemonEvidence := rootlessQualDirectEvidence(daemon, identity, collectorPIDAfter, daemonIDAfter, daemonRootlessAfterRestart, daemonRestartDigest)
daemonEvidence["previous_daemon_pid"] = daemonPIDBefore
daemonEvidence["daemon_pid"] = daemonPIDAfter
daemonEvidence["previous_daemon_invocation_id"] = invocationBefore
daemonEvidence["daemon_invocation_id"] = invocationAfter
appendScenario("daemon_restart", daemonRestartStarted, &daemonRestartReport.Report, daemonEvidence)
lossStarted := time.Now().UTC()
rootlessQualStopUnit(t, daemon.rootlessUnit)
helperReport := rootlessQualWaitReport(t, fixture, lossStarted, 90*time.Second, func(report agentsdocker.Report) bool {
return rootlessQualComplete(report) && report.Host.CollectionMode == agentsdocker.CollectionModeTypedHelperSummary && report.Host.Runtime == runtimeKind
})
rootlessQualAssertHelperSummaryOnly(t, helperReport.Report, runtimeKind)
helperDigest := rootlessQualDigestReport(helperReport.Report)
if helperDigest.SemanticDigest != rootfulBaseline.SemanticDigest || secureRuntimeCollectorMainPID(t) != collectorPIDAfter {
t.Fatalf("typed-helper fallback did not preserve rootful semantic baseline/collector PID")
}
appendScenario("socket_loss_helper_fallback", lossStarted, &helperReport.Report, map[string]any{
"collector_pid": collectorPIDAfter, "collection_mode": agentsdocker.CollectionModeTypedHelperSummary,
"direct_runtime_available": false, "helper_fallback": true, "inventory_complete": true,
"inventory_count": helperDigest.Count, "rootful_baseline_inventory_count": rootfulBaseline.Count,
"semantic_sha256": helperDigest.SemanticDigest, "rootful_baseline_semantic_sha256": rootfulBaseline.SemanticDigest,
"full_fields_present": false, "stats_present": false, "secondary_structure_sha256": "",
"container_actions_enabled": false, "container_updates_enabled": false, "collector_restart_count": 0,
})
recoveryStarted := time.Now().UTC()
rootlessQualStartRootless(t, daemon)
recoveryReport := rootlessQualWaitDirect(t, fixture, recoveryStarted, runtimeKind, rootlessBaseline.SemanticDigest, 90*time.Second)
recoveryDigest := rootlessQualDigestReport(recoveryReport.Report)
daemonRootlessAfterRecovery := rootlessQualDaemonRootless(t, daemon)
if !rootlessQualStableDigestEqual(recoveryDigest, daemonRestartDigest) || secureRuntimeCollectorMainPID(t) != collectorPIDAfter || rootlessQualDaemonID(t, daemon, true) != daemonIDBefore {
t.Fatalf("direct recovery did not restore the exact prior rootless telemetry/identity")
}
appendScenario("direct_recovery", recoveryStarted, &recoveryReport.Report,
rootlessQualDirectEvidence(daemon, identity, collectorPIDAfter, daemonIDBefore, daemonRootlessAfterRecovery, recoveryDigest))
ambiguityStarted := time.Now().UTC()
otherUnit, otherSocket := rootlessQualStartOtherRootless(t, daemon)
defer rootlessQualBestEffortStop(otherUnit)
if _, err := os.Lstat(otherSocket); err != nil {
t.Fatalf("second live rootless socket missing: %v", err)
}
liveSockets := rootlessQualDualSocketEvidence(t, daemon.uid)
secureRuntimeAssertHelperProtocol(t)
ambiguityOutput := rootlessQualRunUnpinnedCollector(t, server.URL, collectorCredential, 6*time.Second)
if !strings.Contains(strings.ToLower(ambiguityOutput), "ambiguous collector-owned rootless runtime endpoints") {
t.Fatalf("unpinned collector did not fail closed on dual sockets:\n%s", ambiguityOutput)
}
appendScenario("dual_socket_ambiguity_refusal", ambiguityStarted, nil, map[string]any{
"protected_collector_pid": collectorPIDAfter,
"live_sockets": liveSockets,
"probe_kind": "separate-unpinned-collector",
"admission_refused": true, "fail_closed": true, "daemon_probe_count": 0,
"container_actions_enabled": false, "collector_restart_count": 0,
})
rootlessQualStopUnit(t, otherUnit)
pinStarted := time.Now().UTC()
rootlessQualStopUnit(t, daemon.rootlessUnit)
unitBefore := rootlessQualServicePin(t, runtimeKind)
pinFallback := rootlessQualWaitReport(t, fixture, pinStarted, 90*time.Second, func(report agentsdocker.Report) bool {
return rootlessQualComplete(report) && report.Host.CollectionMode == agentsdocker.CollectionModeTypedHelperSummary && report.Host.Runtime == runtimeKind
})
previousUpdateStream, _, _ := agentshost.ParseReportSequenceID(pinFallback.Report.SequenceID)
updateStarted := time.Now().UTC()
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, server.URL, collectorCredential, "--update")
unitWhileAbsent := rootlessQualServicePin(t, runtimeKind)
if unitBefore != daemon.rootlessSock || unitWhileAbsent != unitBefore {
t.Fatalf("offline update lost exact rootless pin: before=%q after=%q", unitBefore, unitWhileAbsent)
}
collectorPIDAfterUpdate := secureRuntimeCollectorMainPID(t)
postUpdateFallback := rootlessQualWaitReport(t, fixture, updateStarted, 90*time.Second, func(report agentsdocker.Report) bool {
stream, _, ok := agentshost.ParseReportSequenceID(report.SequenceID)
return ok && stream != previousUpdateStream && rootlessQualComplete(report) && report.Host.CollectionMode == agentsdocker.CollectionModeTypedHelperSummary && report.Host.Runtime == runtimeKind
})
rootlessQualStartRootless(t, daemon)
pinReport := rootlessQualWaitDirect(t, fixture, postUpdateFallback.ReceivedAt, runtimeKind, rootlessBaseline.SemanticDigest, 90*time.Second)
pinDigest := rootlessQualDigestReport(pinReport.Report)
daemonRootlessAfterPinRecovery := rootlessQualDaemonRootless(t, daemon)
if !rootlessQualStableDigestEqual(pinDigest, recoveryDigest) || rootlessQualServicePin(t, runtimeKind) != daemon.rootlessSock {
t.Fatalf("exact pin recovery did not return to the same endpoint and telemetry")
}
pinEvidence := rootlessQualDirectEvidence(daemon, identity, collectorPIDAfterUpdate, daemonIDBefore, daemonRootlessAfterPinRecovery, pinDigest)
_, pinFallbackSequence, _ := agentshost.ParseReportSequenceID(postUpdateFallback.Report.SequenceID)
_, pinRecoverySequence, _ := agentshost.ParseReportSequenceID(pinReport.Report.SequenceID)
pinEvidence["previous_collector_pid"] = collectorPIDAfter
pinEvidence["previous_report_stream_id"] = previousUpdateStream
pinEvidence["pin_source"] = "root-owned-systemd-unit"
pinEvidence["pinned_socket_path"] = unitBefore
pinEvidence["socket_absent_observed"] = true
pinEvidence["fallback_report_sequence"] = pinFallbackSequence
pinEvidence["recovery_report_sequence"] = pinRecoverySequence
pinEvidence["recovered_socket_path"] = daemon.rootlessSock
pinEvidence["selected_socket_path"] = rootlessQualServicePin(t, runtimeKind)
pinEvidence["recovered_socket_uid"] = identity.SocketUID
pinEvidence["recovered_socket_gid"] = identity.SocketGID
pinEvidence["recovered_socket_mode"] = identity.SocketMode
pinEvidence["recovered_socket_type"] = "unix"
pinEvidence["recovered_socket_symlink"] = false
pinEvidence["candidate_count"] = 1
pinEvidence["daemon_probe_count"] = 1
pinEvidence["collector_restart_count"] = 1
appendScenario("exact_pin_recovery", pinStarted, &pinReport.Report, pinEvidence)
parityStarted := time.Now().UTC()
parityReport := rootlessQualWaitDirect(t, fixture, parityStarted, runtimeKind, rootlessBaseline.SemanticDigest, 75*time.Second)
parityDigest := rootlessQualDigestReport(parityReport.Report)
appendScenario("telemetry_parity", parityStarted, &parityReport.Report, map[string]any{
"collector_pid": collectorPIDAfterUpdate, "baseline_kind": "root-client-same-rootless-daemon",
"baseline_inventory_count": rootlessBaseline.Count, "collector_inventory_count": parityDigest.Count,
"baseline_semantic_sha256": rootlessBaseline.SemanticDigest, "collector_semantic_sha256": parityDigest.SemanticDigest,
"collector_full_fields_present": parityDigest.FullFieldsPresent, "collector_stats_present": parityDigest.StatsPresent,
"collector_secondary_inventory_present": parityDigest.SecondaryInventoryPresent,
})
authorityStarted := time.Now().UTC()
collectorUID := rootlessQualUID(t, "pulse-agent")
rootfulDenied := rootlessQualRootfulAccessDenied(t, daemon)
groups := strings.Fields(rootlessQualCommand(t, 10*time.Second, "id", "-nG", "pulse-agent"))
for _, group := range groups {
if group == "docker" || group == "podman" {
t.Fatalf("collector retained daemon group %q", group)
}
}
if !rootfulDenied || secureRuntimeCollectorProcessUID(t) != collectorUID || secureRuntimeCollectorHasArgument("--enable-commands") {
t.Fatal("authority isolation did not remain exact")
}
helperNetworkDenied := rootlessQualAssertHelperNetworkDenied(t)
commandSessionPresent := fixture.actionServer.IsAgentConnectedForOrganization(secureRuntimeLabOrgID, secureRuntimeLabAgentID)
if !helperNetworkDenied || commandSessionPresent || fixture.authorityReductionCount() < 1 {
t.Fatalf("authority evidence mismatch: helper_network_denied=%t command_session=%t reductions=%d", helperNetworkDenied, commandSessionPresent, fixture.authorityReductionCount())
}
appendScenario("authority_isolation", authorityStarted, nil, map[string]any{
"collector_pid": collectorPIDAfterUpdate, "collector_uid": collectorUID, "effective_uid": collectorUID,
"effective_root": false, "safe_profile_enabled": true, "commands_enabled": false,
"privileged_helper_enabled": true, "reduction_request_observed": true,
"collector_command_transport_present": false, "collector_command_session_present": false,
"container_actions_enabled": false, "container_updates_enabled": false,
"rootful_socket_access": false, "helper_network_access": false,
})
cleanupStarted := time.Now().UTC()
rootlessQualUninstallPulse(t, installerPath, server.URL, collectorCredential)
if registered, revoked, uninstalls := fixture.collectorLifecycleSnapshot(); registered || !revoked || uninstalls != 2 {
t.Fatalf("final collector uninstall was not durably modeled: registered=%t revoked=%t uninstalls=%d", registered, revoked, uninstalls)
}
rootlessQualStopUnit(t, daemon.rootlessUnit)
rootlessQualStopUnit(t, daemon.rootfulUnit)
rootlessQualStopUserManager(t, daemon)
rootlessQualRemoveRuntimeState(t, daemon)
rootlessQualAssertPulseRemoved(t)
for _, socket := range []string{daemon.rootlessSock, daemon.rootfulSock} {
if _, err := os.Lstat(socket); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("cleanup left socket %s: %v", socket, err)
}
}
userStateClean := rootlessQualUserStateClean(daemon)
if !userStateClean {
t.Fatal("cleanup left dedicated rootless runtime state")
}
appendScenario("cleanup", cleanupStarted, nil, map[string]any{
"runtime_stopped": true, "socket_absent": true, "fixtures_removed": true, "user_state_clean": userStateClean,
})
artifacts := rootlessQualArtifactIdentities(t, installerPath)
sourceHashes := rootlessQualSourceHashes(t)
receipt := rootlessQualReceipt{
SchemaVersion: 1, Kind: "pulse-secure-runtime-rootless-qualification", Result: "passed",
SourceCommit: strings.TrimSpace(os.Getenv("PULSE_ROOTLESS_SOURCE_COMMIT")),
StartedAt: started.Format(time.RFC3339Nano), CompletedAt: time.Now().UTC().Format(time.RFC3339Nano),
SourceHashes: sourceHashes, Artifacts: artifacts,
Runs: []rootlessQualRun{{
Host: rootlessQualHost{
MachineID: strings.TrimSpace(string(rootlessQualReadFile(t, "/etc/machine-id"))), Architecture: runtime.GOARCH,
Kernel: rootlessQualCommand(t, 10*time.Second, "uname", "-srvmo"),
SystemdVersion: strings.SplitN(rootlessQualCommand(t, 10*time.Second, "systemctl", "--version"), "\n", 2)[0],
},
Runtime: rootlessQualRuntime{
Runtime: runtimeKind, RuntimeVersion: identity.RuntimeVersion, DaemonID: daemonIDBefore,
CollectorUID: daemon.uid, SocketPath: daemon.rootlessSock, SocketUID: identity.SocketUID,
SocketGID: identity.SocketGID, SocketMode: identity.SocketMode, DaemonRootless: daemonRootlessObserved,
SocketType: "unix", SocketSymlink: false,
},
Scenarios: scenarios,
}},
}
if err := rootlessQualValidateReceipt(receipt, 1); err != nil {
t.Fatalf("generated receipt failed validation: %v", err)
}
rootlessQualWriteJSON(t, receiptPath, receipt)
}
type rootlessQualBaseline struct {
Count int
SemanticDigest string
}
type rootlessQualReportDigest struct {
Count int
InventoryDigest string
SemanticDigest string
StatsDigest string
StatsPresent bool
FullFieldsPresent bool
SecondaryDigest string
SecondaryInventoryPresent bool
}
type rootlessQualContainerSemantic struct {
Name string
Image string
State string
}
type rootlessQualIdentity struct {
RuntimeVersion string `json:"runtime_version"`
SocketUID int `json:"socket_uid"`
SocketGID int `json:"socket_gid"`
SocketMode string `json:"socket_mode"`
}
func rootlessQualDirectEvidence(d rootlessQualDaemon, identity rootlessQualIdentity, collectorPID int, daemonID string, daemonRootless bool, digest rootlessQualReportDigest) map[string]any {
return map[string]any{
"collector_pid": collectorPID, "service_pid": collectorPID,
"collection_path": "collector-owned-rootless-socket", "inventory_complete": true, "inventory_count": digest.Count,
"semantic_sha256": digest.SemanticDigest, "full_fields_present": digest.FullFieldsPresent,
"stats_present": digest.StatsPresent, "secondary_structure_sha256": digest.SecondaryDigest,
"daemon_id": daemonID, "daemon_rootless": daemonRootless,
"socket_path": d.rootlessSock, "socket_uid": identity.SocketUID, "socket_gid": identity.SocketGID,
"socket_mode": identity.SocketMode, "socket_type": "unix", "socket_symlink": false,
}
}
func rootlessQualRequireDisposableHost(t *testing.T, runtimeKind, receiptPath string) {
t.Helper()
if os.Geteuid() != 0 || (runtimeKind != "docker" && runtimeKind != "podman") {
t.Fatalf("qualification requires root and PULSE_ROOTLESS_RUNTIME=docker|podman")
}
marker, err := os.ReadFile(rootlessQualMarker)
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)
}
osRelease := string(rootlessQualReadFile(t, "/etc/os-release"))
if !strings.Contains(osRelease, "VERSION_ID=\"24.04\"") && !strings.Contains(osRelease, "VERSION_ID=24.04") {
t.Fatal("qualification host is not Ubuntu 24.04")
}
if _, err := os.Stat("/run/systemd/system"); err != nil {
t.Fatalf("qualification host is not booted under systemd: %v", err)
}
if routes := string(rootlessQualReadFile(t, "/proc/net/route")); rootlessQualHasDefaultRoute(routes) {
t.Fatal("qualification workload must run with outer-container networking disabled")
}
}
func rootlessQualHasDefaultRoute(routes string) bool {
for _, line := range strings.Split(routes, "\n") {
fields := strings.Fields(line)
if len(fields) >= 2 && fields[1] == "00000000" {
return true
}
}
return false
}
func rootlessQualPrepareDaemons(t *testing.T, runtimeKind string) rootlessQualDaemon {
t.Helper()
if _, err := exec.Command("id", "-u", "pulse-agent").Output(); err != nil {
rootlessQualCommand(t, 10*time.Second, "useradd", "--system", "--create-home", "--home-dir", "/var/lib/pulse-rootless", "--shell", "/usr/sbin/nologin", "pulse-agent")
}
uid := rootlessQualUID(t, "pulse-agent")
home := "/var/lib/pulse-rootless"
rootlessQualWriteSubID(t, "/etc/subuid", "pulse-agent:100000:65536")
rootlessQualWriteSubID(t, "/etc/subgid", "pulse-agent:100000:65536")
paths := []string{filepath.Join("/run/user", strconv.Itoa(uid)), filepath.Join(home, "fixture"), filepath.Join(home, "docker"), filepath.Join(home, "podman")}
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)
}
busybox, err := os.ReadFile("/bin/busybox")
if err != nil {
t.Fatalf("read offline busybox fixture: %v", err)
}
if err := os.WriteFile(filepath.Join(home, "fixture", "busybox"), busybox, 0o755); err != nil {
t.Fatal(err)
}
rootlessQualCommand(t, 10*time.Second, "chown", "-R", "pulse-agent:pulse-agent", home)
d := rootlessQualDaemon{runtime: runtimeKind, uid: uid, home: home, rootlessUnit: "pulse-rootless-" + runtimeKind, rootfulUnit: "pulse-rootful-" + runtimeKind}
if runtimeKind == "docker" {
d.rootlessSock = filepath.Join("/run/user", strconv.Itoa(uid), "docker.sock")
d.rootfulSock = "/var/run/docker.sock"
} else {
d.rootlessSock = filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock")
d.rootfulSock = "/run/podman/podman.sock"
}
return d
}
func rootlessQualStartRootless(t *testing.T, d rootlessQualDaemon) {
t.Helper()
rootlessQualBestEffortStop(d.rootlessUnit)
if d.runtime == "docker" {
rootlessQualCommand(t, 20*time.Second, "systemd-run", rootlessQualDockerStartArgs(d)...)
} else {
rootlessQualCommand(t, 20*time.Second, "install", "-d", "-o", "pulse-agent", "-g", "pulse-agent", "-m", "0700", filepath.Dir(d.rootlessSock))
rootlessQualCommand(t, 20*time.Second, "systemd-run", "--quiet", "--collect", "--unit", d.rootlessUnit, "--property=Type=exec", "--property=User=pulse-agent", "--property=Group=pulse-agent", "--",
"/usr/bin/env", "HOME="+d.home, "XDG_RUNTIME_DIR="+filepath.Join("/run/user", strconv.Itoa(d.uid)),
"/usr/bin/podman", "system", "service", "--time=0", "unix://"+d.rootlessSock)
}
rootlessQualWaitSocket(t, d.rootlessSock)
rootlessQualRuntimePing(t, d, true)
rootlessQualCaptureIdentity(t, d)
}
func rootlessQualDockerStartArgs(d rootlessQualDaemon) []string {
return []string{"--quiet", "--collect", "--unit", d.rootlessUnit, "--property=Type=exec", "--property=User=pulse-agent", "--property=Group=pulse-agent", "--",
"/usr/bin/env", "HOME=" + d.home, "XDG_RUNTIME_DIR=" + filepath.Dir(d.rootlessSock), "DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns", "DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=none",
"/usr/bin/dockerd-rootless.sh", "--host=unix://" + d.rootlessSock, "--data-root=" + filepath.Join(d.home, "docker", "data"), "--exec-root=" + filepath.Join(d.home, "docker", "exec"), "--pidfile=" + filepath.Join(d.home, "docker", "dockerd.pid"), "--storage-driver=vfs", "--iptables=false", "--bridge=none"}
}
func rootlessQualStartRootful(t *testing.T, d rootlessQualDaemon) {
t.Helper()
rootlessQualBestEffortStop(d.rootfulUnit)
rootlessQualCommand(t, 10*time.Second, "install", "-d", "-o", "root", "-g", "root", "-m", "0755", filepath.Dir(d.rootfulSock))
if d.runtime == "docker" {
rootlessQualCommand(t, 20*time.Second, "systemd-run", "--quiet", "--collect", "--unit", d.rootfulUnit, "--property=Type=exec", "--",
"/usr/bin/dockerd", "--host=unix://"+d.rootfulSock, "--data-root=/var/lib/pulse-rootful-docker", "--exec-root=/run/pulse-rootful-docker", "--pidfile=/run/pulse-rootful-docker.pid", "--storage-driver=vfs", "--iptables=false", "--bridge=none")
} else {
rootlessQualCommand(t, 20*time.Second, "systemd-run", "--quiet", "--collect", "--unit", d.rootfulUnit, "--property=Type=exec", "--",
"/usr/bin/podman", "system", "service", "--time=0", "unix://"+d.rootfulSock)
}
rootlessQualWaitSocket(t, d.rootfulSock)
rootlessQualCommand(t, 10*time.Second, "chmod", "0660", d.rootfulSock)
rootlessQualRuntimePing(t, d, false)
}
func rootlessQualStartOtherRootless(t *testing.T, d rootlessQualDaemon) (string, string) {
t.Helper()
other := d
if d.runtime == "docker" {
other.runtime, other.rootlessUnit = "podman", "pulse-rootless-podman-ambiguity"
other.rootlessSock = filepath.Join("/run/user", strconv.Itoa(d.uid), "podman", "podman.sock")
} else {
other.runtime, other.rootlessUnit = "docker", "pulse-rootless-docker-ambiguity"
other.rootlessSock = filepath.Join("/run/user", strconv.Itoa(d.uid), "docker.sock")
}
rootlessQualStartRootless(t, other)
return other.rootlessUnit, other.rootlessSock
}
func rootlessQualOtherUnit(runtimeKind string) string {
if runtimeKind == "docker" {
return "pulse-rootless-podman-ambiguity"
}
return "pulse-rootless-docker-ambiguity"
}
func rootlessQualCreateFixture(t *testing.T, d rootlessQualDaemon, rootless bool) {
t.Helper()
cli := rootlessQualCLI(d, rootless)
rootlessQualCommand(t, 2*time.Minute, cli[0], append(cli[1:], "build", "--network=none", "-t", rootlessQualFixture, "-f", filepath.Join(d.home, "fixture", "Containerfile"), filepath.Join(d.home, "fixture"))...)
rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], rootlessQualRunningFixtureArgs()...)...)
if out, err := rootlessQualCommandError(30*time.Second, cli[0], append(cli[1:], "run", "--name", rootlessQualExitedName, rootlessQualFixture, "true")...); err != nil {
t.Fatalf("create exited fixture: %v\n%s", err, out)
}
}
func rootlessQualRunningFixtureArgs() []string {
return []string{"run", "-d", "--restart=always", "--name", rootlessQualRunningName, rootlessQualFixture, "sleep", "3600"}
}
func rootlessQualCLI(d rootlessQualDaemon, rootless bool) []string {
if d.runtime == "docker" {
host := d.rootfulSock
if rootless {
host = d.rootlessSock
return []string{"runuser", "-u", "pulse-agent", "--", "env", "HOME=" + d.home, "XDG_RUNTIME_DIR=" + filepath.Dir(host), "DOCKER_HOST=unix://" + host, "docker"}
}
return []string{"docker", "--host", "unix://" + host}
}
host := d.rootfulSock
if rootless {
host = d.rootlessSock
return []string{"runuser", "-u", "pulse-agent", "--", "env", "HOME=" + d.home, "XDG_RUNTIME_DIR=" + filepath.Join("/run/user", strconv.Itoa(d.uid)), "podman", "--url", "unix://" + host}
}
return []string{"podman", "--url", "unix://" + host}
}
func rootlessQualRuntimePing(t *testing.T, d rootlessQualDaemon, rootless bool) {
t.Helper()
cli := rootlessQualCLI(d, rootless)
rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "info")...)
}
func rootlessQualRuntimeBaseline(t *testing.T, d rootlessQualDaemon, rootless bool) rootlessQualBaseline {
t.Helper()
cli := rootlessQualCLI(d, rootless)
format := "{{.Names}}|{{.Image}}|{{.State}}"
out := rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "ps", "-a", "--format", format)...)
return rootlessQualBaselineFromPSOutput(out)
}
func rootlessQualBaselineFromPSOutput(out string) rootlessQualBaseline {
lines := strings.Split(strings.TrimSpace(out), "\n")
var normalized []rootlessQualContainerSemantic
for _, line := range lines {
parts := strings.SplitN(strings.TrimSpace(line), "|", 3)
if len(parts) == 3 {
normalized = append(normalized, rootlessQualContainerSemantic{
Name: strings.TrimSpace(strings.TrimPrefix(parts[0], "/")),
Image: strings.TrimSpace(parts[1]),
State: strings.TrimSpace(parts[2]),
})
}
}
sort.Slice(normalized, func(i, j int) bool { return normalized[i].Name < normalized[j].Name })
return rootlessQualBaseline{Count: len(normalized), SemanticDigest: rootlessQualHashJSON(normalized)}
}
func rootlessQualWaitDirect(t *testing.T, fixture *secureRuntimeLabFixture, after time.Time, runtimeKind, semanticDigest string, timeout time.Duration) secureRuntimeDockerReport {
t.Helper()
return rootlessQualWaitReport(t, fixture, after, timeout, func(report agentsdocker.Report) bool {
return rootlessQualComplete(report) && report.Host.CollectionMode != agentsdocker.CollectionModeTypedHelperSummary &&
report.Host.Runtime == runtimeKind && len(report.Containers) == 2 && rootlessQualSemanticDigest(report) == semanticDigest
})
}
func rootlessQualWaitReport(t *testing.T, fixture *secureRuntimeLabFixture, after time.Time, timeout time.Duration, predicate func(agentsdocker.Report) bool) secureRuntimeDockerReport {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
for _, candidate := range fixture.dockerSnapshot() {
if candidate.ReceivedAt.After(after) && predicate(candidate.Report) {
return candidate
}
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("timed out waiting for container report after %s", after.Format(time.RFC3339Nano))
return secureRuntimeDockerReport{}
}
func rootlessQualComplete(report agentsdocker.Report) bool {
return report.InventoryComplete == nil || *report.InventoryComplete
}
func rootlessQualAssertHelperSummaryOnly(t *testing.T, report agentsdocker.Report, runtimeKind string) {
t.Helper()
if report.InventoryComplete == nil || !*report.InventoryComplete {
t.Fatal("typed-helper runtime summary did not declare complete inventory")
}
if report.Host.Runtime != runtimeKind || report.Host.CollectionMode != agentsdocker.CollectionModeTypedHelperSummary {
t.Fatalf("typed-helper collection posture = runtime:%q mode:%q", report.Host.Runtime, report.Host.CollectionMode)
}
if len(report.Images) != 0 || len(report.Volumes) != 0 || len(report.Networks) != 0 || len(report.Services) != 0 || len(report.Tasks) != 0 || len(report.Nodes) != 0 || len(report.Secrets) != 0 || len(report.Configs) != 0 || report.StorageUsage != nil {
t.Fatal("typed-helper runtime summary fabricated unsupported secondary inventories")
}
for _, container := range report.Containers {
if container.ImageDigest != "" || container.Health != "" || len(container.HealthcheckTargets) != 0 || container.CPUPercent != 0 || container.MemoryUsageBytes != 0 || container.MemoryLimitBytes != 0 || container.MemoryPercent != 0 || container.UptimeSeconds != 0 || container.RestartCount != 0 || container.ExitCode != 0 || container.OOMKilled != nil || container.StartedAt != nil || container.FinishedAt != nil || len(container.Ports) != 0 || len(container.Labels) != 0 || len(container.Env) != 0 || len(container.Networks) != 0 || container.NetworkRXBytes != 0 || container.NetworkTXBytes != 0 || container.WritableLayerBytes != 0 || container.RootFilesystemBytes != 0 || container.BlockIO != nil || len(container.Mounts) != 0 || container.Podman != nil || container.UpdateStatus != nil {
t.Fatalf("typed-helper container %q escaped the summary-only boundary: %+v", container.ID, container)
}
}
}
func rootlessQualDigestReport(report agentsdocker.Report) rootlessQualReportDigest {
type stats struct {
Name string
MemoryLimited bool
OOMKnown bool
RuntimeDetails bool
}
semanticRows := make([]rootlessQualContainerSemantic, 0, len(report.Containers))
statsRows := make([]stats, 0, len(report.Containers))
fullFieldsPresent := len(report.Containers) > 0
runningStatsPresent := false
for _, item := range report.Containers {
semanticRows = append(semanticRows, rootlessQualContainerSemantic{Name: item.Name, Image: item.Image, State: item.State})
statsRows = append(statsRows, stats{Name: item.Name, MemoryLimited: item.MemoryLimitBytes > 0, OOMKnown: item.OOMKilled != nil, RuntimeDetails: item.StartedAt != nil || item.FinishedAt != nil})
if item.CreatedAt.IsZero() || item.Status == "" || item.OOMKilled == nil || (item.StartedAt == nil && item.FinishedAt == nil) {
fullFieldsPresent = false
}
if strings.EqualFold(item.State, "running") && (item.MemoryUsageBytes > 0 || item.MemoryLimitBytes > 0 || item.CPUPercent != 0 || item.NetworkRXBytes > 0 || item.NetworkTXBytes > 0 || item.BlockIO != nil) {
runningStatsPresent = true
}
}
sort.Slice(semanticRows, func(i, j int) bool { return semanticRows[i].Name < semanticRows[j].Name })
sort.Slice(statsRows, func(i, j int) bool { return statsRows[i].Name < statsRows[j].Name })
imageNames := make([]string, 0, len(report.Images))
for _, image := range report.Images {
imageNames = append(imageNames, strings.Join(image.RepoTags, ","))
}
volumeNames := make([]string, 0, len(report.Volumes))
for _, volume := range report.Volumes {
volumeNames = append(volumeNames, volume.Name)
}
networkNames := make([]string, 0, len(report.Networks))
for _, network := range report.Networks {
networkNames = append(networkNames, network.Name)
}
sort.Strings(imageNames)
sort.Strings(volumeNames)
sort.Strings(networkNames)
secondary := map[string]any{"images": imageNames, "volumes": volumeNames, "networks": networkNames}
inventory := map[string]any{"containers": semanticRows, "runtime": report.Host.Runtime, "mode": report.Host.CollectionMode}
return rootlessQualReportDigest{
Count: len(report.Containers), InventoryDigest: rootlessQualHashJSON(inventory), SemanticDigest: rootlessQualHashJSON(semanticRows),
StatsDigest: rootlessQualHashJSON(statsRows), StatsPresent: runningStatsPresent,
FullFieldsPresent: fullFieldsPresent, SecondaryDigest: rootlessQualHashJSON(secondary),
SecondaryInventoryPresent: len(report.Images) > 0 && len(report.Networks) > 0,
}
}
func rootlessQualSemanticDigest(report agentsdocker.Report) string {
return rootlessQualDigestReport(report).SemanticDigest
}
func rootlessQualStableDigestEqual(left, right rootlessQualReportDigest) bool {
return left.Count == right.Count && left.InventoryDigest == right.InventoryDigest &&
left.SemanticDigest == right.SemanticDigest && left.SecondaryDigest == right.SecondaryDigest &&
left.StatsPresent && right.StatsPresent
}
func rootlessQualHashJSON(value any) string {
raw, err := json.Marshal(value)
if err != nil {
panic(err)
}
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
func rootlessQualDaemonID(t *testing.T, d rootlessQualDaemon, rootless bool) string {
t.Helper()
cli := rootlessQualCLI(d, rootless)
if d.runtime == "docker" {
return rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "info", "--format", "{{.ID}}")...)
}
info := rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "info", "--format", "json")...)
var decoded map[string]any
if err := json.Unmarshal([]byte(info), &decoded); err != nil {
t.Fatalf("decode podman info: %v", err)
}
store, _ := decoded["store"].(map[string]any)
identity := map[string]any{"graphRoot": store["graphRoot"], "runRoot": store["runRoot"], "runtime": d.runtime}
return rootlessQualHashJSON(identity)
}
func rootlessQualDaemonRootless(t *testing.T, d rootlessQualDaemon) bool {
t.Helper()
cli := rootlessQualCLI(d, true)
if d.runtime == "docker" {
securityOptions := rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "info", "--format", "{{json .SecurityOptions}}")...)
return strings.Contains(strings.ToLower(securityOptions), "rootless")
}
rootless := rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "info", "--format", "{{.Host.Security.Rootless}}")...)
return strings.EqualFold(strings.TrimSpace(rootless), "true")
}
func rootlessQualUnitIdentity(t *testing.T, unit string) (int, string) {
t.Helper()
pidText := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", unit, "--property=MainPID", "--value")
pid, err := strconv.Atoi(pidText)
if err != nil || pid <= 1 {
t.Fatalf("unit %s returned invalid MainPID %q", unit, pidText)
}
invocation := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", unit, "--property=InvocationID", "--value")
if len(invocation) != 32 {
t.Fatalf("unit %s returned invalid InvocationID %q", unit, invocation)
}
return pid, invocation
}
func rootlessQualRunUnpinnedCollector(t *testing.T, serverURL, collectorCredential string, duration time.Duration) string {
t.Helper()
protectedPID := secureRuntimeCollectorMainPID(t)
stateDir, err := os.MkdirTemp("/tmp", "pulse-unpinned-probe-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(stateDir)
collectorUID := rootlessQualUID(t, "pulse-agent")
collectorGID := rootlessQualGID(t, "pulse-agent")
if err := os.Chown(stateDir, collectorUID, collectorGID); err != nil {
t.Fatalf("chown ambiguity probe state: %v", err)
}
if err := os.Chmod(stateDir, 0o700); err != nil {
t.Fatalf("chmod ambiguity probe state: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
cmd := rootlessQualProcessGroupCommand(ctx, "runuser", rootlessQualUnpinnedCollectorArgs(serverURL, collectorCredential, stateDir, collectorUID)...)
out, err := cmd.CombinedOutput()
if err == nil || !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatalf("unpinned ambiguity probe did not remain alive until bounded cancellation: err=%v\n%s", err, out)
}
if afterPID := secureRuntimeCollectorMainPID(t); afterPID != protectedPID {
t.Fatalf("separate ambiguity probe disturbed protected collector: before=%d after=%d", protectedPID, afterPID)
}
return string(out)
}
func rootlessQualUnpinnedCollectorArgs(serverURL, collectorCredential, stateDir string, collectorUID int) []string {
return []string{
"-u", "pulse-agent", "--", "env", "-i",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/var/lib/pulse-rootless",
"XDG_RUNTIME_DIR=" + filepath.Join("/run/user", strconv.Itoa(collectorUID)),
"PULSE_DOCKER_RUNTIME=auto",
"PULSE_AGENT_HELPER_SOCKET=/run/pulse-agent/helper.sock",
"PULSE_URL=" + serverURL,
"PULSE_TOKEN=" + collectorCredential,
"PULSE_INTERVAL=1s",
"PULSE_AGENT_ID=" + secureRuntimeLabAgentID,
"PULSE_HOSTNAME=" + secureRuntimeLabHostname,
"PULSE_ENABLE_HOST=false",
"PULSE_ENABLE_DOCKER=true",
"PULSE_ENABLE_COMMANDS=false",
"PULSE_AGENT_ALLOW_PLAINTEXT_HTTP=true",
"/usr/local/bin/pulse-agent", "--state-dir", stateDir, "--health-addr", "",
}
}
func rootlessQualProcessGroupCommand(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, args...)
// The ambiguity probe crosses runuser before starting the collector. Killing
// only runuser leaves the collector holding CombinedOutput's pipe open.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.Cancel = func() error {
if cmd.Process == nil {
return os.ErrProcessDone
}
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil {
if errors.Is(err, syscall.ESRCH) {
return os.ErrProcessDone
}
return err
}
return nil
}
return cmd
}
func TestRootlessQualificationCancellationKillsProbeProcessGroup(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
started := time.Now()
cmd := rootlessQualProcessGroupCommand(ctx, "sh", "-c", "sleep 30 & child=$!; echo $child; wait")
out, err := cmd.CombinedOutput()
if err == nil || !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatalf("process-group probe did not reach bounded cancellation: err=%v output=%q", err, out)
}
if elapsed := time.Since(started); elapsed > 3*time.Second {
t.Fatalf("process-group cancellation waited for an inherited output pipe: elapsed=%s output=%q", elapsed, out)
}
childPID, parseErr := strconv.Atoi(strings.TrimSpace(string(out)))
if parseErr != nil || childPID <= 1 {
t.Fatalf("process-group probe returned invalid child PID %q: %v", out, parseErr)
}
deadline := time.Now().Add(time.Second)
for {
err = syscall.Kill(childPID, 0)
if errors.Is(err, syscall.ESRCH) {
break
}
if time.Now().After(deadline) {
t.Fatalf("probe child %d survived process-group cancellation: %v", childPID, err)
}
time.Sleep(10 * time.Millisecond)
}
}
func TestRootlessQualificationUnpinnedProbeUsesCleanAutomaticRuntimeEnvironment(t *testing.T) {
args := rootlessQualUnpinnedCollectorArgs("http://127.0.0.1:8080", "test-token", "/tmp/test-state", 996)
if len(args) < 5 || !slices.Equal(args[:5], []string{"-u", "pulse-agent", "--", "env", "-i"}) {
t.Fatalf("unpinned probe does not start with a clean runuser environment: %q", args)
}
if !slices.Contains(args, "PULSE_DOCKER_RUNTIME=auto") {
t.Fatalf("unpinned probe does not force automatic runtime selection: %q", args)
}
if !slices.Contains(args, "PULSE_AGENT_HELPER_SOCKET=/run/pulse-agent/helper.sock") {
t.Fatalf("unpinned probe does not exercise the installed typed-helper boundary: %q", args)
}
for _, arg := range args {
for _, forbidden := range []string{"DOCKER_HOST=", "PODMAN_HOST=", "CONTAINER_HOST="} {
if strings.HasPrefix(arg, forbidden) {
t.Fatalf("unpinned probe retained endpoint selector %q", arg)
}
}
}
}
func rootlessQualServicePin(t *testing.T, runtimeKind string) string {
t.Helper()
environment := secureRuntimeSystemdProperty(t, "Environment")
key := "DOCKER_HOST=unix://"
if runtimeKind == "podman" {
key = "CONTAINER_HOST=unix://"
}
for _, field := range strings.Fields(environment) {
field = strings.Trim(field, `"`)
if strings.HasPrefix(field, key) {
return strings.TrimPrefix(field, key)
}
}
t.Fatalf("collector unit lacks %s pin: %s", key, environment)
return ""
}
func rootlessQualRootfulAccessDenied(t *testing.T, d rootlessQualDaemon) bool {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "runuser", "-u", "pulse-agent", "--", "curl", "-fsS", "--max-time", "2", "--unix-socket", d.rootfulSock, "http://runtime/_ping")
return cmd.Run() != nil
}
func rootlessQualDualSocketEvidence(t *testing.T, uid int) []map[string]any {
t.Helper()
result := make([]map[string]any, 0, 2)
for _, item := range []struct{ runtime, path string }{
{"docker", filepath.Join("/run/user", strconv.Itoa(uid), "docker.sock")},
{"podman", filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock")},
} {
socketUID, socketGID, mode := rootlessQualSocketIdentity(t, item.path)
if socketUID != uid {
t.Fatalf("rootless %s socket is owned by UID %d, want collector UID %d", item.runtime, socketUID, uid)
}
result = append(result, map[string]any{
"runtime": item.runtime, "path": item.path, "uid": socketUID, "gid": socketGID,
"mode": mode, "type": "unix", "symlink": false,
})
_ = os.Remove(item.path + ".qualification-identity")
}
return result
}
func rootlessQualAssertHelperNetworkDenied(t *testing.T) bool {
t.Helper()
observations := secureRuntimeAssertHelperLoopbackNetworkDenied(t)
return observations["helper_namespace_connection"] == "denied" && observations["host_canary_reachable"] == true
}
func rootlessQualCaptureIdentity(t *testing.T, d rootlessQualDaemon) {
t.Helper()
uid, gid, mode := rootlessQualSocketIdentity(t, d.rootlessSock)
cli := rootlessQualCLI(d, true)
version := rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "version", "--format", "{{.Server.Version}}")...)
if d.runtime == "podman" {
version = rootlessQualCommand(t, 30*time.Second, cli[0], append(cli[1:], "version", "--format", "{{.Server.Version}}")...)
}
rootlessQualWriteJSON(t, d.rootlessSock+".qualification-identity", rootlessQualIdentity{RuntimeVersion: version, SocketUID: uid, SocketGID: gid, SocketMode: mode})
}
func rootlessQualReadIdentityRecord(t *testing.T, d rootlessQualDaemon) rootlessQualIdentity {
t.Helper()
path := d.rootlessSock + ".qualification-identity"
raw := rootlessQualReadFile(t, path)
var identity rootlessQualIdentity
if err := json.Unmarshal(raw, &identity); err != nil {
t.Fatalf("decode runtime identity: %v", err)
}
return identity
}
func rootlessQualSocketIdentity(t *testing.T, path string) (int, int, string) {
t.Helper()
info, err := os.Lstat(path)
if err != nil {
t.Fatalf("inspect socket identity %s: %v", path, err)
}
if info.Mode()&os.ModeSocket == 0 || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm()&0o600 != 0o600 || info.Mode().Perm()&0o006 != 0 {
t.Fatalf("unsafe rootless socket mode at %s: %s", path, info.Mode())
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
t.Fatalf("socket %s lacks Unix stat identity", path)
}
return int(stat.Uid), int(stat.Gid), fmt.Sprintf("%04o", info.Mode().Perm())
}
func rootlessQualArtifactIdentities(t *testing.T, installerPath string) rootlessQualArtifacts {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
collectorPath := strings.TrimSpace(os.Getenv("PULSE_SECURE_RUNTIME_COLLECTOR"))
helperPath := strings.TrimSpace(os.Getenv("PULSE_SECURE_RUNTIME_HELPER"))
return rootlessQualArtifacts{
QualificationTest: rootlessQualGoArtifact(t, executable, "dockeragent.test"),
Collector: rootlessQualGoArtifact(t, collectorPath, "pulse-agent"),
Helper: rootlessQualGoArtifact(t, helperPath, "pulse-agent-helper"),
Installer: rootlessQualInstallerArtifact{
PathBasename: filepath.Base(installerPath), SHA256: secureRuntimeHash(rootlessQualReadFile(t, installerPath)),
},
}
}
func rootlessQualGoArtifact(t *testing.T, path, basename string) rootlessQualArtifact {
t.Helper()
info, err := buildinfo.ReadFile(path)
if err != nil {
t.Fatalf("read Go build metadata for %s: %v", path, err)
}
artifact := rootlessQualArtifact{
PathBasename: filepath.Base(path), SHA256: secureRuntimeHash(rootlessQualReadFile(t, path)),
Package: info.Path, GoVersion: info.GoVersion,
}
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
artifact.VCSRevision = setting.Value
case "vcs.modified":
artifact.VCSModified = setting.Value == "true"
}
}
wantCommit := strings.TrimSpace(os.Getenv("PULSE_ROOTLESS_SOURCE_COMMIT"))
if artifact.PathBasename != basename || artifact.Package == "" || artifact.VCSRevision != wantCommit || artifact.VCSModified {
t.Fatalf("qualification artifact is not an exact clean source build: %+v", artifact)
}
return artifact
}
func rootlessQualSourceHashes(t *testing.T) map[string]string {
t.Helper()
path := strings.TrimSpace(os.Getenv("PULSE_ROOTLESS_SOURCE_HASHES"))
if !filepath.IsAbs(path) {
t.Fatalf("PULSE_ROOTLESS_SOURCE_HASHES must be absolute: %q", path)
}
var hashes map[string]string
if err := json.Unmarshal(rootlessQualReadFile(t, path), &hashes); err != nil {
t.Fatalf("decode source hashes: %v", err)
}
if len(hashes) == 0 {
t.Fatal("source hash map is empty")
}
return hashes
}
func rootlessQualUninstallPulse(t *testing.T, installerPath, serverURL, collectorCredential string) {
t.Helper()
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, serverURL, collectorCredential, "--uninstall")
}
func rootlessQualAssertPulseRemoved(t *testing.T) {
t.Helper()
for _, path := range secureRuntimeInstalledPaths {
if _, err := os.Lstat(path); err == nil {
t.Fatalf("Pulse cleanup left %s", path)
} else if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("inspect cleanup path %s: %v", path, err)
}
}
}
func rootlessQualRemoveRuntimeState(t *testing.T, d rootlessQualDaemon) {
t.Helper()
for _, path := range []string{d.home, filepath.Join("/run/user", strconv.Itoa(d.uid)), "/var/lib/pulse-rootful-docker", "/var/lib/containers/storage"} {
if err := os.RemoveAll(path); err != nil {
t.Fatalf("remove runtime state %s: %v", path, err)
}
}
}
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", "/var/lib/systemd/linger/pulse-agent"} {
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
return false
}
}
return true
}
func rootlessQualStopUnit(t *testing.T, unit string) {
t.Helper()
rootlessQualCommand(t, 30*time.Second, "systemctl", "stop", unit)
resetOutput, resetErr := rootlessQualCommandError(30*time.Second, "systemctl", "reset-failed", unit)
if resetErr == nil {
return
}
loadState, stateErr := rootlessQualCommandError(30*time.Second, "systemctl", "show", "--property=LoadState", "--value", unit)
if stateErr == nil && strings.TrimSpace(loadState) == "not-found" {
return
}
if rootlessQualResetFailureIsAlreadyUnloaded(resetOutput, unit) {
return
}
t.Fatalf("systemctl reset-failed %s: %v\n%s", unit, resetErr, resetOutput)
}
func rootlessQualResetFailureIsAlreadyUnloaded(resetOutput, unit string) bool {
message := strings.ToLower(resetOutput)
unit = strings.ToLower(strings.TrimSpace(unit))
if unit == "" || !strings.Contains(message, "failed to reset failed state") || !strings.Contains(message, unit) {
return false
}
return strings.Contains(message, "not loaded") || strings.Contains(message, "not found")
}
func TestRootlessQualificationAcceptsAlreadyUnloadedUnitAfterSuccessfulStop(t *testing.T) {
const unit = "user@996.service"
if !rootlessQualResetFailureIsAlreadyUnloaded(
"Failed to reset failed state of unit user@996.service: Unit user@996.service not loaded.",
unit,
) {
t.Fatal("already-unloaded inactive unit was not accepted")
}
for _, test := range []struct {
name string
output string
unit string
}{
{name: "different unit", output: "Failed to reset failed state of unit user@997.service: Unit user@997.service not loaded.", unit: unit},
{name: "different failure", output: "Failed to reset failed state of unit user@996.service: Access denied.", unit: unit},
{name: "unrelated diagnostic", output: "Unit user@996.service not loaded.", unit: unit},
{name: "empty unit", output: "Failed to reset failed state: Unit not loaded.", unit: ""},
} {
t.Run(test.name, func(t *testing.T) {
if rootlessQualResetFailureIsAlreadyUnloaded(test.output, test.unit) {
t.Fatal("unsafe reset failure was accepted")
}
})
}
}
func rootlessQualBestEffortStop(units ...string) {
for _, unit := range units {
if unit != "" {
_ = exec.Command("systemctl", "stop", unit).Run()
_ = exec.Command("systemctl", "reset-failed", unit).Run()
}
}
}
func rootlessQualWaitSocket(t *testing.T, path string) {
t.Helper()
deadline := time.Now().Add(45 * time.Second)
for time.Now().Before(deadline) {
if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSocket != 0 && info.Mode()&os.ModeSymlink == 0 {
return
}
time.Sleep(200 * time.Millisecond)
}
t.Fatalf("runtime socket did not appear: %s", path)
}
func rootlessQualUID(t *testing.T, user string) int {
t.Helper()
uidText := rootlessQualCommand(t, 10*time.Second, "id", "-u", user)
uid, err := strconv.Atoi(uidText)
if err != nil {
t.Fatalf("parse UID %q: %v", uidText, err)
}
return uid
}
func rootlessQualGID(t *testing.T, user string) int {
t.Helper()
gidText := rootlessQualCommand(t, 10*time.Second, "id", "-g", user)
gid, err := strconv.Atoi(gidText)
if err != nil {
t.Fatalf("parse GID %q: %v", gidText, err)
}
return gid
}
func rootlessQualWriteSubID(t *testing.T, path, line string) {
t.Helper()
raw, err := os.ReadFile(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
t.Fatal(err)
}
if strings.Contains(string(raw), strings.SplitN(line, ":", 2)[0]+":") {
return
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
t.Fatal(err)
}
defer f.Close()
if _, err := fmt.Fprintln(f, line); err != nil {
t.Fatal(err)
}
}
func rootlessQualCommand(t *testing.T, timeout time.Duration, name string, args ...string) string {
t.Helper()
out, err := rootlessQualCommandError(timeout, name, args...)
if err != nil {
t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, out)
}
return strings.TrimSpace(out)
}
func rootlessQualCommandError(timeout time.Duration, name string, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
out, err := exec.CommandContext(ctx, name, args...).CombinedOutput()
if ctx.Err() != nil {
return string(out), fmt.Errorf("command timed out: %w", ctx.Err())
}
return string(out), err
}
func rootlessQualReadFile(t *testing.T, path string) []byte {
t.Helper()
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
raw, err := io.ReadAll(io.LimitReader(f, 64<<20))
if err != nil {
t.Fatal(err)
}
return raw
}
func rootlessQualWriteJSON(t *testing.T, path string, value any) {
t.Helper()
raw, err := json.MarshalIndent(value, "", " ")
if err != nil {
t.Fatal(err)
}
tmp := path + ".tmp"
if err := os.WriteFile(tmp, append(raw, '\n'), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Rename(tmp, path); err != nil {
t.Fatal(err)
}
}
func rootlessQualValidateReceipt(receipt rootlessQualReceipt, wantRuns int) error {
if receipt.SchemaVersion != 1 || receipt.Kind != "pulse-secure-runtime-rootless-qualification" || receipt.Result != "passed" {
return errors.New("invalid top-level qualification identity")
}
if len(receipt.SourceCommit) != 40 || len(receipt.Runs) != wantRuns || receipt.Artifacts.QualificationTest.PathBasename != "dockeragent.test" || receipt.Artifacts.QualificationTest.VCSModified || len(receipt.SourceHashes) == 0 {
return errors.New("invalid source, artifact, or runtime binding")
}
seen := map[string]bool{}
for _, run := range receipt.Runs {
rt := run.Runtime
if seen[rt.Runtime] || (rt.Runtime != "docker" && rt.Runtime != "podman") || !rt.DaemonRootless || rt.CollectorUID <= 0 || rt.SocketUID != rt.CollectorUID || rt.SocketPath == "" || rt.DaemonID == "" || rt.SocketType != "unix" || rt.SocketSymlink {
return fmt.Errorf("invalid runtime identity for %q", rt.Runtime)
}
seen[rt.Runtime] = true
if rt.SocketMode != "0600" && rt.SocketMode != "0660" {
return fmt.Errorf("runtime %s socket mode is not owner-private", rt.Runtime)
}
if len(run.Scenarios) != len(rootlessQualScenarioOrder) {
return fmt.Errorf("runtime %s scenario count = %d", rt.Runtime, len(run.Scenarios))
}
for i, scenario := range run.Scenarios {
if scenario.Name != rootlessQualScenarioOrder[i] || scenario.Result != "passed" || scenario.StartedAt == "" || scenario.CompletedAt == "" || len(scenario.Evidence) == 0 {
return fmt.Errorf("runtime %s scenario %d is invalid", rt.Runtime, i)
}
reportExpected := scenario.Name != "dual_socket_ambiguity_refusal" && scenario.Name != "authority_isolation" && scenario.Name != "cleanup"
if reportExpected != (scenario.ReportSequence != nil && scenario.ReportStreamID != nil && *scenario.ReportStreamID != "") {
return fmt.Errorf("runtime %s scenario %s report binding is invalid", rt.Runtime, scenario.Name)
}
}
}
return nil
}
func TestRootlessQualificationReceiptContract(t *testing.T) {
now := time.Now().UTC().Format(time.RFC3339Nano)
sequence := uint64(1)
scenarios := make([]rootlessQualScenario, 0, len(rootlessQualScenarioOrder))
for _, name := range rootlessQualScenarioOrder {
scenario := rootlessQualScenario{Name: name, Result: "passed", StartedAt: now, CompletedAt: now, Evidence: map[string]any{"observed": true}}
if name != "dual_socket_ambiguity_refusal" && name != "authority_isolation" && name != "cleanup" {
scenario.ReportSequence = &sequence
stream := "stream"
scenario.ReportStreamID = &stream
}
scenarios = append(scenarios, scenario)
}
receipt := rootlessQualReceipt{
SchemaVersion: 1, Kind: "pulse-secure-runtime-rootless-qualification", Result: "passed", SourceCommit: strings.Repeat("a", 40),
SourceHashes: map[string]string{"source.go": strings.Repeat("b", 64)},
Artifacts: rootlessQualArtifacts{QualificationTest: rootlessQualArtifact{PathBasename: "dockeragent.test", VCSRevision: strings.Repeat("a", 40)}},
Runs: []rootlessQualRun{{Runtime: rootlessQualRuntime{Runtime: "docker", DaemonID: "daemon", CollectorUID: 1000, SocketUID: 1000, SocketMode: "0600", SocketPath: "/run/user/1000/docker.sock", SocketType: "unix", DaemonRootless: true}, Scenarios: scenarios}},
}
if err := rootlessQualValidateReceipt(receipt, 1); err != nil {
t.Fatal(err)
}
receipt.Runs[0].Scenarios[3].ReportSequence = nil
if err := rootlessQualValidateReceipt(receipt, 1); err == nil {
t.Fatal("receipt validator accepted a report-producing scenario without a sequence")
}
}
func TestRootlessQualificationBaselineUsesReportSemanticShape(t *testing.T) {
baseline := rootlessQualBaselineFromPSOutput(strings.Join([]string{
"/pulse-rootless-running|pulse-rootless-qualification:v1|running",
"pulse-rootless-exited|pulse-rootless-qualification:v1|exited",
}, "\n"))
report := agentsdocker.Report{Containers: []agentsdocker.Container{
{Name: "pulse-rootless-exited", Image: "pulse-rootless-qualification:v1", State: "exited"},
{Name: "pulse-rootless-running", Image: "pulse-rootless-qualification:v1", State: "running"},
}}
digest := rootlessQualDigestReport(report)
if baseline.Count != digest.Count || baseline.SemanticDigest != digest.SemanticDigest {
t.Fatalf("runtime baseline and report semantics diverged: baseline=%+v report=%+v", baseline, digest)
}
}
func TestRootlessQualificationStopAcceptsCollectedTransientUnit(t *testing.T) {
tempDir := t.TempDir()
logPath := filepath.Join(tempDir, "systemctl.log")
systemctlPath := filepath.Join(tempDir, "systemctl")
script := `#!/bin/sh
printf '%s\n' "$*" >>"$ROOTLESS_QUAL_SYSTEMCTL_LOG"
case "$1" in
stop) exit 0 ;;
reset-failed) echo 'Unit is not loaded.' >&2; exit 1 ;;
show) echo 'not-found'; exit 0 ;;
esac
exit 2
`
if err := os.WriteFile(systemctlPath, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("ROOTLESS_QUAL_SYSTEMCTL_LOG", logPath)
t.Setenv("PATH", tempDir+string(os.PathListSeparator)+os.Getenv("PATH"))
rootlessQualStopUnit(t, "pulse-rootless-docker")
logText := string(rootlessQualReadFile(t, logPath))
for _, command := range []string{
"stop pulse-rootless-docker",
"reset-failed pulse-rootless-docker",
"show --property=LoadState --value pulse-rootless-docker",
} {
if !strings.Contains(logText, command) {
t.Fatalf("systemctl lifecycle did not execute %q: %s", command, logText)
}
}
}
func TestRootlessQualificationRunningFixtureRestartsWithDaemon(t *testing.T) {
args := rootlessQualRunningFixtureArgs()
if !slices.Contains(args, "--restart=always") {
t.Fatalf("running qualification fixture lacks daemon-restart policy: %q", args)
}
}
func TestRootlessQualificationGoSchemaPassesPythonValidator(t *testing.T) {
commit := strings.Repeat("a", 40)
digest := strings.Repeat("b", 64)
started := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
receipt := rootlessQualReceipt{
SchemaVersion: 1, Kind: "pulse-secure-runtime-rootless-qualification", Result: "passed",
SourceCommit: commit, StartedAt: started.Format(time.RFC3339Nano),
CompletedAt: started.Add(2 * time.Minute).Format(time.RFC3339Nano),
SourceHashes: map[string]string{"internal/dockeragent/agent.go": digest, "scripts/install.sh": digest},
Artifacts: rootlessQualArtifacts{
QualificationTest: rootlessQualArtifact{PathBasename: "dockeragent.test", SHA256: digest, Package: "github.com/rcourtman/pulse-go-rewrite/scripts/installtests.test", GoVersion: "go1.25.0", VCSRevision: commit},
Collector: rootlessQualArtifact{PathBasename: "pulse-agent", SHA256: digest, Package: "github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent", GoVersion: "go1.25.0", VCSRevision: commit},
Helper: rootlessQualArtifact{PathBasename: "pulse-agent-helper", SHA256: digest, Package: "github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent-helper", GoVersion: "go1.25.0", VCSRevision: commit},
Installer: rootlessQualInstallerArtifact{PathBasename: "install.sh", SHA256: digest},
},
}
for index, runtimeKind := range []string{"docker", "podman"} {
receipt.Runs = append(receipt.Runs, rootlessQualValidatorFixtureRun(runtimeKind, 1000, index, started.Add(time.Duration(index)*time.Minute), digest))
}
path := filepath.Join(t.TempDir(), "receipt.json")
rootlessQualWriteJSON(t, path, receipt)
validator := repoFile("scripts", "release_control", "secure_runtime_rootless_attestation_v1.py")
program := `import importlib.util, pathlib, sys
spec=importlib.util.spec_from_file_location("validator", pathlib.Path(sys.argv[1]))
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
module.parse_receipt_bytes(pathlib.Path(sys.argv[2]).read_bytes())
`
cmd := exec.Command("python3", "-I", "-c", program, validator, path)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Go receipt schema failed the real Python validator: %v\n%s", err, output)
}
}
func rootlessQualValidatorFixtureRun(runtimeKind string, uid, index int, began time.Time, digest string) rootlessQualRun {
socketPath := filepath.Join("/run/user", strconv.Itoa(uid), "docker.sock")
if runtimeKind == "podman" {
socketPath = filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock")
}
daemonID := runtimeKind + "-daemon"
direct := func(pid int) map[string]any {
return map[string]any{
"collector_pid": pid, "service_pid": pid, "collection_path": "collector-owned-rootless-socket",
"inventory_complete": true, "inventory_count": 2, "semantic_sha256": digest,
"full_fields_present": true, "stats_present": true, "secondary_structure_sha256": digest,
"daemon_id": daemonID, "daemon_rootless": true, "socket_path": socketPath,
"socket_uid": uid, "socket_gid": uid, "socket_mode": "0600", "socket_type": "unix", "socket_symlink": false,
}
}
stream := func(value string) *string { return &value }
sequence := func(value uint64) *uint64 { return &value }
makeScenario := func(offset int, name string, streamID *string, seq *uint64, evidence map[string]any) rootlessQualScenario {
start := began.Add(time.Duration(offset) * time.Second)
return rootlessQualScenario{Name: name, Result: "passed", StartedAt: start.Format(time.RFC3339Nano), CompletedAt: start.Add(time.Second).Format(time.RFC3339Nano), ReportStreamID: streamID, ReportSequence: seq, Evidence: evidence}
}
fresh := direct(100 + index*10)
migration := direct(101 + index*10)
migration["legacy_profile"] = "root-command-capable"
migration["target_profile"] = "typed-helper-monitoring-only"
migration["authority_reduced"] = true
migration["legacy_collector_pid"] = 150 + index*10
restart := direct(102 + index*10)
restart["previous_collector_pid"] = 101 + index*10
restart["previous_report_stream_id"] = fmt.Sprintf("%s-migration-%d", runtimeKind, index)
daemon := direct(102 + index*10)
daemon["previous_daemon_pid"] = 200 + index*10
daemon["daemon_pid"] = 201 + index*10
daemon["previous_daemon_invocation_id"] = fmt.Sprintf("%s-old-invocation", runtimeKind)
daemon["daemon_invocation_id"] = fmt.Sprintf("%s-new-invocation", runtimeKind)
fallback := map[string]any{
"collector_pid": 102 + index*10, "collection_mode": "typed-helper-summary", "direct_runtime_available": false,
"helper_fallback": true, "inventory_complete": true, "inventory_count": 2, "rootful_baseline_inventory_count": 2,
"semantic_sha256": digest, "rootful_baseline_semantic_sha256": digest, "full_fields_present": false,
"stats_present": false, "secondary_structure_sha256": "", "container_actions_enabled": false,
"container_updates_enabled": false, "collector_restart_count": 0,
}
liveSockets := []map[string]any{
{"runtime": "docker", "path": filepath.Join("/run/user", strconv.Itoa(uid), "docker.sock"), "uid": uid, "gid": uid, "mode": "0600", "type": "unix", "symlink": false},
{"runtime": "podman", "path": filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock"), "uid": uid, "gid": uid, "mode": "0600", "type": "unix", "symlink": false},
}
ambiguity := map[string]any{
"protected_collector_pid": 102 + index*10, "live_sockets": liveSockets,
"probe_kind": "separate-unpinned-collector", "admission_refused": true, "fail_closed": true,
"daemon_probe_count": 0, "container_actions_enabled": false, "collector_restart_count": 0,
}
pin := direct(103 + index*10)
pin["previous_collector_pid"] = 102 + index*10
pin["previous_report_stream_id"] = fmt.Sprintf("%s-restart-%d", runtimeKind, index)
pin["pin_source"] = "root-owned-systemd-unit"
pin["pinned_socket_path"] = socketPath
pin["socket_absent_observed"] = true
pin["fallback_report_sequence"] = 1
pin["recovery_report_sequence"] = 2
pin["recovered_socket_path"] = socketPath
pin["selected_socket_path"] = socketPath
pin["recovered_socket_uid"] = uid
pin["recovered_socket_gid"] = uid
pin["recovered_socket_mode"] = "0600"
pin["recovered_socket_type"] = "unix"
pin["recovered_socket_symlink"] = false
pin["candidate_count"] = 1
pin["daemon_probe_count"] = 1
pin["collector_restart_count"] = 1
parity := map[string]any{
"collector_pid": 103 + index*10, "baseline_kind": "root-client-same-rootless-daemon",
"baseline_inventory_count": 2, "collector_inventory_count": 2,
"baseline_semantic_sha256": digest, "collector_semantic_sha256": digest,
"collector_full_fields_present": true, "collector_stats_present": true,
"collector_secondary_inventory_present": true,
}
authority := map[string]any{
"collector_pid": 103 + index*10, "collector_uid": uid, "effective_uid": uid, "effective_root": false,
"safe_profile_enabled": true, "commands_enabled": false, "privileged_helper_enabled": true,
"reduction_request_observed": true, "collector_command_transport_present": false,
"collector_command_session_present": false, "container_actions_enabled": false, "container_updates_enabled": false,
"rootful_socket_access": false, "helper_network_access": false,
}
run := rootlessQualRun{
Host: rootlessQualHost{MachineID: fmt.Sprintf("machine-%s", runtimeKind), Architecture: "amd64", Kernel: "Linux fixture", SystemdVersion: "systemd 255"},
Runtime: rootlessQualRuntime{Runtime: runtimeKind, RuntimeVersion: "1.0.0", DaemonID: daemonID, CollectorUID: uid, SocketPath: socketPath, SocketUID: uid, SocketGID: uid, SocketMode: "0600", SocketType: "unix", DaemonRootless: true},
}
run.Scenarios = []rootlessQualScenario{
makeScenario(0, "fresh_install", stream(fmt.Sprintf("%s-fresh-%d", runtimeKind, index)), sequence(1), fresh),
makeScenario(2, "legacy_migration", stream(fmt.Sprintf("%s-migration-%d", runtimeKind, index)), sequence(1), migration),
makeScenario(4, "collector_restart", stream(fmt.Sprintf("%s-restart-%d", runtimeKind, index)), sequence(1), restart),
makeScenario(6, "daemon_restart", stream(fmt.Sprintf("%s-restart-%d", runtimeKind, index)), sequence(2), daemon),
makeScenario(8, "socket_loss_helper_fallback", stream(fmt.Sprintf("%s-restart-%d", runtimeKind, index)), sequence(3), fallback),
makeScenario(10, "direct_recovery", stream(fmt.Sprintf("%s-restart-%d", runtimeKind, index)), sequence(4), direct(102+index*10)),
makeScenario(12, "dual_socket_ambiguity_refusal", nil, nil, ambiguity),
makeScenario(14, "exact_pin_recovery", stream(fmt.Sprintf("%s-update-%d", runtimeKind, index)), sequence(2), pin),
makeScenario(16, "telemetry_parity", stream(fmt.Sprintf("%s-update-%d", runtimeKind, index)), sequence(3), parity),
makeScenario(18, "authority_isolation", nil, nil, authority),
makeScenario(20, "cleanup", nil, nil, map[string]any{"runtime_stopped": true, "socket_absent": true, "fixtures_removed": true, "user_state_clean": true}),
}
return run
}
func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) {
if rootlessQualHasDefaultRoute("Iface Destination Gateway\neth0 00000000 0100007F") != true || rootlessQualHasDefaultRoute("Iface Destination Gateway\nlo 0000007F 00000000") {
t.Fatal("default-route refusal parser drifted")
}
raw, err := os.ReadFile(repoFile("scripts", "run-secure-runtime-rootless-qualification.sh"))
if err != nil {
t.Fatal(err)
}
script := string(raw)
for _, required := range []string{
`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\"`,
`openssl pkeyutl -sign -rawin -inkey`, `qualification output directory must have exact mode 0700`,
`install -d -m 0700 /opt/pulse/packet`,
`capture_qualification_container_diagnostics`, `journalctl --no-pager -n 2000`,
`302a300506032b6570032100`, `len(spki) != len(prefix) + 32`,
} {
if !strings.Contains(script, required) {
t.Fatalf("rootless qualification wrapper missing %q", required)
}
}
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"} {
if strings.Contains(script, forbidden) {
t.Fatalf("rootless qualification wrapper contains forbidden host-runtime mount marker %q", forbidden)
}
}
if strings.Contains(script, "--private-key") || strings.Contains(script, "update_seed=") {
t.Fatal("rootless wrapper must not put its ephemeral signing key in a process argument")
}
createIndex := strings.Index(script, `container_id="$(docker create`)
trackIndex := strings.Index(script, `CONTAINER_IDS+=("${container_id}")`)
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`)
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")
}
}
func TestRootlessQualificationDockerCommandUsesSupportedNetworkDriver(t *testing.T) {
d := rootlessQualDaemon{
rootlessUnit: "pulse-rootless-docker",
rootlessSock: "/run/user/996/docker.sock",
home: "/var/lib/pulse-rootless",
}
command := strings.Join(rootlessQualDockerStartArgs(d), "\x00")
if !strings.Contains(command, "DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns") || !strings.Contains(command, "DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=none") {
t.Fatalf("rootless Docker command does not select the supported contained drivers: %q", command)
}
if strings.Contains(command, "DOCKERD_ROOTLESS_ROOTLESSKIT_NET=host") {
t.Fatalf("rootless Docker command selected the unsupported host network driver: %q", command)
}
}
func runRootlessQualificationWithFakeDocker(t *testing.T, mode string) (string, string, error) {
t.Helper()
if _, err := exec.LookPath("python3"); err != nil {
t.Skip("python3 is required for the wrapper ownership regression")
}
temporary := t.TempDir()
fakeBin := filepath.Join(temporary, "bin")
outputDir := filepath.Join(temporary, "output")
if err := os.MkdirAll(fakeBin, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(outputDir, 0o700); err != nil {
t.Fatal(err)
}
writeExecutable := func(name, body string) {
t.Helper()
if err := os.WriteFile(filepath.Join(fakeBin, name), []byte(body), 0o755); err != nil {
t.Fatalf("write fake %s: %v", name, err)
}
}
writeExecutable("git", `#!/bin/sh
case " $* " in
*" branch --show-current "*) printf '%s\n' main ;;
*" status --porcelain "*) : ;;
*" rev-parse HEAD "*) printf '%040d\n' 0 | tr 0 a ;;
*) printf 'unexpected fake git invocation: %s\n' "$*" >&2; exit 64 ;;
esac
`)
writeExecutable("go", `#!/bin/sh
if [ "${1:-}" = env ] && [ "${2:-}" = GOARCH ]; then
printf '%s\n' amd64
exit 0
fi
output=''
while [ "$#" -gt 0 ]; do
if [ "$1" = -o ]; then
output="$2"
break
fi
shift
done
[ -n "$output" ] || exit 65
printf '%s\n' fake-go-artifact >"$output"
chmod 0700 "$output"
`)
writeExecutable("openssl", `#!/bin/sh
command_name="${1:-}"
shift || true
case "$command_name" in
rand)
printf '%032d\n' 0 | tr 0 a
;;
genpkey)
while [ "$#" -gt 0 ]; do
if [ "$1" = -out ]; then printf '%s\n' fake-private-key >"$2"; exit 0; fi
shift
done
exit 66
;;
pkey)
while [ "$#" -gt 0 ]; do
if [ "$1" = -out ]; then
python3 - "$2" <<'PY'
from pathlib import Path
import sys
Path(sys.argv[1]).write_bytes(bytes.fromhex("302a300506032b6570032100") + bytes(32))
PY
exit 0
fi
shift
done
exit 67
;;
pkeyutl)
printf '%s' fake-signature
;;
base64)
python3 -c 'import base64,sys; sys.stdout.write(base64.b64encode(sys.stdin.buffer.read()).decode())'
;;
*)
exit 68
;;
esac
`)
dockerLog := filepath.Join(temporary, "docker.log")
dockerState := filepath.Join(temporary, "docker.state")
dockerLabel := filepath.Join(temporary, "docker.label")
writeExecutable("docker", `#!/bin/sh
printf '%s\n' "$*" >>"$PULSE_FAKE_DOCKER_LOG"
container_id='cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
case "${1:-}" in
info) exit 0 ;;
build) printf '%s\n' fake-build ; exit 0 ;;
image) printf '%s\n' '[{}]' ; exit 0 ;;
create)
if [ "$PULSE_FAKE_DOCKER_MODE" = name-conflict ]; then
printf '%s\n' 'Conflict. The container name is already in use.' >&2
exit 125
fi
previous=''
for argument in "$@"; do
if [ "$previous" = --label ]; then
printf '%s\n' "${argument#*=}" >"$PULSE_FAKE_DOCKER_LABEL"
fi
previous="$argument"
done
: >"$PULSE_FAKE_DOCKER_STATE"
printf '%s\n' "$container_id"
;;
cp)
case "${2:-}" in
*:*) printf '%s\n' '{}' >"$3" ;;
esac
;;
start) exit 0 ;;
exec)
case " $* " in
*" ip route "*) exit 1 ;;
*) exit 0 ;;
esac
;;
inspect)
case " $* " in
*".Mounts"*) exit 0 ;;
*".Config.Labels"*)
[ "$PULSE_FAKE_DOCKER_MODE" != inspect-failure ] || exit 1
cat "$PULSE_FAKE_DOCKER_LABEL"
;;
*) exit 70 ;;
esac
;;
rm)
[ "$PULSE_FAKE_DOCKER_MODE" != rm-failure ] || exit 1
rm -f "$PULSE_FAKE_DOCKER_STATE"
;;
ps)
[ ! -f "$PULSE_FAKE_DOCKER_STATE" ] || printf '%s\n' "$container_id"
;;
logs) exit 0 ;;
*) printf 'unexpected fake docker invocation: %s\n' "$*" >&2; exit 69 ;;
esac
`)
commit := strings.Repeat("a", 40)
command := exec.Command("bash", repoFile("scripts", "run-secure-runtime-rootless-qualification.sh"))
command.Env = append(os.Environ(),
"PATH="+fakeBin+string(os.PathListSeparator)+os.Getenv("PATH"),
"PULSE_FAKE_DOCKER_LOG="+dockerLog,
"PULSE_FAKE_DOCKER_STATE="+dockerState,
"PULSE_FAKE_DOCKER_LABEL="+dockerLabel,
"PULSE_FAKE_DOCKER_MODE="+mode,
"PULSE_ROOTLESS_UBUNTU_IMAGE=ubuntu@sha256:"+strings.Repeat("b", 64),
"PULSE_ROOTLESS_QUALIFICATION_OUTPUT_DIR="+outputDir,
"PULSE_ROOTLESS_QUALIFICATION_CONFIRM=I_HAVE_VERIFIED_THESE_ARE_DISPOSABLE_ROOTLESS_SYSTEMD_CONTAINERS_COMMIT_"+commit,
)
output, err := command.CombinedOutput()
logBytes, readErr := os.ReadFile(dockerLog)
if readErr != nil {
t.Fatal(readErr)
}
return string(output), string(logBytes), err
}
func TestRootlessQualificationNameConflictDoesNotRemoveContainer(t *testing.T) {
output, logText, err := runRootlessQualificationWithFakeDocker(t, "name-conflict")
if err == nil {
t.Fatalf("wrapper unexpectedly succeeded after Docker name conflict: %s", output)
}
if !strings.Contains(logText, "create --name pulse-rootless-qual-docker-") {
t.Fatalf("fake Docker did not reach the name-conflict create; docker log=%s wrapper output=%s", logText, output)
}
for _, line := range strings.Split(logText, "\n") {
if strings.HasPrefix(line, "rm ") {
t.Fatalf("name-conflict cleanup attempted to remove an unowned container: %s", logText)
}
}
}
func TestRootlessQualificationCleanupFailureCannotEmitPassingPacket(t *testing.T) {
for _, mode := range []string{"inspect-failure", "rm-failure"} {
t.Run(mode, func(t *testing.T) {
output, logText, err := runRootlessQualificationWithFakeDocker(t, mode)
if err == nil {
t.Fatalf("wrapper unexpectedly succeeded after %s: %s", mode, output)
}
if strings.Contains(output, "Rootless qualification passed:") {
t.Fatalf("wrapper emitted a passing packet after %s: %s", mode, output)
}
if !strings.Contains(logText, ".Config.Labels") {
t.Fatalf("wrapper did not reach strict ownership verification after %s: %s", mode, logText)
}
if mode == "rm-failure" && !strings.Contains(logText, "rm -f cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") {
t.Fatalf("wrapper did not exercise strict removal failure: %s", logText)
}
})
}
}