diff --git a/.github/workflows/qualify-secure-runtime-release.yml b/.github/workflows/qualify-secure-runtime-release.yml index d0fcc1a2dd..348c337f31 100644 --- a/.github/workflows/qualify-secure-runtime-release.yml +++ b/.github/workflows/qualify-secure-runtime-release.yml @@ -224,7 +224,7 @@ jobs: COPY --from=dind /usr/local/bin/ /usr/local/bin/ COPY pulse-qualification-docker.service /etc/systemd/system/pulse-qualification-docker.service RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - ca-certificates curl dbus systemd systemd-sysv util-linux && \ + ca-certificates curl dbus systemd systemd-sysv util-linux iproute2 && \ apt-get clean && \ systemctl mask dev-hugepages.mount sys-fs-fuse-connections.mount systemd-remount-fs.service docker.service docker.socket && \ systemctl enable pulse-qualification-docker.service @@ -269,7 +269,19 @@ jobs: sleep 2 done docker exec "${container}" sh -c \ - 'for command in curl docker dockerd id nsenter runuser systemctl; do command -v "$command" >/dev/null || exit 1; done' + 'for command in curl docker dockerd id ip nsenter runuser systemctl; do command -v "$command" >/dev/null || exit 1; done' + # The immutable RC test needs a reachable non-loopback host-interface + # canary to prove that the helper's private network cannot reach it. + # Both veth ends stay inside this --network none namespace; neither + # endpoint is attached to the runner or an egress bridge. + docker exec "${container}" sh -ceu ' + ip link add pulse-can0 type veth peer name pulse-can1 + ip address add 192.0.2.1/32 dev pulse-can0 + ip link set pulse-can1 up + ip link set pulse-can0 up + test -z "$(ip -4 route show default)" + ip -4 address show dev pulse-can0 | grep -F "192.0.2.1/32" + ' docker exec "${container}" systemctl start pulse-qualification-docker.service for attempt in $(seq 1 60); do if docker exec "${container}" docker info >/dev/null 2>&1; then diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 2dc44f5b9f..59716371ea 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1239,6 +1239,14 @@ artifact-selection behaviour. privileged systemd container must mount that exact snapshot read-only and must never execute directly from the download directory. The post-run attester must consume the same snapshot paths. + The outer systemd qualification container must retain `--network none`: + it may create a non-loopback host-interface canary only as a veth pair + wholly inside that container's network namespace, with no runner bridge, + egress attachment, or default IPv4 route. The immutable RC lab uses this + reachable local canary to distinguish host-network access from the typed + helper's private network isolation. Canary setup and the no-default-route + check must fail qualification before that lab if the boundary is absent; + they must not skip the lab or change the release packet under test. PVE jobs must consume their runner users' persistent local Go and npm caches directly; disposable-runner Actions cache restore/save phases must remain disabled because archiving those same caches adds network work after the diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 05b87acc70..03ecfb81e0 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -2847,12 +2847,15 @@ func TestSecureRuntimeQualificationPacketIsHostedAndReleaseBound(t *testing.T) { `git remote set-url origin https://github.com/rcourtman/Pulse.git`, "ca-certificates curl dbus systemd systemd-sysv util-linux", `docker:27.5.1-dind@sha256:f649ef046008ca7f926a2571c32b0ac22e5c59eb61b959617f9acc2a4c638cf5`, - `for command in curl docker dockerd id nsenter runuser systemctl`, + `for command in curl docker dockerd id ip nsenter runuser systemctl`, `--host=unix:///var/run/docker.sock`, `--storage-driver=vfs`, `--bridge=none`, `--iptables=false`, `--network none`, + `ip link add pulse-can0 type veth peer name pulse-can1`, + `ip address add 192.0.2.1/32 dev pulse-can0`, + `test -z "$(ip -4 route show default)"`, `pulse-secure-runtime-fixture:v7`, "--verify-release-packet-only", "--verified-packet-dir", @@ -2870,6 +2873,16 @@ func TestSecureRuntimeQualificationPacketIsHostedAndReleaseBound(t *testing.T) { t.Fatalf("post-publication secure-runtime qualification missing %q", required) } } + canaryIndex := strings.Index(qualificationWorkflow, `ip link add pulse-can0 type veth peer name pulse-can1`) + labIndex := strings.Index(qualificationWorkflow, `--env PULSE_SECURE_RUNTIME_SYSTEMD_LAB=1`) + if canaryIndex <= privilegedExecutionIndex || labIndex <= canaryIndex { + t.Fatal("the isolated host-interface canary must be configured before running the immutable RC lab") + } + if !strings.Contains(qualificationWorkflow, `--privileged \ + --network none \ + --cgroupns=host`) { + t.Fatal("the outer systemd lab must retain its no-network namespace") + } for _, forbidden := range []string{ `/var/run/docker.sock:/var/run/docker.sock`, `--host=tcp://`,