Give RC qualification a local host-interface canary (#2328)

Split out of #2323. The v6.4.5-rc.4 installed-acceptance run
(36547091547) failed before the lab because the no-network qualification
container had no non-loopback IPv4 canary. This carries only the
qualification and demo-resolver workflow fixes, their
deployment-installability contract text and the workflow tests, so
promotion isn't waiting on #2323's conflicting UI changes.
This commit is contained in:
rcourtman
2026-09-29 17:01:13 +01:00
committed by GitHub
4 changed files with 70 additions and 10 deletions
@@ -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
+7 -7
View File
@@ -74,6 +74,13 @@ jobs:
mock_update_interval: ${{ steps.runtime_profile.outputs.mock_update_interval }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
fetch-tags: true
- name: Resolve target tag and demo environment
id: target
env:
@@ -137,13 +144,6 @@ jobs:
echo "environment_name=$ENVIRONMENT_NAME" >> "$GITHUB_OUTPUT"
echo "Resolved demo deployment: tag=${TAG}, target=${TARGET}, environment=${ENVIRONMENT_NAME}"
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
fetch-tags: true
- name: Validate governed release line for selected tag
env:
WORKFLOW_OUTPUT_1: ${{ steps.target.outputs.tag }}
@@ -1296,6 +1296,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
@@ -3831,6 +3839,10 @@ bypass the preflight. That dry run must call `update-demo-server.yml` in
verification-only mode against the latest stable release. It must prove
Tailscale, SSH host identity, runtime version, frontend parity, public health,
and browser smoke without changing the host.
The reusable demo workflow's resolver must check out the repository before it
calls `scripts/write_github_output.py` to emit the selected tag and target.
This checkout is credential-free; a verification-only dry run must not fail
before its demo checks merely because the output helper is absent.
That same release-validation boundary also owns draft-versus-published asset
state. Every normal `.github/workflows/create-release.yml` cut validates the
uploaded packet while the release is still a draft and must pass `draft=true`
@@ -2392,6 +2392,29 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) {
}
}
func TestUpdateDemoResolverChecksOutOutputHelperBeforeUse(t *testing.T) {
workflowBytes, err := os.ReadFile(repoFile(".github", "workflows", "update-demo-server.yml"))
if err != nil {
t.Fatalf("read update-demo-server workflow: %v", err)
}
workflow := string(workflowBytes)
resolveStart := strings.Index(workflow, "\n resolve:\n")
updateStart := strings.Index(workflow, "\n update-demo:\n")
if resolveStart < 0 || updateStart <= resolveStart {
t.Fatal("update-demo-server workflow must retain separate resolve and update-demo jobs")
}
resolve := workflow[resolveStart:updateStart]
checkout := strings.Index(resolve, "- name: Checkout repository")
target := strings.Index(resolve, "- name: Resolve target tag and demo environment")
helper := strings.Index(resolve, "python3 scripts/write_github_output.py tag \"$TAG\"")
if checkout < 0 || target <= checkout || helper <= target {
t.Fatal("demo resolver must check out its source before using scripts/write_github_output.py")
}
if !strings.Contains(resolve[checkout:target], "persist-credentials: false") {
t.Fatal("demo resolver checkout must not persist GitHub credentials")
}
}
func TestDemoMutationAndRecoverySharePhysicalTargetLock(t *testing.T) {
updateBytes, err := os.ReadFile(repoFile(".github", "workflows", "update-demo-server.yml"))
if err != nil {
@@ -2824,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",
@@ -2847,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://`,