mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
79d87a4a2c
The secure-runtime qualification required the candidate commit to be an ancestor of origin/main. Under the release train a patch candidate is cut from release/vX.Y and is not on main at all, so v6.4.3-rc.1's qualification failed at "Verify detached release source" before running any check, and the steward correctly refused to treat the candidate as stable-capable. Resolve the branch the control plane maps the version to, fetch that, check ancestry against it, and hand the same ref to the attestation so the receipt records the line it was verified against. Refs #1753 Contract-Neutral: release qualification checks ancestry against the mapped release branch; no product or contract change
372 lines
19 KiB
YAML
372 lines
19 KiB
YAML
name: Qualify Secure Runtime Release Candidate
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Immutable vX.Y.Z-rc.N release tag to qualify'
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
attestations: read
|
|
contents: read
|
|
|
|
jobs:
|
|
qualify-secure-runtime-release:
|
|
name: Qualify Immutable RC on Disposable systemd
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 45
|
|
steps:
|
|
- name: Checkout release qualification control
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Resolve immutable RC identity
|
|
id: release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
INPUT_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
tag="${INPUT_TAG:-}"
|
|
[[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[1-9][0-9]*$ ]] || {
|
|
echo "::error::Secure-runtime release qualification requires an exact vX.Y.Z-rc.N tag."
|
|
exit 1
|
|
}
|
|
release_json="$RUNNER_TEMP/secure-runtime-release.json"
|
|
for attempt in $(seq 1 30); do
|
|
gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" > "${release_json}"
|
|
if jq -e --arg tag "${tag}" '.tag_name == $tag and .draft == false and .prerelease == true and .immutable == true' "${release_json}" >/dev/null; then
|
|
break
|
|
fi
|
|
if [[ "${attempt}" == 30 ]]; then
|
|
jq -c '{tag_name, draft, prerelease, immutable}' "${release_json}" >&2
|
|
echo "::error::Release did not reach immutable prerelease state."
|
|
exit 1
|
|
fi
|
|
sleep 10
|
|
done
|
|
release_id="$(jq -er '.id | select(type == "number" and . > 0)' "${release_json}")"
|
|
python3 scripts/write_github_output.py tag "${tag}"
|
|
echo "release_id=${release_id}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout exact release tag
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 0
|
|
ref: ${{ steps.release.outputs.tag }}
|
|
|
|
- name: Verify detached release source
|
|
id: source
|
|
env:
|
|
TAG: ${{ steps.release.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
# The release train cuts patch candidates from release/vX.Y, so the
|
|
# candidate is reachable from the branch the control plane maps its
|
|
# version to, not necessarily from main (v6.4.3-rc.1, 2026-09-02).
|
|
required_branch="$(python3 scripts/release_control/control_plane.py \
|
|
--branch-for-version "${TAG#v}")"
|
|
[[ "${required_branch}" =~ ^(main|release/v[0-9]+\.[0-9]+)$ ]] || {
|
|
echo "::error::Control plane mapped ${TAG} to an unexpected branch: ${required_branch}"
|
|
exit 1
|
|
}
|
|
git fetch --no-tags origin \
|
|
"refs/heads/${required_branch}:refs/remotes/origin/${required_branch}"
|
|
commit="$(git rev-parse "${TAG}^{commit}")"
|
|
test "${GITHUB_REF}" = "refs/tags/${TAG}"
|
|
test "${GITHUB_SHA}" = "${commit}"
|
|
test "$(git branch --show-current)" = ""
|
|
test -z "$(git status --short --untracked-files=all)"
|
|
git merge-base --is-ancestor "${commit}" "origin/${required_branch}"
|
|
python3 scripts/write_github_output.py commit "${commit}"
|
|
python3 scripts/write_github_output.py line_ref "origin/${required_branch}"
|
|
|
|
- name: Set up Go
|
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
|
with:
|
|
go-version-file: go.mod
|
|
cache: true
|
|
|
|
- name: Download exact immutable qualification packet
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ steps.release.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p \
|
|
"$RUNNER_TEMP/secure-runtime-downloads" \
|
|
"$RUNNER_TEMP/secure-runtime-evidence" \
|
|
"$RUNNER_TEMP/secure-runtime-harness" \
|
|
"$RUNNER_TEMP/secure-runtime-preexecution"
|
|
for asset in \
|
|
checksums.txt \
|
|
release-build-provenance.sigstore.json \
|
|
secure-runtime-compiler-provenance.sigstore.json \
|
|
secure-runtime-build-contract-v1.json \
|
|
pulse-secure-runtime-collector-v1-linux-amd64 \
|
|
pulse-secure-runtime-collector-v1-linux-amd64.sig \
|
|
pulse-secure-runtime-collector-v2-linux-amd64 \
|
|
pulse-secure-runtime-collector-v2-linux-amd64.sig \
|
|
pulse-secure-runtime-collector-v3-linux-amd64 \
|
|
pulse-secure-runtime-collector-v3-linux-amd64.sig \
|
|
pulse-agent-linux-amd64 \
|
|
pulse-agent-linux-amd64.sig \
|
|
pulse-agent-helper-linux-amd64 \
|
|
pulse-agent-runner-linux-amd64; do
|
|
gh release download "${TAG}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--dir "$RUNNER_TEMP/secure-runtime-downloads" \
|
|
--pattern "${asset}"
|
|
done
|
|
|
|
- name: Pre-authenticate exact qualification packet
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
|
QUALIFIED_COMMIT: ${{ steps.source.outputs.commit }}
|
|
LINE_REF: ${{ steps.source.outputs.line_ref }}
|
|
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
|
TAG: ${{ steps.release.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
download_dir="$RUNNER_TEMP/secure-runtime-downloads"
|
|
verification_dir="$RUNNER_TEMP/secure-runtime-preexecution"
|
|
test -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"
|
|
update_fingerprint="$(
|
|
go run ./scripts/release_update_key.go fingerprint \
|
|
--public-key "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"
|
|
)"
|
|
python3 scripts/release_control/secure_runtime_attestation_v7.py \
|
|
--verify-release-packet-only \
|
|
--checkout "${GITHUB_WORKSPACE}" \
|
|
--commit "${QUALIFIED_COMMIT}" \
|
|
--main-ref "${LINE_REF}" \
|
|
--collector-v1 "${download_dir}/pulse-secure-runtime-collector-v1-linux-amd64" \
|
|
--collector-v2 "${download_dir}/pulse-secure-runtime-collector-v2-linux-amd64" \
|
|
--collector-v3 "${download_dir}/pulse-secure-runtime-collector-v3-linux-amd64" \
|
|
--collector-v4 "${download_dir}/pulse-agent-linux-amd64" \
|
|
--helper "${download_dir}/pulse-agent-helper-linux-amd64" \
|
|
--runner "${download_dir}/pulse-agent-runner-linux-amd64" \
|
|
--collector-v1-signature "${download_dir}/pulse-secure-runtime-collector-v1-linux-amd64.sig" \
|
|
--collector-v2-signature "${download_dir}/pulse-secure-runtime-collector-v2-linux-amd64.sig" \
|
|
--collector-v3-signature "${download_dir}/pulse-secure-runtime-collector-v3-linux-amd64.sig" \
|
|
--collector-v4-signature "${download_dir}/pulse-agent-linux-amd64.sig" \
|
|
--release-candidate-tag "${TAG}" \
|
|
--release-repository "${GITHUB_REPOSITORY}" \
|
|
--release-id "${RELEASE_ID}" \
|
|
--release-checksums "${download_dir}/checksums.txt" \
|
|
--release-assembly-provenance "${download_dir}/release-build-provenance.sigstore.json" \
|
|
--release-compiler-provenance "${download_dir}/secure-runtime-compiler-provenance.sigstore.json" \
|
|
--release-build-contract "${download_dir}/secure-runtime-build-contract-v1.json" \
|
|
--expected-release-update-key-fingerprint "${update_fingerprint}" \
|
|
--expected-architecture amd64 \
|
|
--verified-packet-dir "$RUNNER_TEMP/secure-runtime-verified" \
|
|
--output "${verification_dir}/preexecution-verification.json"
|
|
|
|
- name: Build exact-source qualification harness
|
|
run: |
|
|
set -euo pipefail
|
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
|
go test -c \
|
|
-o "$RUNNER_TEMP/secure-runtime-harness/installtests-linux-amd64.test" \
|
|
./scripts/installtests
|
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
|
go build -trimpath \
|
|
-o "$RUNNER_TEMP/secure-runtime-harness/docker-fixture-linux-amd64" \
|
|
./scripts/installtests/testdata/secure_runtime_docker_fixture.go
|
|
|
|
- name: Build disposable systemd host image
|
|
run: |
|
|
set -euo pipefail
|
|
context="$RUNNER_TEMP/secure-runtime-systemd-image"
|
|
mkdir -p "${context}"
|
|
tee "${context}/pulse-qualification-docker.service" >/dev/null <<'UNIT'
|
|
[Unit]
|
|
Description=Pulse secure-runtime qualification Docker daemon
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=notify
|
|
ExecStart=/usr/local/bin/dockerd --host=unix:///var/run/docker.sock --group=root --data-root=/var/lib/pulse-qualification-docker --exec-root=/run/pulse-qualification-docker --pidfile=/run/pulse-qualification-docker.pid --storage-driver=vfs --bridge=none --iptables=false --ip6tables=false --ip-forward=false --ip-masq=false --userland-proxy=false
|
|
ExecStartPost=/bin/chown root:root /var/run/docker.sock
|
|
ExecStartPost=/bin/chmod 0660 /var/run/docker.sock
|
|
Restart=on-failure
|
|
RestartSec=1
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
tee "${context}/Dockerfile" >/dev/null <<'DOCKERFILE'
|
|
FROM docker:27.5.1-dind@sha256:f649ef046008ca7f926a2571c32b0ac22e5c59eb61b959617f9acc2a4c638cf5 AS dind
|
|
FROM ubuntu:24.04@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517
|
|
ENV container=docker
|
|
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 && \
|
|
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
|
|
STOPSIGNAL SIGRTMIN+3
|
|
CMD ["/sbin/init"]
|
|
DOCKERFILE
|
|
docker build \
|
|
--tag pulse-secure-runtime-systemd:ubuntu-24.04 \
|
|
--file "${context}/Dockerfile" \
|
|
"${context}"
|
|
|
|
- name: Run schema-v7 systemd and rootful Docker qualification
|
|
id: lab
|
|
env:
|
|
TAG: ${{ steps.release.outputs.tag }}
|
|
run: |
|
|
set -euo pipefail
|
|
container="pulse-secure-runtime-qualification-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
echo "container=${container}" >> "$GITHUB_OUTPUT"
|
|
docker run \
|
|
--detach \
|
|
--name "${container}" \
|
|
--privileged \
|
|
--network none \
|
|
--cgroupns=host \
|
|
--tmpfs /run \
|
|
--tmpfs /run/lock \
|
|
--volume /sys/fs/cgroup:/sys/fs/cgroup:rw \
|
|
--volume "${GITHUB_WORKSPACE}:/workspace:ro" \
|
|
--volume "$RUNNER_TEMP/secure-runtime-verified:/release:ro" \
|
|
--volume "$RUNNER_TEMP/secure-runtime-harness:/harness:ro" \
|
|
--volume "$RUNNER_TEMP/secure-runtime-evidence:/evidence:rw" \
|
|
pulse-secure-runtime-systemd:ubuntu-24.04
|
|
for attempt in $(seq 1 30); do
|
|
if docker exec "${container}" systemctl show-environment >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [[ "${attempt}" == 30 ]]; then
|
|
docker logs "${container}"
|
|
exit 1
|
|
fi
|
|
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'
|
|
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
|
|
break
|
|
fi
|
|
if [[ "${attempt}" == 60 ]]; then
|
|
docker exec "${container}" systemctl status pulse-qualification-docker.service --no-pager || true
|
|
docker logs "${container}"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
docker exec "${container}" sh -ceu '
|
|
test -S /var/run/docker.sock
|
|
test "$(stat -c %U:%G:%a /var/run/docker.sock)" = "root:root:660"
|
|
daemon_pid="$(systemctl show pulse-qualification-docker.service --property MainPID --value)"
|
|
test "${daemon_pid}" -gt 0
|
|
case "$(tr "\000" " " < "/proc/${daemon_pid}/cmdline")" in *tcp://*) exit 1;; esac
|
|
rootfs="$(mktemp -d)"
|
|
cp /harness/docker-fixture-linux-amd64 "${rootfs}/fixture"
|
|
chmod 0555 "${rootfs}/fixture"
|
|
tar -C "${rootfs}" -cf /tmp/pulse-secure-runtime-rootfs.tar .
|
|
docker import --change '\''ENTRYPOINT ["/fixture"]'\'' /tmp/pulse-secure-runtime-rootfs.tar pulse-secure-runtime-fixture:v7 >/dev/null
|
|
docker run --detach --name pulse-v7-running --network none pulse-secure-runtime-fixture:v7 >/dev/null
|
|
docker run --name pulse-v7-exited --network none pulse-secure-runtime-fixture:v7 --exit >/dev/null
|
|
test "$(docker inspect --format "{{.State.Status}}" pulse-v7-running)" = "running"
|
|
test "$(docker inspect --format "{{.State.Status}}" pulse-v7-exited)" = "exited"
|
|
'
|
|
docker exec "${container}" sh -c \
|
|
'printf "%s\n" "PULSE_SECURE_RUNTIME_SYSTEMD_LAB=disposable-v1" > /etc/pulse-secure-runtime-lab'
|
|
start_epoch="$(date +%s)"
|
|
docker exec \
|
|
--env PULSE_SECURE_RUNTIME_SYSTEMD_LAB=1 \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V1=/release/pulse-secure-runtime-collector-v1-linux-amd64 \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V1_SIGNATURE=/release/pulse-secure-runtime-collector-v1-linux-amd64.sig \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V2=/release/pulse-secure-runtime-collector-v2-linux-amd64 \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V2_SIGNATURE=/release/pulse-secure-runtime-collector-v2-linux-amd64.sig \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V3=/release/pulse-secure-runtime-collector-v3-linux-amd64 \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V3_SIGNATURE=/release/pulse-secure-runtime-collector-v3-linux-amd64.sig \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V4=/release/pulse-agent-linux-amd64 \
|
|
--env PULSE_SECURE_RUNTIME_COLLECTOR_V4_SIGNATURE=/release/pulse-agent-linux-amd64.sig \
|
|
--env PULSE_SECURE_RUNTIME_HELPER=/release/pulse-agent-helper-linux-amd64 \
|
|
--env PULSE_SECURE_RUNTIME_RUNNER=/release/pulse-agent-runner-linux-amd64 \
|
|
--env PULSE_SECURE_RUNTIME_RECEIPT=/evidence/receipt.json \
|
|
--env "PULSE_SECURE_RUNTIME_RECEIPT_RECORD_PATH=docs/release-control/v6/internal/records/secure-agent-runtime-systemd-receipt-v7-${TAG}.json" \
|
|
--env PULSE_SECURE_RUNTIME_TRANSCRIPT=/evidence/transcript.jsonl \
|
|
--env "PULSE_SECURE_RUNTIME_TRANSCRIPT_RECORD_PATH=docs/release-control/v6/internal/records/secure-agent-runtime-systemd-transcript-v7-${TAG}.jsonl" \
|
|
"${container}" \
|
|
sh -c 'cd /workspace/scripts/installtests && exec /harness/installtests-linux-amd64.test -test.run "^TestSecureRuntimeSystemdDockerV7Lab$" -test.count=1 -test.v'
|
|
elapsed="$(( $(date +%s) - start_epoch ))"
|
|
if [[ "${elapsed}" -lt 1 ]]; then elapsed=1; fi
|
|
echo "elapsed_seconds=${elapsed}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create release-candidate secure-runtime attestation
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
|
QUALIFIED_COMMIT: ${{ steps.source.outputs.commit }}
|
|
LINE_REF: ${{ steps.source.outputs.line_ref }}
|
|
RELEASE_ID: ${{ steps.release.outputs.release_id }}
|
|
TAG: ${{ steps.release.outputs.tag }}
|
|
WORKFLOW_OUTPUT_1: ${{ steps.lab.outputs.elapsed_seconds }}
|
|
run: |
|
|
set -euo pipefail
|
|
release_dir="$RUNNER_TEMP/secure-runtime-verified"
|
|
evidence_dir="$RUNNER_TEMP/secure-runtime-evidence"
|
|
test -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"
|
|
update_fingerprint="$(
|
|
go run ./scripts/release_update_key.go fingerprint \
|
|
--public-key "${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"
|
|
)"
|
|
python3 scripts/release_control/secure_runtime_attestation_v7.py \
|
|
--checkout "${GITHUB_WORKSPACE}" \
|
|
--commit "${QUALIFIED_COMMIT}" \
|
|
--main-ref "${LINE_REF}" \
|
|
--receipt "${evidence_dir}/receipt.json" \
|
|
--receipt-record-path "docs/release-control/v6/internal/records/secure-agent-runtime-systemd-receipt-v7-${TAG}.json" \
|
|
--transcript "${evidence_dir}/transcript.jsonl" \
|
|
--collector-v1 "${release_dir}/pulse-secure-runtime-collector-v1-linux-amd64" \
|
|
--collector-v2 "${release_dir}/pulse-secure-runtime-collector-v2-linux-amd64" \
|
|
--collector-v3 "${release_dir}/pulse-secure-runtime-collector-v3-linux-amd64" \
|
|
--collector-v4 "${release_dir}/pulse-agent-linux-amd64" \
|
|
--helper "${release_dir}/pulse-agent-helper-linux-amd64" \
|
|
--runner "${release_dir}/pulse-agent-runner-linux-amd64" \
|
|
--collector-v1-signature "${release_dir}/pulse-secure-runtime-collector-v1-linux-amd64.sig" \
|
|
--collector-v2-signature "${release_dir}/pulse-secure-runtime-collector-v2-linux-amd64.sig" \
|
|
--collector-v3-signature "${release_dir}/pulse-secure-runtime-collector-v3-linux-amd64.sig" \
|
|
--collector-v4-signature "${release_dir}/pulse-agent-linux-amd64.sig" \
|
|
--elapsed-seconds "${WORKFLOW_OUTPUT_1}" \
|
|
--release-candidate-tag "${TAG}" \
|
|
--release-repository "${GITHUB_REPOSITORY}" \
|
|
--release-id "${RELEASE_ID}" \
|
|
--release-checksums "${release_dir}/checksums.txt" \
|
|
--release-assembly-provenance "${release_dir}/release-build-provenance.sigstore.json" \
|
|
--release-compiler-provenance "${release_dir}/secure-runtime-compiler-provenance.sigstore.json" \
|
|
--release-build-contract "${release_dir}/secure-runtime-build-contract-v1.json" \
|
|
--expected-release-update-key-fingerprint "${update_fingerprint}" \
|
|
--output "${evidence_dir}/attestation.json"
|
|
|
|
- name: Stop disposable systemd host
|
|
if: ${{ always() && steps.lab.outputs.container != '' }}
|
|
env:
|
|
WORKFLOW_OUTPUT_1: ${{ steps.lab.outputs.container }}
|
|
run: docker rm --force "${WORKFLOW_OUTPUT_1}"
|
|
|
|
- name: Upload immutable RC qualification evidence
|
|
if: ${{ success() }}
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: secure-runtime-qualification-${{ steps.release.outputs.tag }}-${{ steps.source.outputs.commit }}
|
|
path: |
|
|
${{ runner.temp }}/secure-runtime-evidence/
|
|
${{ runner.temp }}/secure-runtime-preexecution/preexecution-verification.json
|
|
if-no-files-found: error
|
|
retention-days: 90
|
|
compression-level: 0
|