CoursesCompliance as codeAudit trails & attestation

Audit trails & attestation

Tamper-evident, attested, reusable evidence.

Expert30 min · lesson 9 of 15

An auditor opens your SOC 2 (Service Organization Control 2, the audit report your enterprise customers ask for) evidence folder and finds hundreds of Conftest and OPA (Open Policy Agent, the engine those policy checks run on) results. All green. All timestamped. Then comes the one question that flattens the whole pile: "How do I know this file describes the image you actually deployed, and that nobody flipped a FAIL to a PASS afterwards?" The finding they wrote up was blunt. Evidence collection is automated, but the pipeline runs zero attestation commands. Nothing ties a verdict to the artifact it judged, and nothing makes tampering visible. Evidence that anyone with write access can quietly edit is a spreadsheet, not proof. This lesson closes that hole by turning a compliance result into a signed, publicly logged in-toto attestation an auditor can check without trusting you at all.

From evidence to attested evidence

A notarized letter beats a photocopy for one reason. Someone you both trust stamped it, tied that stamp to that exact page, and wrote the act into a register anyone can look up later. An attestation does the same job for software. It is a signed, machine-checkable statement about one specific artifact. The format the industry settled on is an in-toto Statement, a small JSON (plain-text data) wrapper with three parts. The subject is the artifact, named by its content digest (a sha256 fingerprint of the actual bytes, never a movable tag). The predicateType is a URI (a web address used as a label) saying what kind of claim this is. The predicate is the claim body itself, here your control-by-control compliance results. Cosign wraps that Statement in a DSSE envelope (Dead Simple Signing Envelope), which signs the payload together with its type, so a signature over compliance results can never be replayed to mean something else. Signing is keyless. Instead of keeping a long-lived private key that can leak, cosign trades your CI (continuous integration) system's OIDC (OpenID Connect, the standard that lets one service prove who it is to another) token for a certificate from Fulcio that lives about ten minutes, uses it once, and throws the key away. It then writes the signature into Rekor, Sigstore's public append-only log. Rekor is the courthouse register in that analogy. Every entry is timestamped and carries an inclusion proof, so nothing can be altered, backdated or removed without the change showing. Five words carry the entire trust chain: subject, predicate, DSSE, Fulcio, Rekor.

Step 1: produce the compliance predicate

Start from a real evaluation, not a hand-written summary. Your Conftest run over a rendered manifest already emits a pass or fail for every rule. Capture that as a structured file keyed to control IDs, and pin it to the exact image digest you are about to sign. Keep it small and deterministic, so two runs over the same manifest produce the same bytes. This file is the durable record an auditor reads once the signature is decoded, which means it has to make sense on its own, months later, to someone who has never seen your repository.

compliance.json — the evidence predicate
{
"control_set": "CIS Kubernetes Benchmark v1.9",
"evaluated_at": "2026-07-14T09:12:44Z",
"evaluator": "conftest 0.56.0 / opa 0.68.0",
"subject_digest": "sha256:5b21e0c1f4c9a4d3e77b2a1f9c0e8d6b4a2c1e0f9d8c7b6a5e4f3d2c1b0a9f8e",
"results": [
{ "control": "5.2.7", "requirement": "Minimize root containers", "status": "PASS" },
{ "control": "5.2.2", "requirement": "Disallow privileged containers", "status": "PASS" },
{ "control": "5.7.3", "requirement": "Apply a SecurityContext", "status": "PASS" }
],
"summary": { "pass": 3, "fail": 0 }
}

Step 2: sign it as an in-toto attestation

Install cosign, turn the tag into the digest it currently points at, then attest that digest. Attesting a tag is close to worthless, because a tag is a sticky note that anyone with push access can peel off and stick on a different image. In CI there is no secret to manage. Cosign reads the runner's OIDC token, gets a Fulcio certificate bound to the workflow identity, wraps compliance.json in an in-toto Statement of predicateType custom, signs the DSSE envelope, and uploads the entry to Rekor. The tlog (transparency log) index printed at the end is your permanent public receipt, and anyone can look that number up years later. COSIGN_YES=true answers the transparency-log privacy notice for you, so an unattended pipeline does not sit there forever waiting for someone to type y.

install cosign, pin the digest, attest (keyless)
# 1. install a pinned cosign (Linux amd64) — never 'latest' in CI
curl -sSfL -o cosign \
https://github.com/sigstore/cosign/releases/download/v2.4.1/cosign-linux-amd64
chmod +x cosign && sudo mv cosign /usr/local/bin/cosign
cosign version | head -1
# 2. resolve tag -> immutable digest, then attest that digest
IMAGE=registry.acme.dev/payments/api:1.4.2
DIGEST=$(crane digest "$IMAGE") # sha256:5b21e0c1...
REF="registry.acme.dev/payments/api@${DIGEST}"
COSIGN_YES=true cosign attest \
--predicate compliance.json \
--type custom \
"$REF"
stdout — attestation signed and logged to Rekor
Generating ephemeral keys...
Retrieving signed certificate...
The sigstore service, hosted by sigstore a Series of LF Projects, LLC, is provided pursuant to the Hosted Project Tools Terms of Use, available at https://lfprojects.org/policies/hosted-project-tools-terms-of-use/.
Note that if your submission includes personal data associated with this signed artifact, it will be part of an immutable record.
This may include the email address associated with the account with which you authenticate your contractual Agreement.
This information will be used for signing this artifact and will be stored in public transparency logs and cannot be removed later, and is subject to the Immutable Record notice at https://lfprojects.org/policies/hosted-project-tools-immutable-records/.
By typing 'y', you attest that (1) you are not submitting the personal data of any other person; and (2) you understand and agree to the statement and the Agreement terms at the URLs listed above.
tlog entry created with index: 158843201

Step 3: verify the signed evidence chain

A doorman who confirms that your ID card exists has checked nothing. The one who reads the name on it and the government that issued it has. Verification works the same way. The question is whether this was signed by the identity you trust, over the image you are about to run, and whether the signature sits in the public log. cosign verify-attestation checks the DSSE signature against Fulcio's root, confirms an inclusion proof exists in Rekor (offline, from the proof bundled with the signature), and accepts only certificates whose identity and issuer match the constraints you hand it. Pin --certificate-identity to your exact release workflow and --certificate-oidc-issuer to your OIDC provider. Any mismatch exits non-zero, which is what lets this one command sit in a deploy gate and stop a release. On success it prints the DSSE envelope, and you decode the payload to read which controls passed. Fulcio and Rekor are public, so an auditor can run the same command from their own laptop. That is why they accept the answer. Nothing about it depends on trusting you or your infrastructure.

verify by digest, gate on exit code, read the verdict
cosign verify-attestation \
--type custom \
--certificate-identity 'https://github.com/acme/payments/.github/workflows/release.yml@refs/tags/v1.4.2' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$REF" > attestation.json
echo "verify exit code: $?"
# the custom predicate is nested as .predicate.Data (a JSON string) — decode it
jq -r '.payload | @base64d | fromjson
| .predicate.Data | fromjson | .summary' attestation.json
stdout — chain verified, verdict decoded
Verification for registry.acme.dev/payments/api@sha256:5b21e0c1... --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority certificates
Certificate subject: https://github.com/acme/payments/.github/workflows/release.yml@refs/tags/v1.4.2
Certificate issuer URL: https://token.actions.githubusercontent.com
verify exit code: 0
{
"pass": 3,
"fail": 0
}
Verification without a pinned identity accepts any Sigstore signature
Run cosign verify-attestation without --certificate-identity and --certificate-oidc-issuer and it will accept any certificate Fulcio has ever issued. An attacker who signs a forged compliance predicate with their own GitHub OIDC identity walks straight through your check. Pin both flags to your exact release workflow and issuer. The -regexp variants are fine only with anchored patterns you control. A correct rejection reads 'Error: none of the expected identities matched what was in the certificate' and exits 1, which is precisely the failure that should stop the deploy.
How an auditor verifies the attestation
cosign verify-attestation --type custom <image@sha256:...>
checks identity, transparency log and subject digest
identity ≠ pinned
reject · exit 1
without --certificate-identity any Fulcio cert would pass
not in Rekor
reject · exit 1
no transparency-log inclusion proof, so tampering would not show
digest ≠ running image
reject · exit 1
the attestation describes a different artifact than the deployed one
all checks pass
trust the predicate
decode the DSSE payload, read control results, allow the deploy
The signature ties a verdict to a digest, Rekor makes tampering visible, and identity pinning decides whose verdict counts. All three have to hold.
Quick check
01Your pipeline attests registry.acme.dev/payments/api:1.4.2 and verify-attestation exits 0. Why is that still not proof the running pod is compliant?
Incorrect — Keyless is the stronger audit story, not the weaker one. A ten-minute Fulcio certificate plus a public Rekor entry beats a long-lived key that can leak without anyone noticing.
Correct — The attestation's subject is a content digest, and a tag is a pointer someone can move to an unattested image. Bind, deploy and verify by @sha256 and the chain stays intact.
Incorrect — It verifies the signature and the Rekor inclusion proof, and you decode the predicate yourself with jq to read the verdicts. The gap in this scenario is tag versus digest.
Incorrect — Exit 0 means the signature, the certificate and the transparency-log inclusion all checked out. What is shaky here is what the tag points at, not the exit code.
02Your deploy gate runs cosign verify-attestation with --type custom and the image digest, but nobody passed --certificate-identity or --certificate-oidc-issuer. What is that gate actually enforcing?
Incorrect — Fulcio issues a certificate to anyone who can present a valid OIDC token. Without a pinned identity the gate has no opinion about which repository or which person signed.
Correct — Identity pinning is the part that says whose verdict counts. Leave it out and the gate proves a signature exists and nothing about who produced it.
Incorrect — cosign runs happily without them, and that is what makes this dangerous. The gate stays green and looks like it is doing its job.
Incorrect — The inclusion proof is still verified offline from the bundle, and the entry was logged at signing time. The only missing piece is the identity and issuer constraint.
03A release fails its deploy gate. cosign verify-attestation prints 'Error: none of the expected identities matched what was in the certificate' and exits 1. What do you do next?
Incorrect — That converts the gate into a check any Fulcio certificate satisfies, which is the exact hole the lesson warns about. Never unpin to unblock a release.
Correct — The pinned identity includes the workflow ref, so shipping a new tag changes the certificate subject. Confirm the reported subject really is your release workflow, then pin the identity for the tag you are shipping.
Incorrect — The message is about certificate identity, not the predicate. A malformed predicate would not produce an identity mismatch.
Incorrect — Rekor is append-only, so entries cannot be removed. The log inclusion is not what failed here anyway.

A verified attestation gives you a strong, reusable, auditor-acceptable fact: this control passed at build time, against this exact digest, signed by this workflow. What it says nothing about is 3 a.m. three weeks later, when someone widens a security group by hand or patches a live Deployment in place. The distance between the digest you attested and the state actually running is itself a compliance finding, and the next lesson, Drift as a compliance signal, shows how to collect it.

Try this

Work through “Step 3: verify the signed evidence chain” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: verification without a pinned identity accepts any Sigstore signature. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related