Engineering blog
August 5, 2026 Michael Freeman
Who Owns This Public IP? Kubernetes VIP Inventory for Flow Investigations
NetFlow to a MetalLB or Gateway VIP is not a host conversation. How ServiceRadar maps public IP:port to Service, Gateway API route, and backend pods—without giving host agents the Kubernetes API.
Picture the alert that shows up at 2 a.m.: NetFlow from a Colombian source address into
23.138.124.7:22
. Is that a shell on a host? Git over SSH into Forgejo? Someone probing an Envoy
listener that only forwards after Gateway API matching? Without cluster context, the flow
record stops at “public IP, port 22.”
We have spent a long time making host process attribution excellent—eBPF netprobe, socket inventory, Workload Identity for pod and container metadata. That stack answers which binary on which host owned this socket. It does not, by itself, answer which Kubernetes Service or Gateway owns this public VIP. Those are different planes of truth, and conflating them is how investigations stall.
Two questions that look the same and are not
| Question | Plane | What answers it |
|---|---|---|
| Who owns this public IP:port? | Cluster control plane | Service / Gateway API / MetalLB inventory |
| Which process owns the socket after DNAT? | Host / node | netprobe + process attribution |
| Which pod/image is that process in? | Node runtime | Workload Identity (CRI / Docker) |
On our demo cluster, that Colombian-looking destination is not a host listening on 22. It
is a MetalLB VIP on an Envoy Gateway LoadBalancer, with a Gateway API
TCPRoute
steering port 22 into Forgejo’s git-SSH Service. After DNAT you will see
envoy
on an internal target port (for example
:10022
)
and
gitea
on the backend pod’s
:2222
.
Those process facts are real—and useless for the first five minutes of IR if you cannot
get from the VIP to the Service name without a
kubectl get svc -A
scavenger hunt.
What we shipped: cluster-plane public endpoint inventory
serviceradar-k8s-inventory
is a small in-cluster collector with a read-only ClusterRole. It watches Services,
EndpointSlices, and Gateway API resources (HTTPRoute, TCPRoute, and friends), builds a
snapshot of public edges, and publishes to NATS JetStream on
inventory.k8s.public_endpoints
.
Core’s EventWriter upserts into
platform.public_endpoints_current
.
SRQL and web-ng surface it as
in:public_endpoints
on
/inventory/public-endpoints
.
in:public_endpoints ip:23.138.124.7 port:22
On demo that returns (simplified):
-
LoadBalancer
— Envoy Gateway Service in
envoy-gateway-system, MetalLB poolk3s-pool, backend targetpodIP:10022 -
Gateway
—
TCPRoute/forgejo-ssh→ Serviceforgejo-ssh→ Forgejo pod on:2222
That is the answer to the original IR question in one SRQL query. No ad-hoc
kubectl
from the laptop of whoever still has cluster-admin.
Security model: host agents stay off the kube API
We deliberately did not solve this by teaching every node agent to call the Kubernetes API. Host agents already run with privileged network visibility; giving them broad list/watch on Services and Gateways would widen blast radius for no good reason.
The pattern is the same one mature platforms use for cluster metadata (think Cluster Agent / ActiveGate-style collectors): one Deployment holds a least-privilege ServiceAccount (get/list/watch on Services, EndpointSlices, Gateway API objects—not secrets, not exec). Nodes keep netprobe and Workload Identity on local CRI/procfs paths. Inventory publishes ownership; agents publish process and container facts; core correlates when you ask it to.
What the Attributed Flows page shows today (and what it does not)
Public endpoint inventory does not automatically rewrite the Attributed Flows page. That page is still the join of NetFlow-style conversations with host process (and optional workload) identity. If you search attributed flows for the public VIP alone, you will not magically get “Forgejo git-SSH via Envoy Gateway.” You get process attribution where netprobe saw the post-DNAT sockets.
That is intentional for this release slice:
-
Inventory proves ownership for
in:public_endpointsand the inventory UI. - Correlation hints (VIP:port → podIP:targetPort) exist so humans—and a future automatic join—can pivot into process attribution without guessing DNAT rules.
- Wiring VIP rewrite into the attributed-flow join in core remains a follow-on. We would rather ship a truthful ownership table than a half-joined row that confuses operators.
In practice, the demo investigation is already much faster:
# 1) Ownership (cluster plane)
in:public_endpoints ip:23.138.124.7 port:22
# 2) Process facts on the hinted backends (host plane)
# envoy @ podIP:10022, gitea @ podIP:2222 via netprobe attribution
How to enable it
Helm chart flag (off by default outside demo):
k8sInventory:
enabled: true
clusterId: demo
publishMode: nats
gatewayAPI:
enabled: true
The chart creates the ServiceAccount, ClusterRole, Deployment, and NATS subject wiring.
Core needs the EventWriter processor and the
public_endpoints_current
migration. Full operator notes live in the
Kubernetes Public Endpoint Inventory
guide, with cross-links from the
NetFlow
docs.
Why this matters for NetFlow
Exporters and border routers see the public address. That is the address on the wire, the address in threat intel, the address in the ticket. Kubernetes renames reality after the VIP: MetalLB, cloud LBs, Gateway API listeners, and service backends. If your observability product only understands hosts, every VIP investigation becomes “open a second tool and ask the platform team.”
Public endpoint inventory closes that gap without pretending DNAT never happened. It is the ownership layer for modern Kubernetes edges—and the foundation for eventually joining VIP destinations all the way through to process and workload identity in one attributed conversation.
What is next
- Automatic VIP → backend correlation in the attributed-flow pipeline (use inventory hints in core, not ad-hoc UI glue).
- Stronger cloud LB coverage where Services expose hostnames more often than stable IPs.
-
Tighter pivots in web-ng: from a flow row’s destination VIP straight into
in:public_endpointsand back.
Until then: when NetFlow points at a public VIP, start with
in:public_endpoints
.
The Colombian IP question was never really about Colombia. It was about whether the
platform could say “Forgejo git-SSH behind Envoy Gateway” in one breath—and now it can.