feat(observe): resolve connection peer IPs to cluster service/pod names (informer-backed, zero-egress)#131
Merged
Conversation
…es (informer-backed, zero-egress) A NetworkConnection behavior carries a raw IP:port peer; in the dashboard and the adjudicator prompt that reads as e.g. `connects to 10.42.1.159:8086` — opaque. Resolve same-cluster peer IPs to the workload/service they belong to: `connects to analytics/influxdb:8086 (10.42.1.159)`. Resolution is a pure in-memory lookup against an IP->object index built from the Pod/Service objects the engine's reflector stores already watch (status.podIP, spec.clusterIP) — NOT reverse DNS: cluster pod IPs aren't in external DNS and a PTR lookup would leave the cluster, violating zero-egress. The index (`observe::ip_index::IpIndex`) and resolution live in the engine, never the shared `behavior` crate (which has no cluster access — the wire type stays pure data). Enrichment happens in `RuntimeAdapter::contribute`, the point where behaviors enter evidence, so the resolved peer flows through `Behavior::summary()` to BOTH the prompt and the dashboard unchanged — no edits to either rendering site. Rules: pod IP -> ns/pod-name; service ClusterIP -> ns/name; internet peers kept raw (egress, not resolved); unknown/unresolvable IPs left exactly as the raw IP:port (never fabricate a name); raw IP kept in parens for forensics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Behavior::NetworkConnection { peer, internet }carries a rawIP:portpeer, andBehavior::summary()rendersconnects to {peer}. In the dashboard and the adjudicator prompt that reads asconnects to 10.42.1.159:8086— opaque. The operator (and the model) can't see what a pod connects to.Approach — an informer-backed IP index, NOT reverse DNS
We build an in-memory
IpIndex { ip -> (namespace, kind, name) }from the Pod/Service objects the engine's reflector stores already watch (status.podIPon a Pod,spec.clusterIP/clusterIPson a Service). Resolution is then a pure hashmap probe — zero network calls on the hot path.We deliberately do not do DNS/PTR lookups: cluster pod IPs (
10.42.x.x) aren't in external DNS, and any outbound lookup would violate the zero-egress invariant (the security graph and evidence never leave the cluster — CLAUDE.md / docs/adr). The index is a snapshot read; no blocking, no IO.The index and the resolution live in the engine (which has cluster access), never the shared
behaviorcrate (which has none) — the wire type stays pure data.Where resolution happens
In
RuntimeAdapter::contribute(engine/src/engine/observe/adapter/enrich.rs) — the point where behaviors enter evidence and get attached to a Workload'sruntimesignals. Enriching the peer there means the resolved form flows throughBehavior::summary()to both the adjudicator prompt and the dashboard's evidence display with no change to either rendering site. No dashboard/findings-view edits.connects to analytics/influxdb:8086 (10.42.1.159)Behavior rules
ns/pod-name:port (raw-ip).ns/service-name:port (raw-ip).internet: truepeers -> kept raw (external egress, not resolved).IP:port(never fabricate a name)."None") is not indexed; a Pod wins any (pathological) IP collision with a Service.The peer remains untrusted-adjacent (resolved names derive from cluster object names), and the prompt's existing fence/sanitize/budget treatment is untouched — it sanitizes the resolved string exactly as before.
Tests
observe::ip_index::tests(11): pod IP ->ns/name; service ClusterIP ->ns/name; unknown IP stays raw; internet peer stays raw; non-IP:portuntouched; IPv6; headlessNonenot indexed; pod-wins-collision; index built from a fake set of Pod/Service objects (pure lookup).enrich::tests::runtime_adapter_resolves_cluster_connection_peers_to_names: end-to-end through the full adapter pipeline — pod + service peers resolved, unknown IP raw, internet peer raw.Gates (from
engine/)cargo fmt— cleancargo build— cleancargo clippy --all-targets -- -D warnings— cleancargo test --workspace— 357 lib tests + integration suites pass, 0 failedZero-egress invariant intact: no new outbound calls (pure in-memory lookup). No source file exceeds the 1,000-line cap.
🤖 Generated with Claude Code