Skip to content
About

A valid signature is not enough

Imagine a container image with a cryptographically valid signature from a stranger’s workflow. Its signature may be correct, yet the deployment should still be refused. Now imagine the expected workflow signs a malicious image after its source or build logic is compromised. Its identity may also be correct. The phrase “signed image” leaves both problems unresolved.

A useful verification decision has to name the artifact, authenticate the signer, evaluate the claims required by the consumer, and operate inside an explicit deployment scope. This project combines an image signature, two attestation types, CI assertions, reviewed digest selection, and admission policy. Each adds information; none converts authenticated bytes into a universal safety guarantee.

The signing workflow invokes cosign sign --yes against the immutable image reference. Keyless signing authenticates a workflow identity through Sigstore’s certificate mechanism. The baseline expects the certificate subject to identify the canonical sign-attest.yml reusable workflow on refs/heads/main, with GitHub Actions as OIDC issuer.

The signature is useful because it connects those authenticated signing credentials to the artifact digest. A verifier that checks only “some signature exists” would allow identities outside the intended authority. CI therefore supplies the expected certificate identity and issuer to Cosign, then checks the signature payload’s docker-manifest-digest against the input digest. Kyverno uses the corresponding keyless subject, issuer, Rekor URL, and digest verification configuration.

The signer here is the GitHub workflow identity. The GCP service account used to access Artifact Registry is a separate authorization principal. Conflating the two would obscure which system grants storage access and which system authenticates the signed statement.

An attestation is a signed statement about a subject. In this baseline, the image digest is the subject, while the predicate provides structured data. Signing the statement protects its origin and integrity; a consumer must still decide which predicate type and contents matter.

The SBOM attestation uses SPDX JSON, identified by https://spdx.dev/Document. Syft scans the final image selected by digest. The authenticated statement says the expected workflow attached that inventory to this artifact. CI decodes the attestation, checks predicate type and subject digest, and reports the package count. Admission requires an attestation of the expected type from the approved identity, but does not parse package content or apply vulnerability policy to it.

That is an origin-and-presence check. It does not prove that every package was discovered, that a dependency is benign, or that the inventory contains no vulnerabilities. Scanner findings and accepted exceptions are separate inputs to release risk. The separate non-blocking CycloneDX/VEX job is not the SPDX claim admission requires.

Provenance is structured testimony from the workflow

Section titled “Provenance is structured testimony from the workflow”

The workflow creates a SLSA v0.2 provenance predicate and then signs it with Cosign. It writes a builder identifier, source URI, source commit, workflow entrypoint, invocation parameters, and a source material entry. The reusable-workflow entrypoint is derived from the OIDC token’s job_workflow_ref claim, rather than assuming the top-level calling workflow is the signer.

This prevents a specific implementation mismatch: the top-level Deploy workflow orchestrates jobs, while sign-attest.yml creates and authenticates the provenance. The local regression fixture checks that Kyverno’s JMESPath expressions address the predicate body and expect that actual entrypoint. Neither the presence of a provenance document nor the use of a SLSA schema establishes a SLSA assurance level. The predicate is generated by trusted workflow code; its accuracy still depends on that workflow and runner.

CI and admission ask overlapping questions

Section titled “CI and admission ask overlapping questions”
CI and admission check different contracts
CI and admission check different contractsBoth paths check the trusted keyless identity and digest-bound artifact claims. CI additionally matches the invocation source commit and source material against github.sha. Admission checks builder, workflow entry point and canonical source URI without that commit/material predicate. Shared workflow/policy authority is a correlated failure dependency; these are not automatically independent roots.Digest & signerCI + commitAdmission subsetResidual trust
Both trust the same workflow identity; admission does not repeat CI source commit/material matching.
  1. Both paths check the trusted keyless identity and digest-bound artifact claims.
  2. CI additionally matches the invocation source commit and source material against github.sha.
  3. Admission checks builder, workflow entry point and canonical source URI without that commit/material predicate.
  4. Shared workflow/policy authority is a correlated failure dependency; these are not automatically independent roots.

Solid arrows indicate the stated handoff, not a claim of independent trust. Optional relationships are described in the text equivalent. Historical components are labeled in the caption.

Property CI verification Admission policy
Expected signer and issuer Cosign certificate expectations Keyless attestors
Selected artifact digest Explicit signature and attestation subject comparisons verifyDigest: true and required verification
SPDX attestation Type, authenticated origin, subject digest, package count Type and authenticated origin; no package-content conditions
Provenance predicate type SLSA v0.2 SLSA v0.2
Builder https://github.com/actions/runner Same expected builder
Entrypoint .github/workflows/sign-attest.yml Same expected entrypoint
Source URI Canonical main source URI Same expected source URI
Exact source commit Compared with CI’s github.sha No explicit source-commit condition
Source material commit Canonical source material and commit required No material condition
Namespace and registry scope Candidate passed into the workflow Matching application images outside listed namespace exclusions

CI knows which source commit the candidate run is meant to describe. Admission evaluates the selected image at a later request boundary. It does not obtain CI’s expected source commit from a release record in this baseline. The two verifiers therefore cannot be described as identical checks at two locations.

Checking again at admission matters because the submitted image might differ from the artifact CI verified, or a workload might reach the API through another route. Admission evaluates the actual request under its own match rules. However, both checks trust the same workflow identity and core claim producers. Repetition protects different handoffs; it does not create an independent build authority.

A signed artifact still needs release permission

Section titled “A signed artifact still needs release permission”

After verification, a candidate digest exists with supporting claims. In the GCP edition, a reviewed Helm-value change selects it for Argo CD. A correctly signed but unselected digest has no automatic promotion path from the workflow. Argo CD reconciles the selection, and Kyverno evaluates matching Pod images.

The distinction matters operationally. Fixing a wrong digest selection is a GitOps decision; fixing an unexpected certificate subject is a trust-contract issue; fixing registry access is an IAM issue. Broadening a certificate identity to make a failing release pass would alter the trust decision, not merely repair access.

recorded-live-validation · accepted

The pictured CI verification accepted authenticated SPDX and SLSA v0.2 claims for its digest.

Expected in this setup: Authenticated claims identify the expected digest, signer, issuer, builder, entrypoint and source.

Recorded result: Decoded verification output shows SPDX packageCount 115 and provenance builder/entryPoint/source/sourceCommit fields.

Historical GCP, europe-west1; current environment not checked · observed 2026-08-23 · source revision 27a94b0

Historical CI output for a0073f8f…; verification confirms the captured claims, not an entire supply-chain assurance level.
Historical CI output for a0073f8f…; verification confirms the captured claims, not an entire supply-chain assurance level. · Select the image to read it at full resolution.

Transcript / inspected observation

Selected visible output:
predicateType: https://spdx.dev/Document
packageCount: 115
subjectDigest: sha256:a0073f8f1d73f62ab0a15634a48387e78f6f837cff80f27a5c6e3b0a5c1eb16a
predicateType: https://slsa.dev/provenance/v0.2
builder: https://github.com/actions/runner
entryPoint: .github/workflows/sign-attest.yml
source: git+https://github.com/devSatym/gcp-supply-chain-security@refs/heads/main
sourceCommit: 27a94b066cf28b149b682f981b4e7479fa0c4065

What this does not establish

  • Package count is this capture's output, not a measure of SBOM completeness or package safety.
  • SLSA v0.2 is a schema label; no assurance level established.
  • This digest differs from the later live-acceptance test digest.

Visual review of original capture. Collection capture date attributed to docs/my-validation/README.md; this task did not rerun its cloud experiment. Unmodified original. Public workflow identity, source commit, digest and historical registry scope retained. No credential or personal workstation path visible.

View the related screenshot in the complete gallery →

Inspect the untouched, commit-pinned original →

The approved historical CI screenshot shows authenticated SPDX and SLSA claim verification for the artifact visible there. Its revision and digest differ from the live-acceptance capture. The temporary wrong-trust test shows that the known signed live-test digest was denied when expected signer and source were changed together. That is one combined experiment. It does not isolate subject mismatch from provenance mismatch across separate controlled requests.

recorded-live-validation · rejected

A known signed test digest was rejected when one temporary policy changed both signer subject and provenance source expectations.

Expected in this setup: Reject a known signed image when the configured trust expectations do not match its claims.

Recorded result: The request was denied with subject mismatch and provenance attestation-check failure; temporary policy deletion is visible.

Historical GCP, europe-west1; current environment not checked · observed 2026-08-23 · source revision unknown; not inferred from the documentation baseline

A temporary policy with changed signer/source expectations rejects a known signed digest and is then deleted.
A temporary policy with changed signer/source expectations rejects a known signed digest and is then deleted. · Select the image to read it at full resolution.

Transcript / inspected observation

Selected visible output:
clusterpolicy.kyverno.io/test-invalid-trust-expectations created
resource Pod/default/test-invalid-trust was blocked
reject-wrong-keyless-identity: subject mismatch
expected .../.github/workflows/not-the-signing-workflow.yml@refs/heads/main
received .../.github/workflows/sign-attest.yml@refs/heads/main
reject-wrong-provenance-source: image attestations verification failed, verifiedCount: 0, requiredCount: 1; attestation checks failed
RESULT: PASS - invalid identity/provenance DENIED as expected
clusterpolicy.kyverno.io "test-invalid-trust-expectations" deleted
CLEANUP: Temporary ClusterPolicy removed

What this does not establish

  • One combined experiment changes two expected fields; not two isolated proofs.
  • The signer itself is not changed and the image is not shown being tampered with.
  • The transcript uses ellipses only for repeated canonical repository prefixes and omits incidental terminal metadata.

Visual review of original capture. Collection capture date attributed to docs/my-validation/README.md; this task did not rerun its cloud experiment. The original screenshot is approved for display at the owner’s explicit request for all evidence images and subsequent Cloudflare publication. Original terminal metadata remains visible. No additional pixel redaction was selected or applied. The selected transcript omits incidental prompts without changing the recorded result.

Read the approved selected-output transcript →

View the related screenshot in the complete gallery →

Inspect the untouched, commit-pinned original →

There is no recorded live test here for every absent attestation, malformed predicate, stale commit, compromised approved workflow, or unavailable registry. Those remain distinct gaps. A trusted workflow could honestly sign vulnerable code or dishonestly generate claims; the expected subject would still match. Continue to how controls share a failure to understand that residual trust.