Agentic Covenants

Protect (PR) · Supply chain

Supply chain at the server side layer

external · Outside the agent entirely

If the agent decides to violate this concern, what stops it at this layer?

What this cell does

OCI signature verification (cosign), SBOM admission, egress NetworkPolicy, OPA attestation policy, MCP domain allowlist at network layer.

Artifacts (5)

build-and-sign.ymlview on GitHub
# ABOUTME: Builds the agent image, signs with cosign (keyless via GitHub OIDC), generates SPDX SBOM, attaches as attestation.
# ABOUTME: id-token: write is required for keyless signing. Drop in .github/workflows/. Match the OIDC subject in the Kyverno verifyImages policy.
name: Build and sign agent image

on:
  push:
    branches: [main]
    tags: ['v*']
    paths:
      - 'src/**'
      - 'Dockerfile'

permissions:
  contents: read
  packages: write
  id-token: write   # required for cosign keyless signing
  attestations: write

env:
  REGISTRY: ghcr.io
  IMAGE: ${{ github.repository }}/claude-agent

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v6

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
          tags: |
            type=sha,format=long
            type=ref,event=branch
            type=ref,event=tag

      - id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          provenance: mode=max
          sbom: true

      - uses: sigstore/cosign-installer@v3

      - name: Cosign sign image (keyless)
        run: |
          cosign sign --yes \
            "${REGISTRY}/${IMAGE}@${{ steps.build.outputs.digest }}"

      - name: Generate SPDX SBOM with syft
        uses: anchore/sbom-action@v0
        with:
          image: ${{ env.REGISTRY }}/${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Attach SBOM as cosign attestation
        run: |
          cosign attest --yes \
            --predicate sbom.spdx.json \
            --type spdxjson \
            "${REGISTRY}/${IMAGE}@${{ steps.build.outputs.digest }}"

      - name: Verify signature locally before publishing
        run: |
          cosign verify \
            --certificate-identity-regexp "^https://github.com/${{ github.repository_owner }}/.+/.github/workflows/.+$" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            "${REGISTRY}/${IMAGE}@${{ steps.build.outputs.digest }}"
cilium-mcp-fqdn-egress.yamlview on GitHub
# ABOUTME: CiliumNetworkPolicy restricting agent egress to a fixed list of approved FQDNs at L7 (DNS-aware).
# ABOUTME: Requires Cilium with enable-l7-proxy: true. On a CNI without FQDN support, layer at the egress proxy instead.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: agent-mcp-fqdn-egress
  namespace: agent-claude-prod
spec:
  endpointSelector:
    matchLabels:
      app: claude-code
  egress:

  # Allow HTTPS to a fixed list of FQDNs. Anything not in this list is
  # blocked at the Cilium L7 DNS proxy.
  - toFQDNs:
    - matchName: api.anthropic.com
    - matchName: api.github.com
    - matchName: registry.example.com
    - matchName: ghcr.io
    - matchPattern: "*.s3.us-east-1.amazonaws.com"   # narrow further if you can
    toPorts:
    - ports:
      - port: "443"
        protocol: TCP
      rules:
        http:
        - method: "GET|HEAD|POST|PUT"
          # Optionally constrain paths per host; left open here for breadth.

  # Allow DNS to kube-dns. Without this, FQDN matching cannot resolve names
  # and the agent cannot reach any host, including the allowed ones.
  - toEndpoints:
    - matchLabels:
        k8s:io.kubernetes.pod.namespace: kube-system
        k8s:k8s-app: kube-dns
    toPorts:
    - ports:
      - port: "53"
        protocol: UDP
      - port: "53"
        protocol: TCP
      rules:
        dns:
        - matchPattern: "*"
kyverno-require-sbom.yamlview on GitHub
# ABOUTME: Kyverno ClusterPolicy requiring an SPDX SBOM attestation on every Pod image. Requires Kyverno 1.18+.
# ABOUTME: The condition asserts a syft-generated SBOM; tune for your build tool. Without an SBOM, admission is rejected.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-spdx-sbom
  annotations:
    policies.kyverno.io/title: Require SPDX SBOM attestation on every image
    policies.kyverno.io/category: Agentic Covenants / Supply Chain
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Enforce
  background: false
  rules:

  - name: require-spdx-attestation
    match:
      any:
      - resources:
          kinds: [Pod]
    verifyImages:
    - imageReferences:
      - "*"
      attestations:
      - type: https://spdx.dev/Document
        conditions:
        - all:
          # Heuristic: assert that syft is named in the SBOM creators block.
          # If you build SBOMs with a different tool (Trivy, cyclonedx-bom),
          # adjust the JMESPath to match.
          - key: "{{ creationInfo.creators[?contains(@, 'syft')] }}"
            operator: NotEquals
            value: ""
        attestors:
        - entries:
          - keyless:
              subject: "https://github.com/example-org/*"
              issuer: "https://token.actions.githubusercontent.com"
kyverno-verify-image-signatures.yamlview on GitHub
# ABOUTME: Kyverno ClusterPolicy verifying cosign keyless signatures + SPDX SBOM attestation. Requires Kyverno 1.18+.
# ABOUTME: Substitute example-org for your GitHub org. The keyless subject regex pins signatures to your org's workflow runs only.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
  annotations:
    policies.kyverno.io/title: Verify cosign signatures and SPDX attestations
    policies.kyverno.io/category: Agentic Covenants / Supply Chain
    policies.kyverno.io/severity: critical
spec:
  validationFailureAction: Enforce
  background: false
  webhookTimeoutSeconds: 30
  failurePolicy: Fail
  rules:

  - name: verify-cosign-keyless-signature
    match:
      any:
      - resources:
          kinds: [Pod]
          namespaces:
          - "agent-*"
          - "default"
    verifyImages:
    - imageReferences:
      - "ghcr.io/example-org/*"
      attestors:
      - entries:
        - keyless:
            # Subject regex pins signatures to GitHub Actions workflows in this org.
            # subject: "*" accepts anyone's signature; that is the most common
            # misconfiguration of cosign verification.
            subject: "https://github.com/example-org/*/.github/workflows/*"
            issuer: "https://token.actions.githubusercontent.com"
            rekor:
              url: https://rekor.sigstore.dev

  - name: verify-spdx-sbom-attestation
    match:
      any:
      - resources:
          kinds: [Pod]
          namespaces:
          - "agent-*"
          - "default"
    verifyImages:
    - imageReferences:
      - "ghcr.io/example-org/*"
      attestations:
      - type: https://spdx.dev/Document
        attestors:
        - entries:
          - keyless:
              subject: "https://github.com/example-org/*"
              issuer: "https://token.actions.githubusercontent.com"

  - name: deny-non-allowlisted-registries
    match:
      any:
      - resources:
          kinds: [Pod]
    validate:
      message: >-
        Images must come from approved registries only:
        ghcr.io/example-org/* or registry.example.com/*
      pattern:
        spec:
          containers:
          - image: "ghcr.io/example-org/* | registry.example.com/*"
lockfile-integrity.ymlview on GitHub
# ABOUTME: CI workflow that validates lockfile integrity even when pre-commit was bypassed with --no-verify.
# ABOUTME: This is the real backstop for client-side lockfile pinning. Drop in .github/workflows/.
name: Lockfile Integrity

on:
  pull_request:
    paths:
      - 'package.json'
      - 'package-lock.json'
      - 'requirements*.txt'
      - 'requirements.in'
      - 'pyproject.toml'
      - 'Pipfile'
      - 'Pipfile.lock'
      - 'poetry.lock'
      - 'go.mod'
      - 'go.sum'
      - 'Cargo.toml'
      - 'Cargo.lock'

jobs:

  npm:
    if: hashFiles('package-lock.json') != ''
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
      - name: Verify package-lock.json matches package.json
        # npm ci --dry-run fails if the lockfile and manifest are out of sync.
        run: npm ci --dry-run

      - name: Run npm audit (high severity, fail-on)
        run: npm audit --audit-level=high

  python-requirements:
    if: hashFiles('requirements.in') != ''
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: '3.13'
      - name: Verify requirements.txt is the compiled output of requirements.in
        run: |
          pip install pip-tools==7.5.3
          pip-compile --no-header --no-emit-index-url \
            --output-file=/tmp/requirements.lock requirements.in
          if ! diff -q requirements.txt /tmp/requirements.lock >/dev/null 2>&1; then
            echo "::error::requirements.txt does not match the compiled output of requirements.in."
            echo "An agent that edits requirements.txt directly bypasses pip-compile review."
            diff -u requirements.txt /tmp/requirements.lock || true
            exit 1
          fi

      - name: Run pip-audit
        run: |
          pip install pip-audit==2.10.1
          pip-audit --requirement requirements.txt --strict

  poetry:
    if: hashFiles('poetry.lock') != ''
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with:
          python-version: '3.13'
      - run: pip install poetry==2.4.1
      - name: Verify poetry.lock is in sync with pyproject.toml
        run: poetry check --lock

  go:
    if: hashFiles('go.mod') != ''
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-go@v6
        with:
          go-version: 'stable'
      - name: Verify go.sum matches go.mod
        run: |
          go mod verify
          go mod tidy -diff

  cargo:
    if: hashFiles('Cargo.lock') != ''
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - name: cargo audit
        run: |
          cargo install cargo-audit --locked
          cargo audit --deny warnings
      - name: Verify Cargo.lock is up to date
        run: cargo update --workspace --locked

Cell notes

Supply chain / Server-side

Control. Image registry restrictions in admission policy. OCI signature verification (cosign). SBOM admission requirements. Egress NetworkPolicy to approved registries only. OPA policy denying images without provenance attestation. SLSA build-provenance attestation gates. MCP domain allowlist enforced at the network layer. Server-side lockfile-integrity validation.

Strength. Deterministic at admission. Bypass requires compromise of signing infrastructure (KMS key theft), signature stripping at a registry mirror, policy misconfigured to allow unsigned images in some namespaces ("the tools namespace exception"), or cosign trust policy with subject: "*" accepting anyone's keyless signature.

Tooling

  • - Cosign for OCI signing and verification.
  • - Syft and Grype (or Trivy) for SBOM generation and scanning.
  • - Kyverno 1.18+ or OPA Gatekeeper for admission control.
  • - A signing key managed in KMS (AWS KMS, GCP KMS, or HashiCorp Vault), or keyless signing via GitHub OIDC + Sigstore.
  • - A SLSA provenance generator (slsa-github-generator).
  • - Cilium with FQDN policy support (or another CNI that enforces L7 DNS at egress).

Files in this directory

  • - build-and-sign.yml, GitHub Actions workflow that builds the agent image, signs it with cosign --yes (keyless), generates an SPDX SBOM with syft, and attaches the SBOM as a cosign attestation. Drop in .github/workflows/.
  • - kyverno-verify-image-signatures.yaml, Kyverno ClusterPolicy verifying cosign signatures from a specific GitHub OIDC subject and verifying the SPDX attestation. Requires Kyverno 1.18+ for the attestors/entries/keyless block shape.
  • - kyverno-require-sbom.yaml, Kyverno ClusterPolicy requiring an SPDX SBOM attestation on every image and verifying it was created by syft (heuristic; tune for your build).
  • - cilium-mcp-fqdn-egress.yaml, CiliumNetworkPolicy restricting agent egress to a fixed list of approved FQDNs (api.anthropic.com, api.github.com, registry.example.com). Requires Cilium with enable-l7-proxy: true or DNS denials are not enforced.
  • - lockfile-integrity.yml, CI workflow that runs npm ci --dry-run, pip-compile --check, and pip-audit regardless of whether pre-commit was used locally. The real backstop for client-side lockfile pinning.

Verification


# 1. Confirm unsigned image is rejected
kubectl run test --image=docker.io/alpine
# expected: rejected by verify-cosign-signature

# 2. Confirm signed image is accepted
# Replace the digest with a real signed agent image digest from your registry.
kubectl run test --image=ghcr.io/example-org/claude-agent@sha256:9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f
# expected: success if signature and SBOM attestation are present

# 3. Confirm FQDN egress allowlist
kubectl exec -n agent-claude-prod claude-code -- \
  curl -sS --max-time 5 https://example.com
# expected: failure (example.com not in allowlist)

# 4. Confirm SBOM attestation present on a built image
cosign tree ghcr.io/example-org/claude-agent@sha256:9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f
# expected: SPDX attestation in the tree

# 5. Confirm lockfile validation catches a hand-edited lockfile
echo '"resolved": "https://attacker.example.com/evil-1.0.0.tgz"' >> package-lock.json
gh pr create
# expected: CI fails at npm ci --dry-run

Common mistakes

  • - Cosign verification configured for a single namespace; agents in other namespaces deploy unsigned images.
  • - Keyless signing trust policy with subject: "*" accepts anyone's keyless signature.
  • - FQDN allowlist enforced by Cilium but the cluster default CNI fails open when Cilium is restarted.
  • - SBOM required but not validated against vulnerability scan; you have provenance for the malicious package.
  • - Server-side lockfile check runs after merge, not before. The bad version is already in main.

Citation

NIST CSF 2.0 PR.PS-02, PR.PS-05 (unauthorized software prevented), PR.IR-01, GV.SC-07 (risks from suppliers identified, recorded, prioritized, assessed, responded to, monitored), ID.RA-09 (authenticity and integrity of hardware and software assessed prior to acquisition and use). NIST AI RMF MAP 4.1, MANAGE 3.1. OWASP LLM03, LLM04. OWASP ASI04. NIST SP 800-218 PS.3 (archive and protect each software release). NIST SP 800-161 Rev. 1 (supply chain risk management). SLSA framework. CISA/NSA/FBI AI Data Security CSI (May 2025). CISA/ASD ACSC "Principles for Secure Integration of AI in OT" (Dec 2025). OWASP MCP04, MCP09.

Primary bypasses

Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.

  • signing infrastructure compromise
  • signature stripping at registry mirror
  • signed-but-malicious images

Crosswalk

NIST CSF 2 0PR.PS-02, PR.PS-05, PR.IR-01, GV.SC-07, ID.RA-09
NIST AI RMFMAP 4.1, MANAGE 3.1
OWASP LLMLLM03, LLM04
OWASP AGENTICASI04
OTHERNIST SP 800-218 PS.3, NIST SP 800-161, SLSA