Agentic Covenants

Detect (DE) · Supply chain

Supply chain at the server side layer

external · Outside the agent entirely

If this concern is breached, how do we know?

What this cell does

Image-pull events with signature verification status; SBOM diff over time per workload; cosign verification failures; egress NetworkPolicy denials to non-allowlisted MCP domains.

Artifacts (4)

hubble-fqdn-deny-export.shview on GitHub
#!/usr/bin/env bash
# ABOUTME: Pipes Cilium Hubble L7 DNS denials to the SIEM. The egress-FQDN-allowlist denial signal.
# ABOUTME: Requires Cilium with enable-l7-proxy: true. Run as a long-running service.

set -euo pipefail

SIEM_URL="${SIEM_URL:-https://siem.example.com:9200/agent-sentinel-fqdn-deny/_doc}"
SIEM_TOKEN="${SIEM_TOKEN:?Set SIEM_TOKEN.}"
NAMESPACE_FILTER="${NAMESPACE_FILTER:-agent-}"

# Filter for L7 DNS proxy denials. Cilium emits these as drop verdicts on
# flows that hit the FQDN allowlist with no match.
hubble observe --type drop --output json --follow --namespace "${NAMESPACE_FILTER}" \
| jq --unbuffered -c 'select(.flow.l7 != null and .flow.l7.dns != null and .flow.verdict == "DROPPED")' \
| while IFS= read -r line; do
  curl -sS -X POST "$SIEM_URL" \
    -H "Authorization: Bearer ${SIEM_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "$line" \
    >/dev/null
done
kyverno-log-image-pulls.yamlview on GitHub
# ABOUTME: Mutating Kyverno ClusterPolicy that annotates every Pod with image references and pull timestamp.
# ABOUTME: The annotation is read by ship-policy-reports CronJob and shipped to SIEM. validationFailureAction is irrelevant for mutate.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: log-image-pulls
  annotations:
    policies.kyverno.io/title: Annotate every Pod with its image references for SIEM ingestion
    policies.kyverno.io/category: Agentic Sentinels / Supply Chain
spec:
  background: false
  rules:
  - name: annotate-image-pull
    match:
      any:
      - resources:
          kinds: [Pod]
    mutate:
      patchStrategicMerge:
        metadata:
          annotations:
            agent-sentinel/image-audit: "{{ request.object.spec.containers[].image | join(', ') }}"
            agent-sentinel/image-pull-ts: "{{ time_now_utc() }}"
            agent-sentinel/namespace: "{{ request.object.metadata.namespace }}"
sbom-diff-cronjob.yamlview on GitHub
# ABOUTME: Daily CronJob that runs syft against every image in agent namespaces, compares SBOM hashes day-over-day, ships diffs.
# ABOUTME: SBOM hash captures transitive dependency changes; comparing only top-level package counts misses those.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sbom-differ
  namespace: kyverno
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: sbom-differ-read
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
- apiGroups: [""]
  resources: ["namespaces"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: sbom-differ-read
subjects:
- kind: ServiceAccount
  name: sbom-differ
  namespace: kyverno
roleRef:
  kind: ClusterRole
  name: sbom-differ-read
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: sbom-cache
  namespace: kyverno
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 5Gi
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: sbom-diff
  namespace: kyverno
spec:
  schedule: "0 6 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: sbom-differ
          restartPolicy: OnFailure
          containers:
          - name: differ
            # Pin by digest. Resolve current digest with: crane digest anchore/syft:v1.45.1
            image: anchore/syft@sha256:REPLACE_WITH_DIGEST_FROM_CRANE
            envFrom:
            - secretRef:
                name: siem-credentials
            volumeMounts:
            - name: cache
              mountPath: /var/lib/sbom-cache
            command: ["/bin/sh", "-c"]
            args:
            - |
              set -eu
              for ns in $(kubectl get ns -l agentic-covenants.io/role=agent -o jsonpath='{.items[*].metadata.name}'); do
                for img in $(kubectl get pods -n "$ns" -o jsonpath='{.items[*].spec.containers[*].image}' | tr ' ' '\n' | sort -u); do
                  CURRENT="$(syft "$img" -o spdx-json 2>/dev/null | sha256sum | awk '{print $1}')"
                  KEY="$(printf '%s' "$img" | tr / _).hash"
                  LAST="$(cat "/var/lib/sbom-cache/$KEY" 2>/dev/null || echo none)"
                  if [ "$CURRENT" != "$LAST" ]; then
                    curl -sS -X POST "$SIEM_URL/agent-sentinel-sbom/_doc" \
                      -H "Authorization: Bearer $SIEM_TOKEN" \
                      -H "Content-Type: application/json" \
                      -d "{
                        \"event\": \"sbom_diff\",
                        \"image\": \"$img\",
                        \"namespace\": \"$ns\",
                        \"prev_hash\": \"$LAST\",
                        \"curr_hash\": \"$CURRENT\",
                        \"ts\": \"$(date -Iseconds)\"
                      }"
                    echo "$CURRENT" > "/var/lib/sbom-cache/$KEY"
                  fi
                done
              done
          volumes:
          - name: cache
            persistentVolumeClaim:
              claimName: sbom-cache
sigma-cosign-failure.yamlview on GitHub
# ABOUTME: Sigma rule firing on PolicyReport entries where the verify-image-signatures Kyverno policy failed.
# ABOUTME: Pair with rich context (PR, build SHA, image digest) at alert generation; bare image SHA pages waste oncall time.
title: Cosign image-signature verification failure
id: 9b0c1d2e-3f4a-5b6c-7d8e-9f0a1b2c3d4e
status: experimental
description: >
  Detects an admission failure on the verify-image-signatures Kyverno
  policy. Either an image was deployed without a cosign signature, or its
  signature did not match the trusted issuer. This is high signal: the
  signing pipeline failed, the registry is serving an unsigned image, or
  someone is attempting to deploy a non-allowlisted image.
references:
  - https://github.com/peopleforrester/agentic-covenants/blob/main/SENTINELS_MATRIX.md
author: agentic-covenants
date: 2026/05/08
logsource:
  category: kyverno_policy_report
detection:
  selection:
    event: policy_report
    data.policy: verify-image-signatures
    data.failed|gt: 0
  condition: selection
falsepositives:
  - First-deploy of a new image before its signature has propagated to Rekor (rare with current Sigstore latencies; treat as worth investigating).
  - Mirror registry stripping signatures (treat as a misconfiguration and fix).
level: high
tags:
  - agent
  - supply_chain
  - sentinels

Cell notes

Sentinels, Supply chain / Server-side

Control. Image-pull events captured via Kyverno mutate. Daily SBOM-diff CronJob. Cosign verification failures surface as PolicyReport entries. Cilium FQDN denial flow events shipped to SIEM.

Strength. Deterministic at admission and at the network layer. Failure modes: SBOM diff that compares only top-level package counts (misses transitive dependency changes); image-pull annotation policy in Audit mode (does not actually annotate); cosign failure alert without context (operator gets paged with image SHA but no PR or build context); Hubble FQDN logs disabled by default in some Cilium installs (verify enable-l7-proxy: true).

Tooling

  • - Kyverno 1.18+.
  • - syft for SBOM generation.
  • - Cilium with Hubble and L7 proxy enabled.

Files in this directory

Verification


# 1. Image pull event captured
# Replace digest with a real signed agent image digest from your registry
kubectl run test --image=ghcr.io/example-org/claude-agent@sha256:9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f -n agent-claude-prod
kubectl get pod test -n agent-claude-prod \
  -o jsonpath='{.metadata.annotations.agent-sentinel/image-audit}'
# expected: matches the image

# 2. SBOM diff fires on image update
# Push a new image. Wait for next CronJob run. Check SIEM.

# 3. Cosign verification failure surfaces
kubectl run unsigned --image=docker.io/alpine -n agent-claude-prod
# expected: admission rejection; PolicyReport entry; SIEM alert via sigma-cosign-failure

# 4. FQDN denial captured
kubectl exec -n agent-claude-prod claude-code -- curl -sS --max-time 3 https://blocked-mcp.example.com
hubble observe --type drop --since 1m | grep blocked-mcp
# expected: drop event

Common mistakes

  • - SBOM diff that compares only top-level package counts. Misses transitive dependency changes.
  • - Image pull annotation runs as mutate policy in Audit mode. Does not actually annotate.
  • - Cosign failure alert without context. Operator gets paged with image SHA, no PR or build context.
  • - Hubble FQDN logs disabled by default in some Cilium installs. Verify enable-l7-proxy: true.

Citation

NIST CSF 2.0 DE.CM-09, ID.RA-09, GV.SC-07. NIST SP 800-161 Rev. 1. SLSA framework. CISA/NSA/FBI AI Data Security CSI (May 2025).

Primary failure modes

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

  • SBOM diff compares only top-level package counts (misses transitive)
  • image pull annotation runs as mutate in Audit mode (does not annotate)
  • Hubble FQDN logs disabled by default in some Cilium installs

Crosswalk

NIST CSF 2 0DE.CM-09, ID.RA-09, GV.SC-07
OTHERNIST SP 800-161 Rev. 1, SLSA framework, CISA/NSA/FBI AI Data Security CSI (May 2025)