Skip to content

docs: document the bundled-images attestation predicate-type (ADR-16 … #6

docs: document the bundled-images attestation predicate-type (ADR-16 …

docs: document the bundled-images attestation predicate-type (ADR-16 … #6

Workflow file for this run

name: Release
# Cut a release by pushing a semver tag, e.g.:
# git tag -s v0.1.0 -m "v0.1.0" && git push origin v0.1.0
#
# Produces, per tag:
# - one Kairos image per supported Kubernetes minor, pushed to
# ghcr.io/<owner>/provider-kubernetes:<tag>-k8s<minor> (e.g. v0.1.0-k8s1.34).
# The newest supported minor is additionally tagged <tag> and latest.
# - a GitHub Release with the provider binary + sha256 checksum.
# - SLSA build-provenance + CycloneDX SBOM attestations for every image (by digest)
# and for the binary, signed keyless via the workflow's OIDC identity (ADR-15).
# Verify with `gh attestation verify` -- see the GitHub Release notes / README.
#
# NOTE: the ghcr package inherits the repository's visibility. For EXTERNAL
# testers to pull, make the package public in the org's package settings.
on:
push:
tags:
- "v*"
# Least privilege at the top; each job widens to exactly what it needs
# (images: push + attest; binary: release + attest). See ADR-15.
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# Derive the image matrix from the single source of truth (kubeadm.SupportedMinors),
# exactly as CI does, plus the newest minor for the default/latest aliases.
discover:
name: resolve supported version window
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
minors: ${{ steps.minors.outputs.minors }}
latest_minor: ${{ steps.minors.outputs.latest_minor }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Extract SupportedMinors -> JSON matrix
id: minors
run: |
set -euo pipefail
minors="$(grep 'SupportedMinors *=' internal/kubeadm/version.go \
| grep -oE '[0-9]+\.[0-9]+' \
| jq -R . | jq -cs .)"
if [ "$minors" = "[]" ]; then
echo "could not parse SupportedMinors from version.go" >&2
exit 1
fi
# Newest minor, order-independent (do not rely on slice ordering).
latest="$(echo "$minors" | jq -r 'sort_by(split(".")|map(tonumber))|.[-1]')"
echo "window: $minors ; newest: $latest"
echo "minors=$minors" >> "$GITHUB_OUTPUT"
echo "latest_minor=$latest" >> "$GITHUB_OUTPUT"
# Build and push one image per supported minor.
images:
name: image ${{ github.ref_name }}-k8s${{ matrix.minor }}
needs: discover
runs-on: ubuntu-latest
# The Hadron (musl) image builds containerd AND kubelet static from source,
# which adds several minutes per leg over the old binary-download path.
timeout-minutes: 60
permissions:
packages: write # push images to ghcr
id-token: write # keyless OIDC identity for attestation signing
attestations: write # write provenance + SBOM attestations
strategy:
fail-fast: false
matrix:
minor: ${{ fromJSON(needs.discover.outputs.minors) }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Log in to ghcr.io
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve Kubernetes patch + commit + crictl versions
id: ver
run: |
set -euo pipefail
k8s="$(curl -fsSL "https://dl.k8s.io/release/stable-${{ matrix.minor }}.txt")"
case "$k8s" in
v${{ matrix.minor }}.*) : ;;
*) echo "unexpected patch ${k8s} for minor ${{ matrix.minor }}" >&2; exit 1 ;;
esac
# The Hadron (musl) image builds kubelet static from source, pinning the
# clone to an immutable commit SHA and asserting the resolved tag points
# at it. Resolve the SHA and forward it or the from-source build fails.
# Prefer the peeled tag (^{} -> the commit the annotated tag wraps);
# fall back to the bare ref.
kcommit="$(git ls-remote https://github.com/kubernetes/kubernetes "refs/tags/${k8s}^{}" | cut -f1)"
if [ -z "$kcommit" ]; then
kcommit="$(git ls-remote https://github.com/kubernetes/kubernetes "refs/tags/${k8s}" | cut -f1)"
fi
case "$kcommit" in
?*) : ;;
*) echo "could not resolve commit SHA for ${k8s}" >&2; exit 1 ;;
esac
echo "k8s=${k8s} kcommit=${kcommit}"
echo "k8s=${k8s}" >> "$GITHUB_OUTPUT"
echo "kcommit=${kcommit}" >> "$GITHUB_OUTPUT"
echo "crictl=v${{ matrix.minor }}.0" >> "$GITHUB_OUTPUT"
- name: Build, tag and push image
id: build
env:
TAG: ${{ github.ref_name }}
MINOR: ${{ matrix.minor }}
LATEST_MINOR: ${{ needs.discover.outputs.latest_minor }}
run: |
set -euo pipefail
# Image repo is lowercased ghcr.io/<owner>/<repo> (ghcr requires lowercase).
repo="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
minortag="${repo}:${TAG}-k8s${MINOR}"
make image \
VERSION="${TAG}" \
KUBERNETES_VERSION="${{ steps.ver.outputs.k8s }}" \
KUBERNETES_COMMIT="${{ steps.ver.outputs.kcommit }}" \
CRICTL_VERSION="${{ steps.ver.outputs.crictl }}" \
IMAGE="${minortag}"
# Verify the bundled toolchain matches before publishing.
got="$(docker run --rm --entrypoint /usr/bin/kubeadm "${minortag}" version -o short)"
case "$got" in
v${MINOR}.*) echo "verified kubeadm ${got}" ;;
*) echo "kubeadm mismatch: want v${MINOR}.x got ${got}" >&2; exit 1 ;;
esac
docker push "${minortag}"
# Capture the immutable digest of what we just pushed. Attestation MUST
# target the digest, never a tag (ADR-15). RepoDigests is populated after
# the push; pin to this repo's entry.
digest="$(docker inspect --format='{{range .RepoDigests}}{{println .}}{{end}}' "${minortag}" \
| grep "^${repo}@" | head -1 | sed 's/.*@//')"
case "$digest" in
sha256:*) echo "pushed digest ${digest}" ;;
*) echo "failed to resolve pushed digest for ${minortag}" >&2; exit 1 ;;
esac
echo "image=${repo}" >> "$GITHUB_OUTPUT"
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
# The newest supported minor also gets the plain <tag> and :latest aliases.
# These are the SAME digest (docker tag of the same image), so the single
# digest attestation below covers all three tags.
if [ "${MINOR}" = "${LATEST_MINOR}" ]; then
for alias in "${repo}:${TAG}" "${repo}:latest"; do
docker tag "${minortag}" "${alias}"
docker push "${alias}"
done
fi
# CycloneDX, not SPDX: a full OS-image SPDX SBOM is ~23 MiB and exceeds the
# actions/attest-sbom 16 MiB predicate cap; the equivalent CycloneDX SBOM is
# ~3 MiB. CycloneDX is the compact format for image-scale SBOMs.
- name: Generate image SBOM (CycloneDX)
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
image: ${{ steps.build.outputs.image }}@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: image.cdx.json
upload-artifact: false
upload-release-assets: false # we attest the SBOM; do not auto-attach it to the Release
- name: Attest image build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ${{ steps.build.outputs.image }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Attest image SBOM
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-name: ${{ steps.build.outputs.image }}
subject-digest: ${{ steps.build.outputs.digest }}
sbom-path: image.cdx.json
push-to-registry: true
# ADR-16 P5: attest the pre-bundled control-plane images by DIGEST, including
# whether each was upstream-signature-verified at build. We attest the
# baked-in images.lock DIRECTLY as a custom in-toto predicate (not an SBOM):
# images.lock IS the authoritative ref->digest->verified/verifyReason record,
# and a generic predicate avoids actions/attest-sbom's SBOM-format detector
# (which rejects a compliant hand-rolled CycloneDX) -- more robust and
# future-proof. `gh attestation verify --predicate-type <type>` selects it.
- name: Extract bundled control-plane image lockfile
run: |
set -euo pipefail
docker run --rm --entrypoint cat \
"${{ steps.build.outputs.image }}@${{ steps.build.outputs.digest }}" \
/opt/provider-kubernetes/images/images.lock > images.lock
test -s images.lock
- name: Attest bundled control-plane images
uses: actions/attest@ce27ba3b4a9a139d9a20a4a07d69fabb52f1e5bc # v2
with:
subject-name: ${{ steps.build.outputs.image }}
subject-digest: ${{ steps.build.outputs.digest }}
predicate-type: https://kairos.io/attestations/bundled-control-plane-images/v1
predicate-path: images.lock
push-to-registry: true
# R-4: prove the attestation is real before a tester relies on it. A broken
# signing step fails the release here, not in the field. We verify provenance
# (the load-bearing integrity check); the SBOM attestation's existence is
# already guaranteed by the Attest-image-SBOM step above failing otherwise, so
# we do not add a predicate-type filter here (avoids coupling to the SBOM
# format/spec version).
- name: Self-verify image attestation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
ref="${{ steps.build.outputs.image }}@${{ steps.build.outputs.digest }}"
echo "verifying provenance for oci://${ref}"
gh attestation verify "oci://${ref}" --repo "${{ github.repository }}"
# Build the provider binary and publish a GitHub Release with a checksum.
binary:
name: release binary
needs: [discover, images]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write # create the GitHub Release
id-token: write # keyless OIDC identity for attestation signing
attestations: write # write provenance + SBOM attestations
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
- name: Build binary
run: make build VERSION=${{ github.ref_name }}
- name: Package + checksum
run: |
set -euo pipefail
cd bin
tar -czf "agent-provider-kubernetes_${{ github.ref_name }}_linux_amd64.tar.gz" agent-provider-kubernetes
sha256sum agent-provider-kubernetes_*.tar.gz > checksums.txt
cat checksums.txt
# CycloneDX for consistency with the image SBOM (one SBOM format across all
# published artifacts; well under the attest-sbom 16 MiB cap).
- name: Generate binary SBOM (CycloneDX)
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
file: bin/agent-provider-kubernetes
format: cyclonedx-json
output-file: binary.cdx.json
upload-artifact: false
upload-release-assets: false # we attest the SBOM; do not auto-attach it to the Release
- name: Attest binary build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: bin/agent-provider-kubernetes_*.tar.gz
- name: Attest binary SBOM
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: bin/agent-provider-kubernetes_*.tar.gz
sbom-path: binary.cdx.json
# R-4: verify provenance before publishing the Release (the SBOM attestation's
# existence is guaranteed by the Attest-binary-SBOM step failing otherwise).
- name: Self-verify binary attestation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tarball="$(ls bin/agent-provider-kubernetes_*.tar.gz)"
echo "verifying provenance for ${tarball}"
gh attestation verify "${tarball}" --repo "${{ github.repository }}"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
MINORS: ${{ needs.discover.outputs.minors }}
LATEST_MINOR: ${{ needs.discover.outputs.latest_minor }}
REPO_SLUG: ${{ github.repository }}
run: |
set -euo pipefail
repo="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
# Image list derived from the supported window (single source of truth);
# no hardcoded minors here, so the notes never drift as the window rolls.
imglist=""
for m in $(echo "${MINORS}" | jq -r '.[]'); do
imglist="${imglist}${repo}:${TAG}-k8s${m}"$'\n'
done
verify="$(printf 'Verifying (every image + the binary carry keyless SLSA provenance + CycloneDX SBOM attestations):\n```\n# image (resolve the digest first, then verify by digest)\ngh attestation verify oci://%s:%s --repo %s\n# binary\ngh attestation verify agent-provider-kubernetes_%s_linux_amd64.tar.gz --repo %s\n```\n' \
"${repo}" "${TAG}" "${REPO_SLUG}" "${TAG}" "${REPO_SLUG}")"
notes="$(printf 'Kairos images (one per supported Kubernetes minor):\n```\n%s```\nThe newest supported minor (%s) is also published as `%s:%s` and `%s:latest`.\n\n%s\nProvider binary (linux/amd64) attached below. This is a development release; not yet field-ready.\n' \
"${imglist}" "${LATEST_MINOR}" "${repo}" "${TAG}" "${repo}" "${verify}")"
# Idempotent: update an existing release (e.g. a re-run), else create it.
if gh release view "${TAG}" >/dev/null 2>&1; then
gh release upload "${TAG}" bin/agent-provider-kubernetes_*.tar.gz bin/checksums.txt --clobber
else
gh release create "${TAG}" \
bin/agent-provider-kubernetes_*.tar.gz bin/checksums.txt \
--title "${TAG}" --notes "${notes}" --prerelease
fi