Skip to content

Harden production SSH execution and CodeQL gate (#110) #147

Harden production SSH execution and CodeQL gate (#110)

Harden production SSH execution and CodeQL gate (#110) #147

name: release-artifacts
# One release graph owns binaries, the VM candidate test, the draft GitHub
# release, and GHCR promotion. A tag is not made visible as a release until the
# exact candidate binaries pass positive and classified-negative VM tests and
# the candidate container has been built and verified.
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
GO_VERSION: "1.25.12"
IMAGE: ghcr.io/kernel-guard/bpfcompat
jobs:
build-and-sign:
name: Build candidate, SBOM, sign
runs-on: ubuntu-latest
timeout-minutes: 25
permissions:
actions: read
contents: read
id-token: write
attestations: write
packages: read
outputs:
version: ${{ steps.meta.outputs.version }}
channel: ${{ steps.meta.outputs.channel }}
commit: ${{ steps.meta.outputs.commit }}
build_date: ${{ steps.meta.outputs.build_date }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Validate release metadata
run: make check-release-consistency
- name: Verify protected production environment
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
reviewer="$(awk -F': ' '$1 == "release_reviewer" {print $2; exit}' release.yaml)"
bash scripts/check-production-environment.sh \
"$GITHUB_REPOSITORY" production-release "$reviewer"
- name: Refuse mutation of an already-published release
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
run: bash scripts/check-release-state.sh "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME"
- name: Release quality gates
run: |
set -euo pipefail
make check-docs-drift
bash scripts/check-release-state_test.sh
bash scripts/check-release-consistency_test.sh
bash scripts/check-production-environment_test.sh
bash scripts/promote-release_test.sh
bash scripts/production-readiness-report_test.sh
bash scripts/verify-release-assets_test.sh
go vet ./...
go test -race -count=1 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.6.0 ./...
make production-tech-check
- name: Resolve build metadata
id: meta
env:
REF: ${{ github.ref }}
run: |
set -euo pipefail
release_channel="$(awk -F': ' '$1 == "release_channel" {print $2; exit}' release.yaml)"
if [[ "$REF" == refs/tags/* ]]; then
version="${REF#refs/tags/}"
channel="$release_channel"
else
version="$(git describe --tags --always --dirty)"
channel="preview"
fi
{
echo "version=${version}"
echo "channel=${channel}"
echo "commit=$(git rev-parse --short=12 HEAD)"
echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} >> "$GITHUB_OUTPUT"
- name: Build candidate binaries
env:
VERSION: ${{ steps.meta.outputs.version }}
COMMIT: ${{ steps.meta.outputs.commit }}
BUILD_DATE: ${{ steps.meta.outputs.build_date }}
run: |
set -euo pipefail
mkdir -p dist
LDFLAGS="-X github.com/kernel-guard/bpfcompat/internal/version.Version=${VERSION} \
-X github.com/kernel-guard/bpfcompat/internal/version.Commit=${COMMIT} \
-X github.com/kernel-guard/bpfcompat/internal/version.BuildDate=${BUILD_DATE}"
CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" \
-o dist/bpfcompat-linux-amd64 ./cmd/bpfcompat
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" \
-o dist/bpfcompat-linux-arm64 ./cmd/bpfcompat
- name: Build static validator
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libbpf-dev libelf-dev libzstd-dev zlib1g-dev
make validator-static
cp validator/c-libbpf/bin/bpfcompat-validator \
dist/bpfcompat-validator-static-linux-amd64
- name: Stage production readiness evidence
if: startsWith(github.ref, 'refs/tags/v') && steps.meta.outputs.channel == 'stable'
env:
RELEASE_VERSION: ${{ steps.meta.outputs.version }}
run: |
set -euo pipefail
version="${RELEASE_VERSION#v}"
source_base="docs/releases/bpfcompat-${version}-readiness"
test -s "${source_base}.json"
test -s "${source_base}.md"
jq -e --arg version "$version" '
.schema_version == "v0.1" and
.gate_status == "ready" and
.release_version == $version and
.slo.campaign_count == 4 and
.slo.infrastructure_errors == 0 and
.reviewer.approved == true
' "${source_base}.json" >/dev/null
cp "${source_base}.json" dist/production-readiness.json
cp "${source_base}.md" dist/production-readiness.md
- name: Compute checksums
run: |
set -euo pipefail
cd dist
assets=(
bpfcompat-linux-amd64
bpfcompat-linux-arm64
bpfcompat-validator-static-linux-amd64
)
if [[ -s production-readiness.json && -s production-readiness.md ]]; then
assets+=(production-readiness.json production-readiness.md)
fi
sha256sum "${assets[@]}" > SHA256SUMS
- name: Generate SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
with:
path: .
format: cyclonedx-json
output-file: dist/bpfcompat.sbom.cdx.json
- name: Attest candidate provenance
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
dist/bpfcompat-linux-amd64
dist/bpfcompat-linux-arm64
dist/bpfcompat-validator-static-linux-amd64
- name: Attest production readiness evidence
if: startsWith(github.ref, 'refs/tags/v') && steps.meta.outputs.channel == 'stable'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
dist/production-readiness.json
dist/production-readiness.md
- name: Attest candidate SBOM
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: dist/bpfcompat-linux-amd64
sbom-path: dist/bpfcompat.sbom.cdx.json
- name: Install cosign
if: startsWith(github.ref, 'refs/tags/v')
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: "v2.6.3"
- name: Sign checksums and SBOM
if: startsWith(github.ref, 'refs/tags/v')
run: |
set -euo pipefail
cosign sign-blob --yes dist/SHA256SUMS \
--output-signature dist/SHA256SUMS.sig \
--output-certificate dist/SHA256SUMS.crt
cosign sign-blob --yes dist/bpfcompat.sbom.cdx.json \
--output-signature dist/bpfcompat.sbom.cdx.sig \
--output-certificate dist/bpfcompat.sbom.cdx.crt
- name: Upload candidate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: bpfcompat-release-${{ steps.meta.outputs.version }}
path: dist/
if-no-files-found: error
- name: Upload production technical evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: production-tech-evidence-${{ steps.meta.outputs.version }}
path: evidence/production-tech/
if-no-files-found: error
candidate-vm-gate:
name: Candidate VM positive + classified negative
if: startsWith(github.ref, 'refs/tags/v')
needs: build-and-sign
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
attestations: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: bpfcompat-release-${{ needs.build-and-sign.outputs.version }}
path: dist
- name: Verify candidate attestations
env:
GH_TOKEN: ${{ github.token }}
run: >
bash scripts/verify-release-assets.sh
dist Kernel-Guard/bpfcompat
bpfcompat-linux-amd64
bpfcompat-validator-static-linux-amd64
- name: Install VM dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
qemu-system-x86 qemu-utils cloud-image-utils clang llvm \
libbpf-dev libelf-dev zlib1g-dev pkg-config jq
if [[ -e /dev/kvm ]]; then
sudo chmod 0666 /dev/kvm
fi
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: vm/cache/ubuntu-22.04-release-20260722.qcow2
key: ubuntu-22.04-release-20260722-757908b2
- name: Prepare exact candidates and fixtures
env:
EXPECTED_VERSION: ${{ needs.build-and-sign.outputs.version }}
run: |
set -euo pipefail
chmod +x dist/bpfcompat-linux-amd64 \
dist/bpfcompat-validator-static-linux-amd64
actual="$(dist/bpfcompat-linux-amd64 version --json | jq -r .version)"
test "$actual" = "$EXPECTED_VERSION"
make examples
echo "BPFCOMPAT_VALIDATOR_BIN=${GITHUB_WORKSPACE}/dist/bpfcompat-validator-static-linux-amd64" >> "$GITHUB_ENV"
echo "BPFCOMPAT_VALIDATOR_SHA256=$(sha256sum dist/bpfcompat-validator-static-linux-amd64 | awk '{print $1}')" >> "$GITHUB_ENV"
- name: Positive test through the candidate Action
uses: ./
with:
artifact: examples/simple-pass/simple_pass.bpf.o
manifest: examples/simple-pass/manifest-dev-one.yaml
matrix: matrices/dev-one.yaml
out: reports/release-positive.json
markdown: reports/release-positive.md
timeout: 8m
concurrency: "1"
binary: dist/bpfcompat-linux-amd64
build: "false"
prebuilt: "never"
- name: Classified negative test through the candidate binary
run: |
set -euo pipefail
set +e
dist/bpfcompat-linux-amd64 test \
--artifact examples/unknown-load-fail/invalid_object.bin \
--manifest examples/unknown-load-fail/manifest-dev-one.yaml \
--matrix matrices/dev-one.yaml \
--out reports/release-negative.json \
--markdown reports/release-negative.md \
--timeout 8m \
--concurrency 1
rc=$?
set -e
test "$rc" -eq 2
jq -e '.summary.status == "pass"' reports/release-positive.json >/dev/null
jq -e '.summary.status == "fail"' reports/release-negative.json >/dev/null
jq -e '.targets | all(.status != "infra_error")' \
reports/release-positive.json reports/release-negative.json >/dev/null
jq -e --arg validator "$BPFCOMPAT_VALIDATOR_SHA256" \
'.validator.sha256 == $validator' \
reports/release-positive.json reports/release-negative.json >/dev/null
jq -e '.. | objects | select(.classification_code? == "UNKNOWN")' \
reports/release-negative.json >/dev/null
jq -e --arg digest "757908b2fd6d5b1431bb45070fc1f56cbf017d4025568d292ece37d9cc75e812" \
'all(.targets[]; any(.notes[]?; contains($digest)))' \
reports/release-positive.json reports/release-negative.json >/dev/null
- name: Upload release-gate evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-gate-evidence-${{ github.run_id }}
path: |
reports/release-positive.json
reports/release-positive.md
reports/release-negative.json
reports/release-negative.md
.bpfcompat/runs/**/targets/**/serial.log
.bpfcompat/runs/**/targets/**/libbpf.log
.bpfcompat/runs/**/targets/**/validator-result.json
if-no-files-found: warn
candidate-arm64-cli-gate:
name: Candidate ARM64 CLI native smoke
if: startsWith(github.ref, 'refs/tags/v')
needs: build-and-sign
runs-on: ubuntu-24.04-arm
timeout-minutes: 15
permissions:
contents: read
attestations: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: bpfcompat-release-${{ needs.build-and-sign.outputs.version }}
path: dist
- name: Verify ARM64 candidate attestation
env:
GH_TOKEN: ${{ github.token }}
run: >
bash scripts/verify-release-assets.sh
dist Kernel-Guard/bpfcompat
bpfcompat-linux-arm64
- name: Execute ARM64 candidate natively
env:
EXPECTED_VERSION: ${{ needs.build-and-sign.outputs.version }}
run: |
set -euo pipefail
test "$(uname -m)" = "aarch64"
chmod +x dist/bpfcompat-linux-arm64
actual="$(dist/bpfcompat-linux-arm64 version --json | jq -r .version)"
test "$actual" = "$EXPECTED_VERSION"
dist/bpfcompat-linux-arm64 --help >/dev/null
build-candidate-image:
name: Build, verify, sign candidate image
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-and-sign, candidate-vm-gate, candidate-arm64-cli-gate]
runs-on: ubuntu-latest
timeout-minutes: 40
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build candidate image
id: build
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
context: .
platforms: linux/amd64,linux/arm64
target: final
push: true
provenance: false
sbom: false
tags: ${{ env.IMAGE }}:candidate-${{ github.sha }}
build-args: |
VERSION=${{ needs.build-and-sign.outputs.version }}
COMMIT=${{ needs.build-and-sign.outputs.commit }}
BUILD_DATE=${{ needs.build-and-sign.outputs.build_date }}
- name: Verify candidate image version
env:
DIGEST: ${{ steps.build.outputs.digest }}
EXPECTED_VERSION: ${{ needs.build-and-sign.outputs.version }}
run: |
set -euo pipefail
actual="$(docker run --rm "${IMAGE}@${DIGEST}" version --json | jq -r .version)"
test "$actual" = "$EXPECTED_VERSION"
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: "v2.6.3"
- name: Sign candidate image
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: cosign sign --yes "${IMAGE}@${DIGEST}"
- name: Attest candidate image provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
assemble-candidate-evidence:
name: Assemble candidate promotion evidence
if: startsWith(github.ref, 'refs/tags/v')
needs:
- build-and-sign
- candidate-vm-gate
- candidate-arm64-cli-gate
- build-candidate-image
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: bpfcompat-release-${{ needs.build-and-sign.outputs.version }}
path: candidate/dist
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: release-gate-evidence-${{ github.run_id }}
path: candidate/vm
- name: Bind promotion evidence to exact inputs and outputs
env:
RELEASE_VERSION: ${{ needs.build-and-sign.outputs.version }}
RELEASE_CHANNEL: ${{ needs.build-and-sign.outputs.channel }}
IMAGE_DIGEST: ${{ needs.build-candidate-image.outputs.digest }}
run: |
set -euo pipefail
positive="candidate/vm/reports/release-positive.json"
negative="candidate/vm/reports/release-negative.json"
checksums="candidate/dist/SHA256SUMS"
test -s "$positive"
test -s "$negative"
test -s "$checksums"
jq -e '.summary.status == "pass"' "$positive" >/dev/null
jq -e '
.summary.status == "fail" and
([.targets[] | select(.status == "infra_error")] | length == 0)
' "$negative" >/dev/null
mkdir -p candidate/evidence
jq -n \
--arg schema_version "v0.1" \
--arg repository "$GITHUB_REPOSITORY" \
--arg workflow "$GITHUB_WORKFLOW_REF" \
--argjson run_id "$GITHUB_RUN_ID" \
--argjson run_attempt "$GITHUB_RUN_ATTEMPT" \
--arg commit_sha "$GITHUB_SHA" \
--arg tag "$RELEASE_VERSION" \
--arg channel "$RELEASE_CHANNEL" \
--arg image "${IMAGE}@${IMAGE_DIGEST}" \
--arg checksums_sha256 "$(sha256sum "$checksums" | awk '{print $1}')" \
--arg positive_report_sha256 "$(sha256sum "$positive" | awk '{print $1}')" \
--arg negative_report_sha256 "$(sha256sum "$negative" | awk '{print $1}')" \
--arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{
schema_version: $schema_version,
repository: $repository,
workflow: $workflow,
workflow_run_id: $run_id,
workflow_run_attempt: $run_attempt,
commit_sha: $commit_sha,
tag: $tag,
channel: $channel,
image: $image,
checksums_sha256: $checksums_sha256,
positive_report_sha256: $positive_report_sha256,
negative_report_sha256: $negative_report_sha256,
generated_at: $generated_at
}' >candidate/evidence/release-candidate-evidence.json
- name: Attest candidate promotion evidence
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: candidate/evidence/release-candidate-evidence.json
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: bpfcompat-candidate-evidence-${{ needs.build-and-sign.outputs.version }}
path: candidate/evidence/release-candidate-evidence.json
if-no-files-found: error
stage-draft-release:
name: Stage draft release
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-and-sign, assemble-candidate-evidence]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: bpfcompat-release-${{ needs.build-and-sign.outputs.version }}
path: dist
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: bpfcompat-candidate-evidence-${{ needs.build-and-sign.outputs.version }}
path: dist
- uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
draft: true
prerelease: ${{ needs.build-and-sign.outputs.channel == 'prerelease' }}
make_latest: false
fail_on_unmatched_files: ${{ needs.build-and-sign.outputs.channel == 'stable' }}
generate_release_notes: true
files: |
dist/bpfcompat-linux-amd64
dist/bpfcompat-linux-arm64
dist/bpfcompat-validator-static-linux-amd64
dist/SHA256SUMS
dist/SHA256SUMS.sig
dist/SHA256SUMS.crt
dist/bpfcompat.sbom.cdx.json
dist/bpfcompat.sbom.cdx.sig
dist/bpfcompat.sbom.cdx.crt
dist/release-candidate-evidence.json
dist/production-readiness.*
promote:
name: Promote image and publish release
if: startsWith(github.ref, 'refs/tags/v')
needs:
- build-and-sign
- build-candidate-image
- assemble-candidate-evidence
- stage-draft-release
runs-on: ubuntu-latest
environment:
name: production-release
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Promote the verified image digest
env:
DIGEST: ${{ needs.build-candidate-image.outputs.digest }}
RELEASE_VERSION: ${{ needs.build-and-sign.outputs.version }}
RELEASE_CHANNEL: ${{ needs.build-and-sign.outputs.channel }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
bash scripts/promote-release.sh \
"$IMAGE" "$DIGEST" "$RELEASE_VERSION" "$RELEASE_CHANNEL"