mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-12 07:18:49 +00:00
351 lines
18 KiB
YAML
351 lines
18 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: 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
|