Kubernetes
Every pod in your cluster egresses from a shared NAT gateway, so "which pod called that host" is a question your firewall cannot answer.
The Whisper operator gives each opted-in pod its own routable /128 identity and a governed door out, injected by a webhook you never have to touch by hand.
The pain
A Kubernetes Service and a NetworkPolicy are great at describing east-west traffic — pod to pod, namespace to namespace. They have nothing to say about north-south traffic once a pod calls out to the internet. In the default setup:
- Every outbound connection shares the node's IP (or the cluster's single egress IP, if you've bothered to set one up with a
NAT gateway/egress-ipCRD).kubectl logstells you a pod ran; nothing tells you what it reached, because at the network layer every pod behind that IP looks identical. - Giving a pod real network privilege is a
hostNetwork: trueor aNET_ADMINcapability away — both of which mean the container can now see (or spoof) every other pod's traffic on that node. That is a large blast radius to hand an LLM-driven workload that decides its own destinations at runtime. - Attribution after the fact is a log-correlation exercise. You reconstruct "which pod talked to
evil.example" by cross-referencing conntrack, app logs, and a timestamp, if you kept any of them.
The fix isn't a bigger NAT gateway. It's giving each agent workload — not each node, not the whole cluster — its own address, so the identity is the address and the address is in the DNS. That's what the whisper-operator does for pods, the same way whisper init <tool> does it for a local process (see Connect) and the WireGuard recipe does it for a VM (see WireGuard egress).
What gets installed
whisper-operator is a standard controller-runtime (kubebuilder-layout) operator with one job: watch pod creation and, for pods that opted in, mutate the pod spec before it's scheduled. Concretely, on helm install you get:
- A mutating admission webhook (a
MutatingWebhookConfigurationfromadmissionregistration.k8s.io/v1, receivingAdmissionReviewobjects ofadmission.k8s.io/v1) registered againstpodsCREATE, with a namespace selector so the webhook is only invoked for opted-in namespaces. - A zero-privilege sidecar container — the same public
ghcr.io/whisper-sec/whisperimage used everywhere else in the ecosystem, run asconnect— injected into the pod, plus a small init step that points the pod's outbound traffic at it. - Self-managed webhook TLS: the operator generates and rotates its own serving certificate and patches the
caBundleinto the webhook config itself. Nocert-managerdependency — one fewer moving part in a cluster that may not run it. - An optional
WhisperEgressCRD for pods that want to declare their intent (which agent identity, which policy) declaratively instead of via annotation.
None of this is magic outside Kubernetes' own primitives: it's a webhook, a sidecar, and a Secret. The value is that you get a routable, attributable, RFC-correct IPv6 identity per pod without writing any of that plumbing yourself.
Opt-in, not opt-out
Nothing is mutated unless you ask for it — label the namespace or the pod:
apiVersion: v1
kind: Namespace
metadata:
name: agents
labels:
whisper.security/egress: enabled # every pod in this namespace gets a sidecar
or, per-pod, inside a namespace that isn't globally opted in:
apiVersion: v1
kind: Pod
metadata:
name: research-agent
labels:
whisper.security/egress: enabled
annotations:
whisper.security/agent: acef2002a323d40d4 # pin to an existing identity (optional)
spec:
containers:
- name: agent
image: your-org/research-agent:latest
The webhook fires on CREATE, sees the label, and rewrites the pod: adds the whisper-egress sidecar container with all Linux capabilities dropped (securityContext.capabilities.drop: ["ALL"], no NET_ADMIN, no hostNetwork), reads the API key from a secretKeyRef you provide once per namespace, and sets HTTPS_PROXY/ALL_PROXY on the application container to point at the sidecar's loopback SOCKS5 port. The application container needs no changes, no root, and no awareness that Whisper exists beyond honoring the standard proxy env vars — which every HTTP client already does.
kubectl create secret generic whisper -n agents \
--from-literal=api-key=whisper_live_xxxxxxxxxxxxxxxxxxxx
Install
helm install whisper-operator oci://ghcr.io/whisper-sec/charts/whisper-operator \
--namespace whisper-system --create-namespace
That's the whole install — one chart, no CRDs to apply separately (Helm manages the optional WhisperEgress CRD for you), no cert-manager, no extra RBAC to hand-author. Source: github.com/whisper-sec/whisper-operator (MIT); the image is ghcr.io/whisper-sec/whisper-operator, published multi-arch (amd64/arm64) alongside a Helm chart at oci://ghcr.io/whisper-sec/charts/whisper-operator — same publishing pipeline as the standalone Docker image.
Proving it: without Whisper vs. with Whisper
The point of a per-pod identity is that it's verifiable from outside the cluster with nothing but stock tools — the pod's egress address resolves, reverse-resolves, and carries RDAP like any other internet host.
Prove the egress /128 — from inside the pod, ask what the world sees:
kubectl exec research-agent -n agents -- curl -s https://v6.ident.me
# 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 — the agent's own /128, not a node IP
That address reverse-resolves, carries RDAP, and is DANE-pinned like any Whisper agent — verify it from anywhere, keyless, with the full chain on Verify an agent.
With Whisper (once the sidecar is running, from your own machine, no cluster access needed):
whisper verify 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
agent yes
fqdn acef2002a323d40d4.<tenant>.agents.whisper.online
operator t9f3a1c2e7b4…
dane_ok yes
jws_ok yes
dane_tlsa_sha256 b653a4ef…fcb82d1d
whisper: acef2002a323d40d4.<tenant>.agents.whisper.online is a verified Whisper agent (DANE-anchored)
The /128 sits in AS219419's 2a04:2a01::/32, DANE binds the certificate to the name, and the DNSSEC chain grades all the way to the IANA root — the exact same table you'd get for any agent identity, cluster or not.
And to see the pod's own attributable log trail — every DNS lookup and connection it made, keyed to its identity, not its node's shared IP:
whisper agents logs --agent acef2002a323d40d4 --since 1h
which is the same whisper.agents({op:'logs'}) control verb documented in Control plane — the operator doesn't invent a parallel API, it just wires the identity that verb already understands into the pod lifecycle.
Why a webhook and not a CNI plugin
A CNI plugin operates below the pod, at the network-interface level, and would need to run privileged on every node — exactly the blast radius this design avoids. A mutating webhook operates at admission time, before the pod ever exists, so the sidecar it injects is a normal, unprivileged container subject to the same PodSecurityStandard as everything else in the namespace. The tradeoff is explicit: the operator can only shape pods it's told about (label opt-in), and it can't retrofit already-running pods — restart to pick up a new policy. That's the same tradeoff every mutating-webhook-based sidecar injector makes (Istio's istio-injection label works identically), and it's the one that keeps NET_ADMIN out of your cluster.
Proven end-to-end
The operator is validated the same way every Whisper integration is (see Integration standard): a real kind cluster, a real opted-in namespace, a real pod whose egress is checked from outside with curl https://v6.ident.me, dig -x, and a live RDAP lookup — never a structural "does the chart install" pass. If your workload can run in a pod, it can carry its own routable, revocable, logged identity without ever holding a network capability.
Next
- Docker — the same sidecar as a single-container recipe, no cluster required.
- Egress governance — the policy model the sidecar enforces, independent of where it runs.