Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/attest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Reusable workflow that generates artifact PROVENANCE in isolation from the build
# jobs that call it. A reusable workflow runs in its own execution context with its
# own OIDC identity — the caller can inject neither steps nor secrets — so the
# signing material is unreachable by the build's user-defined steps. That isolation
# is the SLSA Build L3 requirement; GitHub artifact attestations generated in-job
# are only L2. Provenance is still written to GitHub's attestation API, so
# `gh attestation verify` keeps working — but the signer-workflow is now this file:
#
# gh attestation verify <artifact-or-oci-ref> --repo <repo> \
# --signer-workflow <repo>/.github/workflows/attest.yml --source-ref refs/tags/<tag>
#
# SLSA note: L3 makes the signed provenance unforgeable; it does not, by itself,
# make the build hermetic. The build job still computes the checksums/digest it
# passes in — L3's guarantee is that the signature cannot be tampered with because
# the key lives only in this isolated workflow.

name: Attest (reusable)

on:
workflow_call:
inputs:
checksums-artifact:
description: Name of an uploaded artifact containing checksums.txt (binary provenance).
type: string
required: false
default: ''
subject-name:
description: Fully-qualified image/chart name without tag/digest (OCI provenance).
type: string
required: false
default: ''
subject-digest:
description: Image/chart digest in sha256:... form (OCI provenance).
type: string
required: false
default: ''
push-to-registry:
description: Attach the OCI provenance attestation to the registry.
type: boolean
required: false
default: false

permissions: {}

jobs:
attest:
name: Attest
runs-on: ubuntu-24.04
permissions:
id-token: write # OIDC token minted HERE, isolated from the build job
attestations: write # write provenance to GitHub's attestation API
contents: read
packages: write # used only for the OCI attestation (push-to-registry)
steps:
- name: Download checksums
if: ${{ inputs.checksums-artifact != '' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.checksums-artifact }}

- name: Attest binary checksums
if: ${{ inputs.checksums-artifact != '' }}
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-checksums: checksums.txt

# The OCI provenance is attached to the registry as a referrer, so this
# isolated job needs its own GHCR login — the build job's docker login does
# not carry into the reusable workflow's separate runner.
- name: Log in to GitHub Container Registry
if: ${{ inputs.subject-digest != '' }}
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Attest OCI provenance
if: ${{ inputs.subject-digest != '' }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ inputs.subject-name }}
subject-digest: ${{ inputs.subject-digest }}
push-to-registry: ${{ inputs.push-to-registry }}
89 changes: 63 additions & 26 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ jobs:
- resolve-release
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -151,10 +149,31 @@ jobs:
RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }}
run: gh release upload "$RELEASE_TAG" dist/release-assets/* --clobber

- name: Attest release checksums
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
- name: Upload checksums for isolated attestation
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
subject-checksums: dist/release-assets/checksums.txt
name: release-checksums
path: dist/release-assets/checksums.txt
if-no-files-found: error
retention-days: 1

# Generate binary provenance in an ISOLATED reusable workflow (SLSA L3): the
# signing OIDC token is minted in attest.yml, unreachable by the build steps.
attest-binaries:
needs:
- binary-release-assets
permissions:
id-token: write
attestations: write
contents: read
# attest.yml's shared job declares packages: write (for the OCI
# attestation's push-to-registry). A reusable workflow cannot request more
# permissions than its caller grants, so every caller must grant it — even
# the binary one, whose attestation never pushes to a registry.
packages: write
uses: ./.github/workflows/attest.yml
with:
checksums-artifact: release-checksums

# Build the per-arch signed apk on a native runner (no QEMU). Each runner mints
# its own ephemeral melange key (distinct filename) and uploads its apk plus its
Expand Down Expand Up @@ -359,15 +378,22 @@ jobs:
sbom-path: image.spdx.json
push-to-registry: true

# SLSA provenance (GitHub artifact attestation). This is the attestation the
# chart's optional Kyverno policy verifies; the signer stays release.yml here.
# Moving it into an isolated reusable workflow (SLSA L3) is a later change.
- name: Attest container image
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ steps.manifest.outputs.name }}
subject-digest: ${{ steps.manifest.outputs.digest }}
push-to-registry: true
# Generate container provenance in the ISOLATED reusable workflow (SLSA L3). This
# is the attestation the chart's optional Kyverno policy verifies — its signer is
# now attest.yml, which the chart's Kyverno defaults trust.
attest-image:
needs:
- container-image-release
permissions:
id-token: write
attestations: write
packages: write
contents: read
uses: ./.github/workflows/attest.yml
with:
subject-name: ${{ needs.container-image-release.outputs.image-name }}
subject-digest: ${{ needs.container-image-release.outputs.image-digest }}
push-to-registry: true

helm-chart-release:
name: Helm Chart Release
Expand All @@ -378,10 +404,8 @@ jobs:
permissions:
contents: read
packages: write
id-token: write
attestations: write
artifact-metadata: write
outputs:
chart-name: ${{ steps.push-chart.outputs.name }}
chart-digest: ${{ steps.push-chart.outputs.digest }}
steps:
- name: Check out repository
Expand Down Expand Up @@ -486,14 +510,23 @@ jobs:
exit "$push_rc"
fi

echo "name=$CHART_NAME" >> "$GITHUB_OUTPUT"
echo "digest=$digest" >> "$GITHUB_OUTPUT"

- name: Attest Helm chart
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-name: ${{ env.CHART_NAME }}
subject-digest: ${{ steps.push-chart.outputs.digest }}
push-to-registry: true
# Generate Helm chart provenance in the ISOLATED reusable workflow (SLSA L3).
attest-chart:
needs:
- helm-chart-release
permissions:
id-token: write
attestations: write
packages: write
contents: read
uses: ./.github/workflows/attest.yml
with:
subject-name: ${{ needs.helm-chart-release.outputs.chart-name }}
subject-digest: ${{ needs.helm-chart-release.outputs.chart-digest }}
push-to-registry: true

release-inspection-summary:
name: Release Inspection Summary
Expand All @@ -503,6 +536,9 @@ jobs:
- binary-release-assets
- container-image-release
- helm-chart-release
- attest-binaries
- attest-image
- attest-chart
permissions: {}
steps:
- name: Write inspection summary
Expand All @@ -523,7 +559,7 @@ jobs:
echo '```sh'
echo "gh release view $RELEASE_TAG --repo $GITHUB_REPOSITORY --json isDraft,assets"
echo "asset=\"template-k8s_${RELEASE_VERSION}_\$(go env GOOS)_\$(go env GOARCH)\""
echo "gh attestation verify \"dist/release-assets/\${asset}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "gh attestation verify \"dist/release-assets/\${asset}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo '```'
echo
echo "Container verification commands:"
Expand All @@ -532,7 +568,8 @@ jobs:
echo "docker login ghcr.io"
echo "docker pull \"${IMAGE_NAME}:${RELEASE_TAG}\""
echo "docker run --rm \"${IMAGE_NAME}:${RELEASE_TAG}\" --help"
echo "gh attestation verify \"oci://${IMAGE_NAME}@${IMAGE_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "gh attestation verify \"oci://${IMAGE_NAME}@${IMAGE_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "cosign verify \"${IMAGE_NAME}@${IMAGE_DIGEST}\" --certificate-identity-regexp \"^https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@.*\" --certificate-oidc-issuer https://token.actions.githubusercontent.com"
echo '```'
echo
echo "Helm chart verification commands:"
Expand All @@ -541,7 +578,7 @@ jobs:
echo "helm show chart \"${CHART_REF}\" --version \"${RELEASE_VERSION}\""
echo "helm pull \"${CHART_REF}\" --version \"${RELEASE_VERSION}\""
echo "helm install template-k8s \"${CHART_REF}\" --version \"${RELEASE_VERSION}\" --namespace template-k8s-system --create-namespace"
echo "gh attestation verify \"${CHART_REF}@${CHART_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "gh attestation verify \"${CHART_REF}@${CHART_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo '```'
echo
echo "Publish or reject the draft release manually after inspection. The container image and Helm chart are already available in GHCR because GHCR does not have a draft release state."
Expand Down
12 changes: 9 additions & 3 deletions DELETE_ME.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ that shape, trim the release files before the first release.
- `charts/template-k8s/values.yaml`
- Update `image.repository`.
- Update `kyverno.imageVerification.attestor.subjectRegExp` so optional
Kyverno image verification trusts the generated repository's release
workflow.
Kyverno image verification trusts the generated repository's provenance
signer, the reusable `.github/workflows/attest.yml` (not `release.yml`).
- Add, remove, or rename values for real controller runtime options.
- Keep fixed image tags or digests; do not default to `latest`.

Expand All @@ -243,7 +243,7 @@ that shape, trim the release files before the first release.

- `charts/template-k8s/templates/kyverno-image-policy.yaml`
- Update the policy name helper and default attestor subject if the chart or
release workflow identity changes.
the provenance signer workflow (`attest.yml`) identity changes.
- Keep it optional unless Kyverno is a hard prerequisite for the generated
repository.

Expand Down Expand Up @@ -290,6 +290,12 @@ that shape, trim the release files before the first release.
- Update Helm chart paths, rendered-output assertions, install examples, and
release inspection summary commands.

- `.github/workflows/attest.yml`
- The reusable workflow that signs binary/image/chart provenance in isolation
(SLSA Build L3). It has no project-specific identifiers, but it IS the signer
identity the Kyverno policy and the release inspection summary trust — keep it
in sync with `kyverno.imageVerification.attestor.subjectRegExp`.

- `.github/workflows/release-dry-run.yml`
- Update image and chart refs.
- Update binary validation names, dry-run image names, OCI archive names,
Expand Down
2 changes: 1 addition & 1 deletion charts/template-k8s/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ kyverno:
attestor:
issuer: https://token.actions.githubusercontent.com
subject: ""
subjectRegExp: ^https://github\.com/meigma/template-k8s/\.github/workflows/release\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$
subjectRegExp: ^https://github\.com/meigma/template-k8s/\.github/workflows/attest\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$
rekor:
url: https://rekor.sigstore.dev
attestation:
Expand Down
2 changes: 1 addition & 1 deletion test/chart/rbac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func TestKyvernoImageVerificationPolicyRendersGitHubAttestationPolicy(t *testing
requireNestedString(
t,
keyless,
"^https://github\\.com/meigma/template-k8s/\\.github/workflows/release\\.yml@refs/tags/"+
"^https://github\\.com/meigma/template-k8s/\\.github/workflows/attest\\.yml@refs/tags/"+
"v[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$",
"subjectRegExp",
)
Expand Down