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: 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}")" echo "tag=${tag}" >> "$GITHUB_OUTPUT" echo "release_id=${release_id}" >> "$GITHUB_OUTPUT" - name: Checkout exact release tag uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 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 git fetch --no-tags origin refs/heads/main:refs/remotes/origin/main 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/main echo "commit=${commit}" >> "$GITHUB_OUTPUT" - 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 }} 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 origin/main \ --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 }} RELEASE_ID: ${{ steps.release.outputs.release_id }} TAG: ${{ steps.release.outputs.tag }} 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 origin/main \ --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 "${{ steps.lab.outputs.elapsed_seconds }}" \ --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 != '' }} run: docker rm --force "${{ steps.lab.outputs.container }}" - 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