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 withcosign --yes(keyless), generates an SPDX SBOM withsyft, 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 theattestors/entries/keylessblock shape. - -
kyverno-require-sbom.yaml, Kyverno ClusterPolicy requiring an SPDX SBOM attestation on every image and verifying it was created bysyft(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 withenable-l7-proxy: trueor DNS denials are not enforced. - -
lockfile-integrity.yml, CI workflow that runsnpm ci --dry-run,pip-compile --check, andpip-auditregardless 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 0 | PR.PS-02, PR.PS-05, PR.IR-01, GV.SC-07, ID.RA-09 |
|---|---|
| NIST AI RMF | MAP 4.1, MANAGE 3.1 |
| OWASP LLM | LLM03, LLM04 |
| OWASP AGENTIC | ASI04 |
| OTHER | NIST SP 800-218 PS.3, NIST SP 800-161, SLSA |
Cite this cell:
https://agenticcovenants.com/protect/supply-chain/server-side/