attest-docker.yml
Generate and attach security attestations (SLSA provenance and/or SBOM) and/or a cosign keyless signature to an already-built Docker image. Designed to run after build-docker.yml.
Inputs
| Input | Type | Description | Required | Default |
|---|---|---|---|---|
| IMAGE_NAME | string | Full image name including registry and path (e.g. ghcr.io/my-org/my-image). Normalized automatically. | Yes | - |
| DIGEST | string | Digest of the image to attest (e.g. sha256:abc123...). Use the digest output of build-docker.yml. | Yes | - |
| PROVENANCE | boolean | Generate GitHub's standard SLSA build provenance attestation (calling workflow, repository and commit) | No | false |
| SBOM | boolean | Generate an SBOM (Software Bill of Materials) attestation for the image | No | false |
| SIGN | boolean | Keyless-sign the image digest with cosign | No | false |
| PREDICATE_TYPE | string | URI identifying the type of a custom in-toto predicate to attach in addition to the standard attestations. Set together with PREDICATE. Use a URI you control (not https://slsa.dev/provenance/v1). | No | - |
| PREDICATE | string | JSON content for the custom in-toto predicate. Set together with PREDICATE_TYPE. | No | - |
| RUNS_ON | string | Runner labels as JSON array (e.g., '["ubuntu-24.04"]' or '["self-hosted", "linux"]') | No | ["ubuntu-24.04"] |
Secrets
| Secret | Description | Required |
|---|---|---|
| REGISTRY_USERNAME | Username used to login into registry (not needed for ghcr.io) | No |
| REGISTRY_PASSWORD | Password used to login into registry (not needed for ghcr.io) | No |
Permissions
| Scope | Access | Description |
|---|---|---|
| packages | write | Push attestations to the registry |
| id-token | write | Required to sign attestations via OIDC |
| attestations | write | Required to create GitHub attestations |
Notes
- This workflow is designed to be called after
build-docker.yml, using itsdigestandimageoutputs. - At least one of
PROVENANCE,SBOM,SIGN, or a custom predicate (bothPREDICATEandPREDICATE_TYPEset) must be provided for the job to perform a useful action. - SLSA Provenance:
PROVENANCE: truegenerates GitHub's standard auto-detected SLSA build provenance (workflow, repo, commit of the calling repository) viaactions/attest-build-provenance, attached to the image in the registry. - SBOM: generates an SPDX SBOM via Trivy, then attaches it to the image in the registry as a cosign attestation (keyless, Sigstore/Fulcio via OIDC). See Verifying attestations below — the SBOM is verified differently from the provenance.
- The SBOM is an inventory, not a vulnerability report — Trivy logs
"--format spdx-json" disables security scanning, which is expected. Findings go stale within days while the package list does not, so baking them in would ship a verdict that is wrong shortly after publication. Scan the SBOM against a current database when you need one, without re-pulling the image:trivy sbom sbom.spdx.json. - Signing: when
SIGNistrue, the image digest is keyless-signed with cosign (Sigstore/Fulcio via OIDC), independent ofPROVENANCE/SBOM. - Custom predicate: set both
PREDICATE_TYPEandPREDICATEtogether to attach an extra in-toto attestation alongside the standard provenance — useful, for example, to record an upstream source/version and architectures that this build mirrors, which the auto-generated provenance has no field for. Omitting either input while providing the other causes a fast-fail error. Use a predicate type URI you control; do not reuse the reservedhttps://slsa.dev/provenance/v1type, as GitHub validates itsbuildTypeagainst a fixed allowlist and rejects custom values. - The image name is automatically normalized (lowercase,
_replaced with-) for OCI registry compatibility. - For
ghcr.io, authentication usesgithub.tokenautomatically; for other registries, provideREGISTRY_USERNAMEandREGISTRY_PASSWORDas secrets. - Matrix builds: a matrix
build-docker.ymljob cannot feed a single matrix-shapedattestjob —needs.<job>.outputs.<name>collapses to one value across all matrix combinations (GitHub's documented last-write-wins behavior), so the digest a matrixedattestjob would see is wrong for every combination but one. Use one explicit, non-matrixedbuild/attestjob pair per image instead. See build-docker.yml → Matrix builds for the full pattern. - Provenance is not gated on the SBOM steps: it is the attestation that establishes where and from what the image was built, so a problem generating or attaching an SBOM never costs an image its provenance.
Verifying attestations
The two attestations use different mechanisms, so they are verified with different commands.
Provenance — a GitHub attestation, also visible in the repository's Attestations tab:
gh attestation verify oci://<registry>/<image>:<tag> --owner <org>SBOM — a cosign attestation:
cosign verify-attestation --type spdxjson \
--certificate-identity-regexp '^https://github.com/<org>/<workflows-repo>/.github/workflows/attest-docker.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
<registry>/<image>@<digest>IMPORTANT
The certificate identity is the reusable workflow that signed it — attest-docker.yml in the repository hosting these workflows — not the repository being released. Keyless signing records the called workflow in the certificate, so anchoring the pattern to your own repository will fail to match. Do not drop the constraint to make it pass: without it, verification accepts a signature from anyone.
Why the SBOM uses cosign
actions/attest refuses an SBOM larger than 16 MiB. That is reachable on ordinary content — the transitive module graph of a few dozen statically linked Go or Rust binaries runs to thousands of packages — and the only way to fit under it would be to drop entries, which removes exactly the supply-chain data the SBOM exists to carry.
cosign has no such ceiling, so it is used for every SBOM rather than as a fallback past some size. If the mechanism switched with size, the command needed to verify an image's SBOM would depend on how large that image happened to be, and could change from one release to the next as it grew. One mechanism means one command, for every image, permanently.
Provenance stays on actions/attest-build-provenance because that action generates the SLSA predicate — build platform, workflow, commit, invocation. cosign only signs a predicate you already have.
The trade-off: SBOMs do not appear in the repository's Attestations tab and are not returned by gh attestation verify. Provenance still is.
Examples
After a build with provenance and SBOM
jobs:
build:
uses: this-is-tobi/github-workflows/.github/workflows/build-docker.yml@v0
permissions:
packages: write
contents: read
with:
IMAGE_NAME: ghcr.io/my-org/my-app
IMAGE_TAG: ${{ needs.release.outputs.version }}
IMAGE_CONTEXT: ./
IMAGE_DOCKERFILE: ./Dockerfile
LATEST_TAG: true
attest:
uses: this-is-tobi/github-workflows/.github/workflows/attest-docker.yml@v0
needs:
- build
permissions:
packages: write
id-token: write
attestations: write
with:
IMAGE_NAME: ${{ needs.build.outputs.image }}
DIGEST: ${{ needs.build.outputs.digest }}
PROVENANCE: true
SBOM: trueProvenance only
jobs:
attest:
uses: this-is-tobi/github-workflows/.github/workflows/attest-docker.yml@v0
needs:
- build
permissions:
packages: write
id-token: write
attestations: write
with:
IMAGE_NAME: ${{ needs.build.outputs.image }}
DIGEST: ${{ needs.build.outputs.digest }}
PROVENANCE: trueStandard provenance plus a custom predicate (e.g. mirror metadata)
jobs:
attest:
uses: this-is-tobi/github-workflows/.github/workflows/attest-docker.yml@v0
needs:
- build
permissions:
packages: write
id-token: write
attestations: write
with:
IMAGE_NAME: ${{ needs.build.outputs.image }}
DIGEST: ${{ needs.build.outputs.digest }}
SIGN: true
SBOM: true
PROVENANCE: true
PREDICATE_TYPE: https://my-org.github.io/my-repo/mirror/v1
PREDICATE: '{"upstream":{"repository":"upstream-org/upstream-repo","source":"https://github.com/upstream-org/upstream-repo","version":"1.2.3","ref":"v1.2.3"},"mirror":{"architectures":["amd64","arm64"]}}'With a custom registry
jobs:
attest:
uses: this-is-tobi/github-workflows/.github/workflows/attest-docker.yml@v0
needs:
- build
permissions:
packages: write
id-token: write
attestations: write
with:
IMAGE_NAME: docker.io/my-org/my-image
DIGEST: ${{ needs.build.outputs.digest }}
PROVENANCE: true
SBOM: true
secrets:
REGISTRY_USERNAME: ${{ secrets.DOCKER_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}