What builder identity proofs must be verified before an agent pulls a container image from a public registry?

**Date of Record:** 2026 October 26 **Scope:** Applies when an agentic session pulls a container image (Docker, OCI) from a public registry (Docker Hub, Quay, GHCR) and intends to execute it in a production or security-sensitive environment. Does not apply to local, air-gapped registries with strict access control or development-only environments.

Core Distinction

The separation between **image tag** (the human-readable label like `latest` or `v1.2.3`) and **builder identity proof** (the cryptographic attestation of which specific CI/CD runner built the image, with which source code commit, under which pipeline). Relying solely on tags creates the **Registry Identity Crisis**: an attacker compromises a maintainer's account, pushes a malicious image under the same tag, and the agent pulls and executes it because the tag matches what the documentation says. Builder identity proofs bind the image to its actual origin.

What Was Observed

In container-based agent architectures, failing to verify builder identity produces three primary failure modes:

  1. **Tag Hijacking:** An attacker compromises a public registry account and replaces the `v1.2.3` image with a malicious version. The agent pulls the tag and executes the attacker's code, thinking it is the legitimate version.
  2. **Image Layer Injection:** The attacker adds a malicious layer to an existing image without modifying the tag. The agent pulls the image, and the malicious layer executes a backdoor.
  3. **Provenance Replay:** The attacker captures a legitimate image and its provenance attestation from a previous build. They replay both during a new build, making the malicious image appear legitimate.

Recorded Elements

  1. **`slsa_provenance_attestation`:** The SLSA Level 3+ provenance record, cryptographically binding the image digest to the source repository, commit hash, and build instructions.
  2. **`builder_identity_certificate_hash`:** The cryptographic proof of the CI/CD runner's identity (e.g., a SPIFFE ID or Sigstore Fulcio certificate), ensuring the image was built by a trusted pipeline, not a developer workstation.
  3. **`source_commit_hash`:** The exact Git commit hash of the source code used to build the image, allowing the successor to verify the image corresponds to a known-good codebase.
  4. **`signature_verification_receipt`:** The cryptographic proof that the image's signature was verified against the trusted root CA and that the image digest matches the signed attestation.

Condition of Validity

* **Applies when:** The image is pulled from a public or shared registry, and the agent will execute it with access to sensitive data, network, or compute resources. * **Does not apply when:** The image is pulled from a strictly private, air-gapped registry with hardware-rooted access controls, or the image is used only for read-only data processing with no execution privileges.

Decision Boundary

  1. **Is the image signed with a valid SLSA provenance attestation?** If no, the image's origin is unverified. The agent must not execute it.
  2. **Is the `builder_identity_certificate_hash` in the trusted CI/CD runner list?** If no, the image may have been built by an untrusted source. The agent must halt.
  3. **Does the `source_commit_hash` match a known-good commit in the repository?** If no, the image may be built from malicious or compromised code.
  4. **Conversely, when can an agent execute an unsigned image?** Only in a strictly sandboxed environment with no network access, no persistent storage, and no access to sensitive credentials.

Operational Effect

  1. **Supply Chain Immunity:** Prevents attackers from hijacking tags or injecting malicious layers by requiring cryptographic builder identity proofs.
  2. **Provenance Audit:** Enables the successor to trace every container back to its source commit and CI/CD runner, satisfying modern software supply chain security requirements.
  3. **Deterministic Trust:** Replaces tag-based trust with cryptographic proof, ensuring the exact image the predecessor validated is the exact image the successor executes.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class ImageVerdict(Enum):
    VERIFIED_IMAGE = auto()
    UNSIGNED_IMAGE = auto()
    UNTRUSTED_BUILDER = auto()
    SOURCE_MISMATCH = auto()

@dataclass(frozen=True)
class ContainerImage:
    image_digest: str
    slsa_provenance_hash: str | None
    builder_cert_hash: str | None
    source_commit_hash: str | None

@dataclass(frozen=True)
class VerificationPolicy:
    trusted_builders: frozenset[str]
    allowed_source_commits: frozenset[str]

@dataclass(frozen=True)
class ImageManifest:
    verdict: ImageVerdict
    directive: str
    manifest_digest: str

class ContainerIdentityGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_image(
        cls, image: ContainerImage, policy: VerificationPolicy
    ) -> ImageManifest:
        
        # Gate 1: SLSA provenance presence
        if not image.slsa_provenance_hash:
            return ImageManifest(
                verdict=ImageVerdict.UNSIGNED_IMAGE,
                directive="HALT: No SLSA provenance. Image origin unverified.",
                manifest_digest=cls._hash(f"NO_PROVENANCE:{image.image_digest}")
            )

        # Gate 2: Builder trust
        if not image.builder_cert_hash or image.builder_cert_hash not in policy.trusted_builders:
            return ImageManifest(
                verdict=ImageVerdict.UNTRUSTED_BUILDER,
                directive=f"HALT: Builder {image.builder_cert_hash} is not trusted.",
                manifest_digest=cls._hash(f"UNTRUSTED_BUILDER:{image.image_digest}")
            )

        # Gate 3: Source commit verification
        if image.source_commit_hash not in policy.allowed_source_commits:
            return ImageManifest(
                verdict=ImageVerdict.SOURCE_MISMATCH,
                directive=f"HALT: Source commit {image.source_commit_hash} is not in the allowed list.",
                manifest_digest=cls._hash(f"SOURCE_MISMATCH:{image.source_commit_hash}")
            )

        # Gate 4: Verified
        digest = cls._hash(json.dumps({
            "digest": image.image_digest,
            "builder": image.builder_cert_hash,
            "source": image.source_commit_hash
        }, sort_keys=True))
        
        return ImageManifest(
            verdict=ImageVerdict.VERIFIED_IMAGE,
            directive="PROCEED: Container image origin and builder identity verified.",
            manifest_digest=digest
        )