Skip to content
Container Management

Container Provenance for AI-Generated Builds: SLSA Attestations When the Source Is Half Human, Half Agent

How to issue SLSA Build provenance attestations on container images when an autonomous coding agent contributed to the Dockerfile, the build script, or the source — without losing the audit trail.

Advanced 9 min read Updated May 2026

SLSA Build provenance was designed for a world where humans wrote code and CI systems built containers. That world is gone. Today's container image likely descends from a Dockerfile that an agent rewrote on Tuesday, a package.json that an agent updated on Wednesday, and an entrypoint script that a human edited after an agent's first draft on Thursday.

The SLSA specification accommodates this — but only if you treat the agent as a first-class build participant, not as an invisible co-author. This article shows how to issue SLSA Build L3 provenance attestations on container images when coding agents are part of the source path.

What SLSA Build Provenance Promises

A SLSA Build attestation answers two questions about a container image:

  1. What were the inputs? Source repo, commit, builder configuration, build dependencies.
  2. Who produced it? The builder identity, hardened against forgery.

It does not, by default, answer "who wrote the source." That is the gap we have to close when agents are involved. The SLSA spec leaves room for it: the predicate.buildDefinition.resolvedDependencies array can carry arbitrary URIs and digests, and the predicate.runDetails.metadata block accepts builder-defined extensions.

The Failure Mode

Without agent attribution in the provenance, an audit trail looks like this:

Image: ghcr.io/myorg/api@sha256:9f3a...
Built from: github.com/myorg/api @ commit abc123
Builder: github-actions / build-and-push.yml @ run 4815162342
Status: SLSA Build L3, signed by GitHub OIDC

Looks clean. But commit abc123 was 60% authored by an agent prompted with "rewrite the Dockerfile to use distroless," and the provenance has no trace of that. When a CVE drops in a base image the agent chose, you cannot answer "did a human approve this base image?" without manually walking the git log.

The Fix: Source Provenance Inside Build Provenance

The cleanest approach is to compute a source provenance summary during the build and embed it in the SLSA Build attestation. Two pieces:

  1. A summary of which commits in the build's source range were agent-authored (signal: Co-Authored-By: trailer or commits made by a known agent identity).
  2. A list of which files changed by each agent commit, scoped to build-relevant paths (Dockerfile, *.lock, Containerfile, .dockerignore, build.sh, etc.).

Computing the Summary in CI

#!/usr/bin/env bash
# scripts/agent-provenance.sh — emits agent-provenance.json
set -euo pipefail

# Range = previous successful build's commit → HEAD
PREV_SHA="${PREV_SHA:-$(git rev-parse HEAD~1)}"
HEAD_SHA="$(git rev-parse HEAD)"

# Build-relevant paths only — adjust to match your repo layout
BUILD_PATHS=(Dockerfile Containerfile .dockerignore *.lock package-lock.json yarn.lock pnpm-lock.yaml requirements.txt Pipfile.lock go.sum Cargo.lock build.sh scripts/)

# Find every commit in range that touched a build-relevant path
mapfile -t COMMITS < <(git log --format='%H' "${PREV_SHA}..${HEAD_SHA}" -- "${BUILD_PATHS[@]}")

jq -n --arg prev "$PREV_SHA" --arg head "$HEAD_SHA" '
  {
    source_range: { from: $prev, to: $head },
    agent_commits: [],
    human_commits: []
  }
' > agent-provenance.json

for SHA in "${COMMITS[@]}"; do
  AUTHOR=$(git show -s --format='%an <%ae>' "$SHA")
  TRAILERS=$(git log -1 --format='%(trailers:only=true)' "$SHA")
  AGENT_TRAILER=$(echo "$TRAILERS" | grep -iE '^co-authored-by:.*(claude|copilot|cursor|codex|gemini|coding-agent)' || true)
  FILES=$(git diff-tree --no-commit-id --name-only -r "$SHA" -- "${BUILD_PATHS[@]}" | jq -R . | jq -s .)

  if [[ -n "$AGENT_TRAILER" ]]; then
    ENTRY=$(jq -n --arg sha "$SHA" --arg author "$AUTHOR" --arg agent "$AGENT_TRAILER" --argjson files "$FILES" \
      '{commit: $sha, author: $author, agent: $agent, build_relevant_files: $files}')
    jq --argjson e "$ENTRY" '.agent_commits += [$e]' agent-provenance.json > tmp && mv tmp agent-provenance.json
  else
    ENTRY=$(jq -n --arg sha "$SHA" --arg author "$AUTHOR" --argjson files "$FILES" \
      '{commit: $sha, author: $author, build_relevant_files: $files}')
    jq --argjson e "$ENTRY" '.human_commits += [$e]' agent-provenance.json > tmp && mv tmp agent-provenance.json
  fi
done

The output, agent-provenance.json, is a structured record of every build-relevant commit in the source range, partitioned by whether an agent contributed.

Embedding It in the SLSA Predicate

The Sigstore cosign toolchain accepts arbitrary in-toto attestations. After your image is built and pushed, attest the agent provenance separately and link it:

IMAGE="ghcr.io/myorg/api@${IMAGE_DIGEST}"

# Cosign reads the password (not the key) from $COSIGN_PASSWORD automatically.
# It does NOT support an env:// URI for the private key itself — supported
# --key schemes are file paths plus awskms://, gcpkms://, azurekms://,
# hashivault://, openbao://, and k8s:// (per the cosign key-management docs).
# In CI, write the key to a tmpfs file from a secret env var, then point
# --key at the file:
echo "$COSIGN_PRIVATE_KEY" > /dev/shm/cosign.key
trap 'rm -f /dev/shm/cosign.key' EXIT

# Agent provenance attestation (custom predicate type)
cosign attest \
  --predicate agent-provenance.json \
  --type "https://crashoverride.com/attestations/agent-provenance/v1" \
  --key /dev/shm/cosign.key \
  "$IMAGE"

# Standard SLSA Build provenance — slsa-github-generator emits this
# automatically (see workflow snippet below); we then explicitly link the
# two attestations with a separate cosign attest call so a verifier can
# match them by image digest at audit time.

To link the two attestations, attest the agent-provenance file's SHA-256 against the same image digest. The SLSA GitHub generator's container builder reusable workflow does not accept arbitrary extra inputs (its documented inputs are image, digest, registry-username, compile-generator, private-repository, continue-on-error, gcp-workload-identity-provider, gcp-service-account, provenance-registry-username, and provenance-registry — see slsa-framework/slsa-github-generator/internal/builders/container), so the linkage is done with a follow-on cosign attest:

# Build provenance: emitted by the slsa-github-generator container builder.
# The reusable workflow filename is generator_container_slsa3.yml (note the
# `generator_` prefix — there is no `builder_` workflow for this builder).
- name: SLSA Build provenance
  uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
  with:
    image: ghcr.io/myorg/api
    digest: ${{ steps.build.outputs.digest }}
    registry-username: ${{ github.actor }}
  secrets:
    registry-password: ${{ secrets.GITHUB_TOKEN }}

# Agent provenance: a separate signed in-toto attestation against the same
# image digest, so a verifier can fetch both attestations and confirm they
# describe the same artifact.
- name: Attach agent provenance
  env:
    IMAGE: ghcr.io/myorg/api@${{ steps.build.outputs.digest }}
  run: |
    cosign attest --yes \
      --predicate agent-provenance.json \
      --type "https://crashoverride.com/attestations/agent-provenance/v1" \
      "$IMAGE"
    # Keyless OIDC signing — no --key flag; cosign uses the GitHub Actions
    # OIDC token automatically (the workflow needs `id-token: write`).

The image now carries two attestations bound to the same digest: one SLSA Build predicate (https://slsa.dev/provenance/v1) emitted by the generator, and one custom agent-provenance predicate emitted by cosign attest. A verifier fetches both via cosign download attestation --predicate-type ... and reasons about agent involvement at audit time.

If you want the agent-provenance digest to appear inside the SLSA predicate's resolvedDependencies array (rather than as a sibling attestation), use the BYOB (Bring Your Own Builder) workflow, which supports custom predicate construction. The two-attestations approach above is simpler and equally verifiable.

Verifying at Deploy Time

A Kyverno policy that requires both a SLSA Build attestation and an agent-provenance attestation, and that fails-closed if any agent commit touched the Dockerfile without a human reviewer:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-agent-provenance
spec:
  validationFailureAction: Enforce
  webhookConfiguration:
    failurePolicy: Fail
  rules:
    - name: verify-slsa-and-agent-provenance
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestations:
            - type: "https://slsa.dev/provenance/v1"
              attestors:
                - entries:
                    - keyless:
                        subject: "https://github.com/myorg/*"
                        issuer: "https://token.actions.githubusercontent.com"
            - type: "https://crashoverride.com/attestations/agent-provenance/v1"
              attestors:
                - entries:
                    - keyless:
                        subject: "https://github.com/myorg/*"
                        issuer: "https://token.actions.githubusercontent.com"
              conditions:
                - all:
                    - key: "{{ agent_commits[?build_relevant_files[?contains(@, 'Dockerfile')]] | length(@) }}"
                      operator: Equals
                      value: 0

This policy enforces: any image whose source range contains an agent commit that touched the Dockerfile is rejected at admission. To accept such images, a human must first apply a human-reviewed: true label to the agent commit (a separate hook in your CI), which the policy can also check.

Why Not Just Tag Commits?

Tagging agent commits in git is necessary but not sufficient. The git tag lives in the source repo, which an attacker who has compromised CI can rewrite. The agent-provenance attestation is signed by the builder's keyless OIDC identity at build time and stored alongside the image — it is much harder to forge after the fact.

The two work together: the git trailer gives developers fast, local visibility; the signed attestation gives auditors and admission controllers cryptographic evidence.

Handling Edge Cases

  • Agent commits that touched non-build paths only — these still belong in human_commits for the purposes of build provenance. Build provenance asks "what produced this image"; agent edits to README do not.
  • Squash merges that hide the original agent author — preserve the trailer in the squash commit message. Configure git config trailer.co-authored-by.ifExists addIfDifferent in CI to prevent loss.
  • Agents committing as themselves (no trailer) — recognise their email or GPG key explicitly. Maintain a .agents file in the repo root listing known agent identities, and consult it during the provenance computation.
  • Multi-repo builds — run the provenance script in each source repo and concatenate. The SLSA predicate's resolvedDependencies can carry multiple agent-provenance files.

What Auditors Actually Ask For

When the EU AI Act or NIST SSDF auditors come knocking, the questions about an AI-coded container image have settled into a predictable shape:

  1. Was an autonomous agent involved in producing this artifact? Yes/no, by attestation lookup.
  2. If yes, which build-relevant files did it touch? List, by predicate query.
  3. Was each agent change reviewed by a human before the build? Yes/no, by review-attestation cross-reference.
  4. Is the attestation cryptographically tied to the image? Yes, by digest match.

The agent-provenance predicate answers all four in seconds. A verbose audit log answers them in days. The cost of getting this right at build time is low; the cost of reconstructing it during an audit is enormous.

Practical Rollout

  • Start by emitting the agent-provenance.json file as a build artifact only — no signing, no policy enforcement. Inspect a week of builds to calibrate which agents and trailer formats appear in your codebase.
  • Add cosign signing once the structure is stable. Verify the attestation with cosign verify-attestation in a non-blocking step.
  • Introduce the Kyverno policy in Audit mode (validationFailureAction: Audit). Watch for false positives on legitimate squash merges or rebases.
  • Switch to Enforce mode (validationFailureAction: Enforce) in non-prod first, then production, with a documented break-glass label for emergencies. Note: Enforce and Audit are case-sensitive — Kyverno rejects enforce, audit, or block.

The endpoint is a deployment pipeline where every container image carries cryptographic evidence of both what was built and who, human or agent, made the source decisions that led to it.


References

This article is part of the Container Management knowledge series (6 articles) Browse all Container Management articles →
Related Use Case

AI Code Traceability — Your developers don't write the code

Nobody has control anymore. Leaders have visibility.

Explore Use Case →