Skip to content

UID2-6764: Add SLSA build provenance attestations to docker publish workflows#228

Open
BehnamMozafari wants to merge 14 commits intomainfrom
bmz-UID2-6764-artifact-attestation
Open

UID2-6764: Add SLSA build provenance attestations to docker publish workflows#228
BehnamMozafari wants to merge 14 commits intomainfrom
bmz-UID2-6764-artifact-attestation

Conversation

@BehnamMozafari
Copy link
Copy Markdown
Contributor

@BehnamMozafari BehnamMozafari commented May 6, 2026

Summary

Adds SLSA build-provenance attestation to every non-snapshot image published by the shared docker workflows.

  • New composite action actions/attest_image wraps the full attest+verify path: it lowercases the image ref once, calls actions/attest@v4.1.0 (pinned to 59d8942), and runs gh attestation verify against the just-pushed digest. Verify failure is hard on public repos; demoted to a warning on private repos (see "Private-repo verify caveat" below).
  • Both shared-publish-java-to-docker-versioned.yaml and actions/shared_publish_to_docker/action.yaml now call attest_image@v3 instead of inlining the attest block.
  • Publish jobs gain id-token: write and attestations: write.
  • Attestation is skipped on snapshot builds via the existing not_snapshot guard.

Closes UID2-6764. Spike was UID2-5763.

Review-comment responses

# Comment Resolution
1 Real smoke test without the attest step skipped Two evidence trails below — an in-repo unit-smoke and an end-to-end smoke through the full shared workflow chain on UnifiedID2/uid2-test-source.
2 Add gh attestation verify step Built into attest_image. Public repos hard-fail on mismatch; private repos warn-only due to a known gh CLI limitation.
3 Case sensitivity of subject-name actions/attest@v4 already auto-lowercases subject-name when push-to-registry: true (verified in src/main.tsdowncaseName: inputs.pushToRegistry, applied at src/subject.ts line 47). However gh attestation verify does not lowercase the OCI URI we pass it. attest_image lowercases once and reuses for both. (The first unit-smoke run caught a real case-sensitivity failure when ${{ github.repository }} evaluated to IABTechLab/... and docker push rejected it — proves the concern.)
4 Comment for NODE_OPTIONS Added inline in attest_image/action.yaml.
5 Extract duplication into a composite action Done — actions/attest_image/action.yaml.

Smoke test evidence

1. Unit smoke (composite action in isolation)

Run 25542801315attest_image against a throwaway alpine image built inline. External verify:

$ gh attestation verify \
    "oci://ghcr.io/iabtechlab/uid2-shared-actions/test-attest@sha256:e008cbdd1c67eee898020ad96d56ff0d42d762585ef4c1153479abaf5a4112bb" \
    --owner "IABTechLab"
✓ Verification succeeded!

- Attestation #1
  - Build repo:..... IABTechLab/uid2-shared-actions
  - Build workflow:. .github/workflows/test-attest-image.yaml@refs/heads/bmz-UID2-6764-artifact-attestation
  - Signer repo:.... IABTechLab/uid2-shared-actions
  - Signer workflow: .github/workflows/test-attest-image.yaml@refs/heads/bmz-UID2-6764-artifact-attestation

2. End-to-end smoke (full shared workflow chain)

Run 25643422322 on UnifiedID2/uid2-test-source (private), branch release-UID2-6764-smoke. Workflow conclusion: success. Exercised in order: shared_publish_setup → docker login → docker metadata → docker build (with load: true) → actions/vulnerability_scan@v3 → docker push (push: true) → actions/attest_image (lowercase → actions/attest@v4.1.0 → in-CI verify) → actions/shared_create_releases@v2 (draft release).

Attestation created for ghcr.io/unifiedid2/uid2-test-source/uid2-6764-smoke@sha256:05058e770dbfc2fe1c8c6a516e2a9b224cd668af5c52f13b87c44f17e2b69e4c, signed by GitHub's internal Sigstore instance, uploaded to both the GitHub attestations API and the OCI registry.

Private-repo verify caveat

The end-to-end smoke surfaced a real gh CLI limitation: for attestations signed by GitHub's internal Sigstore instance (the cert chain used for private repos), gh attestation verify fails with:

Error: verifying with issuer "GitHub, Inc."

Tried --no-public-good, --bundle-from-oci, and explicit --cert-oidc-issuer; same failure. The attestation itself is genuinely created and uploaded — both Attestation uploaded to repository and Attestation uploaded to registry log lines fire, and the bundle is fetchable.

To keep private consumers (UnifiedID2/uid2-snowflake, UnifiedID2/uid2-databricks) from hard-failing on every publish, attest_image demotes a verify failure to a ::warning:: when github.event.repository.private == true. Public repos (the four IABTechLab/* consumers) still hard-fail on a real verify mismatch, preserving Jon's review-#2 intent. External verifiers remain authoritative for both.

Test plan

  • Snapshot smoke on IABTechLab/uid2-admin (Java path) — run 25421656856not_snapshot guard verified.
  • Unit smoke of attest_imagerun 25542801315 — sign + verify green.
  • End-to-end smoke of the full non-Java shared workflow chain — run 25643422322 — green on a private repo with the verify-warning fallback exercised.
  • Release-tag E2E on each public consumer's first real publish after v3 is promoted; verified digests recorded in UID2-6764.

Post-merge sequence

  1. Merge this PR.
  2. Run update-major-version-tags.yaml on main immediately after — the refactored workflows reference actions/attest_image@v3, and consumers triggering between merge and tag promotion would fail with "action not found".
  3. Merge each of the 6 caller-repo follow-up PRs below.
  4. On each public consumer's first real publish, capture the verified digest in UID2-6764.
  5. Delete IABTechLab/uid2-admin:bmz-UID2-6764-test (the snapshot smoke pin branch).

Caller-repo follow-up — already opened (one PR each, all open)

Repo PR
IABTechLab/uid2-operator #2531
IABTechLab/uid2-core #403
IABTechLab/uid2-admin #632
IABTechLab/uid2-optout #402
UnifiedID2/uid2-snowflake #299
UnifiedID2/uid2-databricks #132

Each grants id-token: write + attestations: write (plus the implicit defaults the publish job already relied on). They're additive and harmless until this PR merges and v3 is promoted.

SDK images are explicitly out of scope; follow-up ticket to be filed separately.

BehnamMozafari and others added 5 commits May 6, 2026 13:46
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Attestation runs after the docker push but before the changelog/release
steps. Without continue-on-error, an attest failure leaves a half-finished
release: image pushed, no GitHub Release created. Tolerate attest failures
during the v3 rollout so consumers aren't stuck mid-release if attestation
breaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jon8787
Copy link
Copy Markdown
Contributor

jon8787 commented May 8, 2026

Can we do a real smoke test without "Attest build provenance" step being skipped?
e.g. using uid2-test-source or similar?

Comment thread actions/shared_publish_to_docker/action.yaml Outdated
Comment thread .github/workflows/shared-publish-java-to-docker-versioned.yaml Outdated
Comment thread actions/shared_publish_to_docker/action.yaml Outdated
Comment thread actions/shared_publish_to_docker/action.yaml Outdated
BehnamMozafari and others added 9 commits May 8, 2026 17:19
Addresses jon8787's review comments on PR #228:
- #2 verify step: attest_image now calls 'gh attestation verify' immediately
  after signing so misconfigured signatures fail at build time, not consumer
  pull time.
- #3 case sensitivity: lowercase the image ref once and reuse it for both
  signing and verifying. actions/attest@v4 already lowercases subject-name
  internally when push-to-registry is true (verified at the pinned commit
  59d8942 in src/main.ts and src/subject.ts), but 'gh attestation verify'
  does NOT lowercase the OCI URI we pass it; doing it ourselves keeps the
  signed name and the verified URI byte-identical.
- #4 NODE_OPTIONS comment: brief comment explaining why we mirror
  actions/attest-build-provenance's defensive HTTP header bump.
- #5 extract: pulled the attest+verify pair into a single composite action
  so the Java workflow and the non-Java composite action share one
  implementation.

Adds .github/workflows/test-attest-image.yaml: a manually-dispatched smoke
test that builds a throwaway image and exercises the full attest+verify
path. Use this whenever attest_image or actions/attest@v4 changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop after merge — only here so the smoke test can run before the workflow
file lands on main (gh workflow run / API dispatch require the file to
exist on the default branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github.repository is mixed case; docker rejects mixed-case tags at push
time. Compute a lowercased ref once and reuse it for the push tag, the
attest_image input, and the independent re-verify command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st is green

Run 25542801315 verified the attest+verify path end-to-end. Reverting to
workflow_dispatch only so the test stops auto-firing and remains as an
on-demand regression check after merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run 25542801315 captured the verified attestation evidence on PR #228;
keeping the workflow would just push throwaway test images on every
manual dispatch. The composite action lives at actions/attest_image
and can be re-tested in any future change by re-adding this workflow
file ad-hoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end smoke against private UnifiedID2/uid2-test-source surfaced a
real gh CLI limitation: attestations signed by GitHub's internal Sigstore
instance (used for private repos) fail verification with
'Error: verifying with issuer "GitHub, Inc."'. Tried --no-public-good,
--bundle-from-oci, --cert-oidc-issuer combinations; same result.

Signing and upload still succeed (bundle lands in both the attestations
API and the OCI registry), so external verifiers remain authoritative.
Demote the in-CI verify failure to a warning for private repos only;
public repos still hard-fail on verify mismatch as Jon's review #2
intended.

Evidence: UnifiedID2/uid2-test-source actions run 25643422322 — full
shared-publish-to-docker-versioned.yaml chain green (setup → buildx →
vulnerability_scan → push → attest_image sign+upload → shared_create_releases
draft), attestation signed for ghcr.io/.../uid2-6764-smoke@sha256:05058e77...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
actions/attest@v4.1.0 sets create-storage-record:true by default, which
calls GitHub's artifact-metadata API to cross-link the signed attestation
to the build artifact. Without artifact-metadata:write the call returns
403 and the run logs 'Failed to persist storage record'.

The storage record itself is independent of the signature/upload chain
(those still succeed), but it powers the "Attestations" tab in the
GitHub UI and surfaces attestations for org-wide policy/discovery.
Required on the callee's job permissions block too; reusable workflows
take the intersection of caller and callee permissions.

Consumer rollout PRs receive a matching grant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous "private-repo verify caveat" misdiagnosed cli/cli#9045:
the failure isn't about the GitHub-internal Sigstore CA, it's that the
cert SAN of a reusable-workflow signer doesn't match the default
--owner regex. Add --cert-identity-regex to accept either signer
pattern (reusable workflow under IABTechLab/uid2-shared-actions, or
composite-action caller under ${{ github.repository }}), and remove
the warning demotion so verify hard-fails uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants