One CI shape: build an image by digest, Syft SBOM, Cosign sign + SBOM attest, verify success, and prove wrong-key/unsigned failure — the operational follow-on to after-xz.
9 min read
id-token: write) — or a local key when OIDC is blocked, labeled honestly.Related reading lives on the xz-utils backdoor technical deep dive (narrative context) and Not Git Yard / fuzz-based defense (detection complementary to signing). The after-xz solo-team checklist is the “why” companion.
Related links:
Lab honesty (29 Sep 2026 IST): Docker and GHCR were unavailable on the lab box. We ran a throwaway local OCI image on 127.0.0.1:5000 (distribution/registry 3.0.0 + crane 0.20.2): digest sha256:de23658418d8f8b507e13cb2f36e45d6fc394b2015b5e2671c74e3a656f36f91. Syft 1.18.1 produced SPDX/CycloneDX. Cosign v2.4.1 keyless hit Sigstore device-flow OIDC and timed out (no browser / no Actions id-token). Local key sign+verify succeeded (Rekor 3003586630); SBOM attest Rekor 3003588212; wrong-key exit 12; unsigned exit 10. Attestation path for v1: Cosign attest (not actions/attest).
A practical Cosign + SBOM gate is: build by digest → Syft SBOM → Cosign sign → Cosign attest SBOM → verify expected key/identity → deliberately fail wrong-key/unsigned. Signatures without a verify gate are theater.
| Goal | Pass criteria | Lab 29 Sep 2026 IST |
|---|---|---|
| Sign | Image digest has a Cosign signature | Local key → Rekor 3003586630 |
| SBOM | SPDX or CycloneDX from the image | Syft 1.18.1 → stdlib @ go1.24.4 + module |
| Attest | SBOM attestation attached | Cosign attest SPDX → Rekor 3003588212 |
| Verify | Expected key/identity succeeds | cosign verify --key lab08.pub → exit 0 |
| Fail demo | Wrong key / unsigned / wrong-identity fails | exits 12 / 10 / 12 |
If you only push latest unsigned, stop reading tutorials and start here.
Sigstore’s Cosign overview explains identity-based (“keyless”) signing: Fulcio issues a short-lived certificate binding an ephemeral key to an OIDC identity; Rekor records the signing event.
Related links:
In GitHub Actions you need (OIDC in Fulcio; CI quickstart):
Related links:
Verify with identity + issuer, not “a signature exists”:
certificate-oidc-issuer: Open token.actions.githubusercontent.comcertificate-identity: Open github.comLab note: Keyless did not complete here (device-flow OIDC, exit 124 after 25 s). The identity URI above is the CI target, not a measured Fulcio subject from this box. What we measured was local-key verify against lab08.pub.
Inventory ≠ malware scan. Still non-optional for release hygiene.
Pinned in lab: Syft 1.18.1.
| Package (SPDX) | Version |
|---|---|
shoppercove.local/lab08-throwaway | (devel) |
stdlib | go1.24.4 |
| image root | digest sha256:de236584… |
Official tooling: anchore/syft. We attached the SBOM as a Cosign attestation and kept the JSON as a lab artifact.
Related links:
Lab proved the sign / SBOM / attest / verify / fail loop on a local OCI digest with a local Cosign key. The YAML below is the GitHub Actions keyless target for when you have id-token: write + a registry. Pin action SHAs in production; replace ORG/APP.
Attestation path for this article’s lab: Cosign attest with --type spdxjson. We did not run actions/attest (no Actions runner). Prefer one verify story you will actually run. GitHub documents the alternate path via actions/attest.
Related links:
Local-key equivalent we ran (when keyless is blocked):
Label local keys honestly: they prove you hold the private key, not that GitHub OIDC minted a Fulcio cert for a workflow identity.
| Case | Expected exit | Lab result |
|---|---|---|
| Correct local key | 0 | 0 |
| Wrong key | ≠0 | 12 (PEM mismatch / no matching signatures) |
| Unsigned digest | ≠0 | 10 (no signatures found) |
| Wrong certificate-identity (on key-signed image) | ≠0 | 12 (nil certificate provided) |
Wire the wrong-key / wrong-identity command so the job fails closed — signatures without a verify gate are theater.
Signing in CI is useless if CD pulls by mutable tag without verify.
Minimum for a solo team:
cosign verify in CD or admission policy.We simulated the reject with an unsigned sibling digest (sha256:3e64b43d…) → verify exit 10. Kubernetes admission (Kyverno/Gatekeeper/Ratify/etc.) is optional for v1; a shell verify in CD counts.
| Claim to avoid | Why |
|---|---|
| “Signed = safe code” | Identity of builder ≠ correctness of logic |
| “SBOM finds backdoors” | Inventory only |
| “We would have stopped xz automatically” | Signing your own image ≠ detecting malicious upstream build scripts; keep tarball diffs + fuzz/Lily for that class |
“Pinning @v4 actions is enough” | Prefer SHA pins for actions in production |
| “This lab ran keyless on GHCR” | It did not — local OCI + local key; keyless blocked on device-flow OIDC |
Cross-link the after-xz checklist for tarball≠git; cross-link Lily / fuzz-based defense for behavioral detection.
Related links:
| Step | Artifact / signal | Lab status |
|---|---|---|
| Throwaway image by digest | 127.0.0.1:5000/lab08/throwaway@sha256:de236584… | Done (local OCI; not GHCR) |
| Syft SBOM | lab08-throwaway.spdx.json (stdlib go1.24.4) | Done |
| Cosign sign | Rekor 3003586630 | Done (local key) |
| Verify success | exit 0 | Done |
| Verify fail | wrong-key 12, unsigned 10 | Done |
| Attestation | Cosign attest SPDX, Rekor 3003588212, verify-attestation 0 | Done |
| Keyless OIDC | device-flow timeout exit 124 | Blocked (honest) |
Time budget this afternoon: tools + image + SBOM + sign/verify/attest loop on the order of minutes once binaries were present; first-time GHCR + GHA keyless wiring was not timed.
Cosign vs actions/attest — pick one?
Cosign is portable across registries and non-GitHub verifiers. actions/attest integrates tightly with GitHub’s attestation store + gh attestation verify. This lab used Cosign attest only. Start with one verify story you will actually run.
Do private repos use public Rekor?
GitHub’s attestation docs note public-good Sigstore for public repos and GitHub’s private Sigstore instance for private/internal. Confirm for your org settings. Our local-key signatures still wrote public Rekor entries (indexes 3003586630 / 3003588212).
Why verify SBOM with an explicit type?
cosign verify-attestation --type spdxjson (or gh attestation verify with --predicate-type) must not accidentally treat an SBOM attestation as build provenance.
Is keyless safe without long-lived keys?
It removes laptop signing keys; you must still protect workflow permissions, environment approvals, and who can change the signing workflow. When keyless is blocked, a local key is acceptable for a lab only if you label it as not an OIDC identity proof.
Can I skip SBOM if I sign?
You can ship unsigned inventory gaps. Signing proves who built the bits; SBOM tells you what is inside. Different questions.
How does this relate to Lily?
Cosign answers “who built this digest?” Lily-style fuzzing answers “does this change introduce triggerable malicious behavior?” Use both when feasible. See Not Git Yard / fuzz-based defense.
Related links:
Signing proves who built the image digest. An SBOM attestation attaches a signed inventory of what is inside it. Signing alone doesn't tell you the contents, and an SBOM is not a malware scan.
permissions: id-token: write so the workflow can mint an OIDC token for Sigstore's Fulcio, which issues a short-lived certificate. Rekor records the signing event. Verify with --certificate-identity and --certificate-oidc-issuer.
No. Signing your own image proves who built it, not that upstream code is clean. The post pairs it with tarball-vs-git diffs and fuzz-based checks for that class of attack.
Lab evidence
Lab 29 Sep 2026 IST. Local OCI image sha256:de23658418d8f8b507e13cb2f36e45d6fc394b2015b5e2671c74e3a656f36f91. Syft 1.18.1 SBOM (stdlib go1.24.4). Cosign v2.4.1 keyless blocked (OIDC); local-key sign Rekor 3003586630; SBOM attest Rekor 3003588212. Verify OK 0 / wrong-key 12 / unsigned 10 / wrong-identity 12.
Explore the product
Untested by us. Check what's included, the license and refund terms before you buy.
Turn the xz-utils backdoor lesson into action: tarball vs git diffs, SBOM, Sigstore Cosign, SLSA provenance, and a solo/small-team release gate you can run this week.
Summary29 Sept 202610 min read
Hands-on Lily (ASE 2026) on an owned C toy: rosa/lily 0.6.0 pin, directed catch of a localhost-bind trigger, clean refactor with zero flags, discovery miss, and CI cost math.
Summary30 Sept 20268 min read
Both cut a string at a separator, but they fail in different ways. We ran them on Python 3.13.5 to show where partition() is safer and where split() is the right tool.
Summary2 Nov 20263 min read