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
- -
kyverno-log-image-pulls.yaml, mutating ClusterPolicy that annotates every Pod with the image references and pull timestamp; the annotation is shipped to SIEM by the policy-report shipping CronJob in../../authorization/server-side/ship-policy-reports.yaml. - -
sbom-diff-cronjob.yaml, daily CronJob that runs syft against every image in agent namespaces, hashes the SBOM, compares to the previous day's hash, ships sbom_diff events when changed. - -
hubble-fqdn-deny-export.sh, pipes Hubble L7 DNS denials to SIEM (the FQDN-egress denial signal). - -
sigma-cosign-failure.yaml, SIEM rule firing on policy-report entries wheredata.policy: verify-image-signaturesanddata.failed > 0.
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
mutatepolicy 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 0 | DE.CM-09, ID.RA-09, GV.SC-07 |
|---|---|
| OTHER | NIST SP 800-161 Rev. 1, SLSA framework, CISA/NSA/FBI AI Data Security CSI (May 2025) |
Cite this cell:
https://agenticcovenants.com/detect/supply-chain/server-side/