Kubernetes

Pods egress from whatever node they land on, which is exactly the problem this solves. Two patterns, depending on how many workloads need the identity.

Pattern 1 — sidecar in the pod

Containers in a pod already share a network namespace, so a tunnel container gives every container in that pod your address. Use a full tunnel config. Store it as a Secret — it holds a private key.

kubectl create secret generic aip-tunnel --from-file=aip0.conf=./anchoredip-42.conf

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: partner-sync
spec:
  replicas: 1          # see "replicas and peers" below
  selector:
    matchLabels: { app: partner-sync }
  template:
    metadata:
      labels: { app: partner-sync }
    spec:
      containers:
        - name: tunnel
          image: lscr.io/linuxserver/wireguard:latest
          securityContext:
            capabilities:
              add: ["NET_ADMIN"]
          volumeMounts:
            - name: conf
              mountPath: /config/wg_confs
              readOnly: true
        - name: app
          image: your/app:latest
      volumes:
        - name: conf
          secret:
            secretName: aip-tunnel
            defaultMode: 0400
Replicas and peers. One config is one WireGuard peer. Scaling this Deployment to three replicas puts the same key on three pods, and they will fight over one session — handshakes will succeed and traffic will disappear, intermittently, which is a miserable thing to debug. Keep it at one replica, give each replica its own tunnel via a StatefulSet, or use pattern 2.

Pattern 2 — one egress proxy for the cluster

Better when several workloads need the same identity. Run the tunnel once, behind a Service, and have your applications use it as an HTTP proxy. One peer, one machine slot, any number of clients.

egress.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: aip-egress
spec:
  replicas: 1
  selector:
    matchLabels: { app: aip-egress }
  template:
    metadata:
      labels: { app: aip-egress }
    spec:
      containers:
        - name: tunnel
          image: lscr.io/linuxserver/wireguard:latest
          securityContext:
            capabilities: { add: ["NET_ADMIN"] }
          volumeMounts:
            - { name: conf, mountPath: /config/wg_confs, readOnly: true }
        - name: proxy
          image: ubuntu/squid:latest
          ports:
            - { containerPort: 3128 }
      volumes:
        - name: conf
          secret: { secretName: aip-tunnel, defaultMode: 0400 }
---
apiVersion: v1
kind: Service
metadata:
  name: aip-egress
spec:
  selector: { app: aip-egress }
  ports:
    - { port: 3128, targetPort: 3128 }

in your application

env:
  - name: HTTPS_PROXY
    value: http://aip-egress:3128
  - name: HTTP_PROXY
    value: http://aip-egress:3128
  - name: NO_PROXY
    value: .svc,.cluster.local,10.0.0.0/8

Set NO_PROXY carefully. Without it, in-cluster calls take a detour to Kazakhstan and back, which turns a sub-millisecond hop into a few hundred milliseconds and will look like your application got slow for no reason.

Managed clusters

Nothing here needs a cloud-specific feature — no NAT gateway, no reserved IP, no VPC peering. It works the same on EKS, GKE, AKS, DigitalOcean, Hetzner and a k3s box under a desk, which is the point: the identity belongs to you rather than to the cluster, and it survives you moving the cluster.

NET_ADMIN is required for the tunnel container. On a cluster with a restrictive Pod Security Standard, that container needs the baseline profile; your application container does not have to be touched.

Verify

kubectl exec deploy/partner-sync -c app -- curl -s https://api.ipify.org
kubectl exec deploy/aip-egress -c tunnel -- wg show

Check from the application container, not the tunnel one. See troubleshooting if the handshake never completes — on a managed cluster, egress UDP is the usual culprit.