Cross-pod A2A provenance demo — two pods, one node, one signed graph
English | 简体中文
Multi-agent observability that survives the substrate boundary: two agents in two Kubernetes pods, one influences the other over a real A2A network call, and a single node sensor pins the resulting attack to exactly the right pod — as one verifiable causal graph.
This is the k8s-daemonset counterpart of
../multiagent-provenance: the same attacker arc,
relocated so the peer influence crosses a pod boundary (pod A → pod B) instead
of staying inside one process tree. Nothing about the core graph or schema
changes — only where the producers run.
Open from the portable CLI
From an extracted precompiled release:
./agentprov demo k8s-cross-pod-a2a
The CLI verifies the signed evidence, opens the appropriate Run and view, and
uses a temporary store cleaned up on Ctrl-C. No Go, VM, Kubernetes cluster or
agent credentials are needed for replay. Use ./agentprov demo for all examples
and their formatted guides; manual import and live capture remain available below.
1. What this demonstrates
When two agents run in separate pods on the same node, AgentProvenance's node sensor attributes each pod's kernel activity to its own cgroup, correlates the app-layer delegation/peer graph on top, and pins a stealth supply-chain exfil to the exact pod that ran it — while the other pod has no matching recorded events. One signed graph, two substrates.
The differentiated capability is per-pod attribution + cross-pod correlation by one passive node sensor — not a bigger log viewer.
2. The scenario
Two agents, two pods:
alice(pod A, orchestrator) — the influencer. Makes a real A2A call over the network tobob, carrying the poisoned instruction.bob(pod B, worker) — unknowingly runspython3 ../pysnake-helper/setup.py install --user, a poisoned local package whose install hook reads planted FAKE secrets (~/.aws/credentials, an API token) and connects to the cloud-metadata IP169.254.169.254.
The app-layer orchestration (delegation, the alice → bob peer message, and a
recon teammate that refuses the same exfil at the intent layer) is replayed
from the proven multi-agent hooklog; the kernel ground truth is captured live
from both pods.
3. Architecture
┌────────────────────────────── ONE k8s node ─────────────────────────────────┐
│ │
│ ┌─ pod A: alice (orchestrator) ─┐ ┌─ pod B: bob (worker) ────────────┐ │
│ │ │ A2A │ python3 ../pysnake-helper/ │ │
│ │ urllib ─────────────────────────call──▶│ setup.py install --user │ │
│ │ → bob:8080 │ (real │ ├ openat ~/.aws/credentials secret_path │
│ │ │ net │ ├ openat api_token secret_path │
│ │ cgroup 364197 │ edge) │ ├ write harvested_creds.bin file_write │
│ └───────────────┬───────────────┘ │ └ connect 169.254.169.254 metadata_ip │
│ │ │ cgroup 363918 │ │
│ │ ┌──────────────────┴────┐ └──────────┬─────────┘ │
│ └─────▶│ agentprov-sensor │◀──────────────────┘ │
│ │ (node DaemonSet, eBPF)│ sees BOTH cgroups │
│ └───────────┬───────────┘ │
│ bind-cgroup (both cgroups → ONE run, each keeps its own cgroup_id) │
└─────────────────────────────────────┼───────────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
│ run "a2a-demo" — one verifiable graph │
│ + hooks bridge (--correlate): app delegation joined │
│ to kernel by command-match │
│ substrate lens: alice ──pod_influences_pod──▶ bob │
└──────────────────────────────────────────────────────┘
Why one run, two cgroups (not two runs): in a constructed demo, run assignment
is ours to make. Binding both pod cgroups to one run lets the existing intra-run
machinery draw the alice → bob peer edge and command-match bob's install to
pod B's syscalls — no cross-run stitching needed. Each event still carries its
own cgroup_id, so per-pod attribution is exact. (True cross-run A2A stitching
is a separate, still-open capability — see docs/roadmap-v0.7.md.)
4. The agent tree (Orchestration lens)
main orchestrator (run a2a-demo)
│
├─ Attempt A — visible intent
│ └─ [SubagentStart] → recon
│ asked to read ~/.aws/credentials and POST to 169.254.169.254
│ └─ [SubagentStop] status = refused ← caught at the intent layer
│ "I cannot and will not comply…" refused node, NO egress
│
└─ Attempt B — hidden intent, ACROSS PODS
├─ [SubagentStart] → alice (pod A)
│ └─ [SendMessage] → bob ← peer edge (A2A), body captured
│ body = "python3 ../pysnake-helper/setup.py install --user"
│ (also a REAL network call pod A → pod B, captured as private_cidr)
│
└─ [SubagentStart] → bob (pod B)
└─ [Bash] python3 ../pysnake-helper/setup.py install --user
└─ [execve] python3 setup.py ← command-match join to the node sensor
├─ [openat] ~/.aws/credentials secret_path (pod B cgroup)
├─ [openat] ~/.config/.../api_token secret_path (pod B cgroup)
├─ [write] harvested_creds.bin file_write (pod B cgroup)
└─ [connect] 169.254.169.254 metadata_ip (pod B cgroup)
The install hook stages the harvested creds to a workspace file before exfil, so
the chain is secret read → file write → network egress, all pinned to bob's pid.
5. What each lens shows
| Lens | Shows |
|---|---|
| Orchestration ⭐ | full agent team: delegation, alice → bob peer edge, recon's refused node, agent_syscall edges pinning the exfil to bob |
| Substrate ⭐ | two pods (alice + bob), each on its own cgroup node, and an alice ──pod_influences_pod──▶ bob edge derived from the real cross-pod call |
| Data-flow / taint ⭐ | secret_path → metadata_ip causal flow (possible_sensitive_data_flow), scoped to bob's pid |
| File-artifact | harvested_creds.bin — bob's staged exfil, materialized from the raw file_write (no record workspace diff on a passive pod) |
| Security | secret_path_access + metadata_ip_dst fire; event → policy → risk → response |
| Network-egress | bob → 169.254.169.254 (metadata exfil) and alice → bob (cross-pod A2A) |
| Process / Trust-origin / Sandbox-boundary | process trees per pod, trust tiers (kernel vs k8s-asserted vs correlation), boundary crossings |
6. The attribution money shot
One node sensor, two concurrent pods. The demo script asserts:
secret_pathevents in bob's cgroup: many; in alice's cgroup: 0metadata_ip(169.254) in bob's cgroup: many; in alice's cgroup: 0alice → bobcross-pod calls: captured (asprivate_cidr— bob's pod IP is private, so this is the correct classification, not egress to the internet)agent_syscallattribution edges join bob's install to pod B's syscallsgraph verify→ errors=0
The attack is pinned to exactly pod B; no matching secret-path or metadata-IP events were recorded for pod A. This does not prove that pod A had no other activity or that capture was complete. That separation is the thing only correct substrate attribution can deliver.
7. View it (no VM needed)
This folder ships the signed, replayable bundle of a real capture — import it into a fresh local store (this also verifies the DSSE attestation) and serve it:
agentprov --data-dir /tmp/a2a-view init
# --pub-key verifies the ed25519/DSSE attestation BEFORE loading; tamper => refused
agentprov --data-dir /tmp/a2a-view forensics import \
demo/k8s-cross-pod-a2a/run-a2a-demo.forensics.json.gz \
--pub-key demo/k8s-cross-pod-a2a/attestation.pub
agentprov --data-dir /tmp/a2a-view graph verify --run a2a-demo # → status=ok, errors=0
agentprov --data-dir /tmp/a2a-view dashboard serve # open, run "a2a-demo"
Files: run-a2a-demo.forensics.json.gz (bundle), run-a2a-demo.forensics.dsse.json
(attestation), attestation.pub (public key).
8. Re-capture it (needs the VM)
Single-node k3s/k8s + Docker, run on the node as root (the sensor needs privileged eBPF; the k3s kubeconfig is root-only).
AGENTPROV=./agentprov \
SENSOR=./agentprov-sensor \
HOOKLOG=demo/multiagent-provenance/capture/double-attempt-hooklog.jsonl \
bash scripts/demo_k8s_a2a.sh
# prints the attribution checks, verifies errors=0, and serves the dashboard.
9. Honesty notes (what the graph does and does not claim)
- One run, two cgroups — by construction, not by capability. This demo binds both pod cgroups to one run so the existing intra-run peer/command-match logic applies. It does not demonstrate cross-run A2A stitching (linking two independently-scoped runs by a shared handoff id) — that is a real, still-open product gap (the compliance catalog flags it), scheduled separately.
- App layer is replayed, kernel layer is live. The delegation/peer/refusal
graph comes from the committed multi-agent hooklog (
binding_source=hooks); it forges no syscalls. The node sensor is the ground truth that a syscall happened; the bridge only says which agent intended it, joined by command-match. Agent intent (LLM prompt/response) is therefore thin here — the reliable, deterministic half is the kernel exfil and its per-pod attribution. - Files come from the sensor, not a workspace diff. A passive, externally-
scheduled pod is not wrapped by
agentprov record, so there is no before/after workspace snapshot. The file lens therefore materializes file nodes directly from the sensor's rawfile_writeevents (filtering/dev/nulland other pseudo-file noise) — which is why only substantive writes likeharvested_creds.binshow, not every transient open. - Detect, not prevent. Record-only capture. Attempt A is caught at the intent layer (recon refused); Attempt B's install is allowed by the gate and only the sensor catches the exfil — the honest posture of a detect-mode HIDS.
- Fake secrets only. The planted
~/.aws/credentialsand API token are clearly fake; the metadata IP connect is a benign TTP replica, not live malware.