# 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-ip` CRD). `kubectl logs` tells 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: true` or a `NET_ADMIN` capability 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](/docs/connect)) and the WireGuard recipe does it for a VM (see [WireGuard egress](/docs/connect)).

## What gets installed

`whisper-operator` is a standard [controller-runtime](https://github.com/kubernetes-sigs/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 `MutatingWebhookConfiguration` from `admissionregistration.k8s.io/v1`, receiving `AdmissionReview` objects of `admission.k8s.io/v1`) registered against `pods` `CREATE`, 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/whisper` image used everywhere else in the ecosystem, run as `connect` — 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 `caBundle` into the webhook config itself. **No `cert-manager` dependency** — one fewer moving part in a cluster that may not run it.
- An optional `WhisperEgress` CRD 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:

```yaml
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:

```yaml
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.

```bash
kubectl create secret generic whisper -n agents \
  --from-literal=api-key=whisper_live_xxxxxxxxxxxxxxxxxxxx
```

## Install

```bash
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](https://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](/docs/docker).

## 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:

```bash
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](/docs/verify).

**With Whisper** (once the sidecar is running, from your own machine, no cluster access needed):

```bash
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:

```bash
whisper agents logs --agent acef2002a323d40d4 --since 1h
```

which is the same `whisper.agents({op:'logs'})` control verb documented in [Control plane](/docs/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](/docs/integrations)): 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](/docs/docker) — the same sidecar as a single-container recipe, no cluster required.
- [Egress governance](/docs/egress-governance) — the policy model the sidecar enforces, independent of where it runs.
