diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index e7292a5b5..67aa4f057 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -7324,6 +7324,10 @@ 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. +Before each disposable host's first systemd boot, the wrapper installs a fresh +valid machine ID into that stopped container. The combined receipt validator +rejects a Docker and Podman pair that reports the same machine identity; a +shared image fallback identity cannot stand in for two independent hosts. The in-container control-plane fixture must implement the same authenticated collector-uninstall response contract as the production lifecycle client. A teardown succeeds only after the fixture records the exact registered binding diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index e2a21d7a2..adbc8f6a4 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -259,6 +259,9 @@ the immutable schema-v7 systemd packet. The opt-in `scripts/run-secure-runtime-rootless-qualification.sh` wrapper creates fresh, network-isolated Ubuntu/systemd hosts for real rootless Docker and Podman, never mounts a host daemon socket, and records each host identity separately. +It installs a fresh valid machine ID into each stopped container before first +systemd boot, and the combined validator rejects duplicate Docker and Podman +machine identities rather than accepting a shared image-derived fallback. 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. The image diff --git a/scripts/installtests/secure_runtime_rootless_qualification_test.go b/scripts/installtests/secure_runtime_rootless_qualification_test.go index 30ae4a613..df1baf771 100644 --- a/scripts/installtests/secure_runtime_rootless_qualification_test.go +++ b/scripts/installtests/secure_runtime_rootless_qualification_test.go @@ -1856,6 +1856,7 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) { `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`, + `machine_id="$(openssl rand -hex 16)"`, `docker cp "${machine_id_file}" "${container_id}:/etc/machine-id"`, } { if !strings.Contains(script, required) { t.Fatalf("rootless qualification wrapper missing %q", required) @@ -1882,6 +1883,11 @@ func TestRootlessQualificationGuardAndWrapperInvariants(t *testing.T) { if packetDirectoryIndex < 0 || packetCopyIndex < 0 || packetCopyIndex < packetDirectoryIndex { t.Fatal("rootless wrapper must create the private packet destination in the image before artifact injection") } + machineIDCopyIndex := strings.Index(script, `docker cp "${machine_id_file}" "${container_id}:/etc/machine-id"`) + containerStartIndex := strings.Index(script, `docker start "${container_id}"`) + if machineIDCopyIndex < 0 || containerStartIndex < 0 || machineIDCopyIndex > packetCopyIndex || machineIDCopyIndex > containerStartIndex { + t.Fatal("rootless wrapper must install a per-host machine ID before packet injection and first systemd boot") + } } func TestRootlessQualificationDockerCommandUsesSupportedNetworkDriver(t *testing.T) { diff --git a/scripts/run-secure-runtime-rootless-qualification.sh b/scripts/run-secure-runtime-rootless-qualification.sh index 565287c59..2a299206c 100755 --- a/scripts/run-secure-runtime-rootless-qualification.sh +++ b/scripts/run-secure-runtime-rootless-qualification.sh @@ -234,6 +234,16 @@ run_runtime() { local container_name="pulse-rootless-qual-${runtime_name}-${SOURCE_COMMIT:0:8}-$$" local container_id local local_receipt="${OUTPUT_DIR}/${runtime_name}-receipt.json" + local machine_id_file="${PACKET_DIR}/.machine-id-${runtime_name}" + local machine_id + + machine_id="$(openssl rand -hex 16)" + if [[ ! "${machine_id}" =~ ^[0-9a-f]{32}$ || "${machine_id}" == "00000000000000000000000000000000" ]]; then + echo "ERROR: unable to generate a valid machine ID for ${runtime_name} qualification" >&2 + return 1 + fi + printf '%s\n' "${machine_id}" >"${machine_id_file}" + chmod 0444 "${machine_id_file}" container_id="$(docker create --name "${container_name}" --hostname "pulse-rootless-${runtime_name}" \ --label "${CONTAINER_RUN_LABEL}=${CONTAINER_RUN_NONCE}" \ @@ -241,6 +251,8 @@ run_runtime() { --tmpfs /run:rw,nosuid,nodev,mode=755 --tmpfs /run/lock:rw,nosuid,nodev,mode=755 \ "${IMAGE_TAG}")" CONTAINER_IDS+=("${container_id}") + docker cp "${machine_id_file}" "${container_id}:/etc/machine-id" + rm -f -- "${machine_id_file}" docker cp "${PACKET_DIR}/." "${container_id}:/opt/pulse/packet" docker start "${container_id}" >/dev/null