Ausgehender Traffic in Kubernetes und Proxys: Pod-Env, Sidecar, Egress-Gateway und die Rolle von NO_PROXY
Inhalt des Artikels
- Einleitung: warum die konfiguration „wie auf einem normalen server“ im cluster nicht funktioniert
- Grundlagen: wie ausgehender pod-traffic physisch aufgebaut ist
- Drei ebenen, auf denen sich ein proxy setzen lässt: container, sidecar, egress-gateway
- Umgebungsvariablen im manifest: http_proxy, https_proxy, no_proxy und warum ohne korrektes no_proxy der cluster bricht
- Was diese variablen nicht aufnimmt: kubelet, images mit eigener konfiguration, sdks
- Secrets: proxy-credentials über ein secret, nicht im manifest
- Sidecar-ansatz: wann er gerechtfertigt ist und was er über den proxy hinaus bietet
- Egress-gateway des clusters: ein einheitlicher ausgangspunkt
- Diagnose im cluster: wie man die ausgehende ip aus einem pod prüft und wo man fehler sucht
- Typische fehler
- Werkzeuge und ressourcen
- Fallbeispiele und ergebnisse
- Checkliste für das rollout
- Faq
- Fazit
Einleitung: Warum die Konfiguration „wie auf einem normalen Server“ im Cluster nicht funktioniert
Auf einer normalen virtuellen Maschine ist das Einrichten eines Proxys in fünf Minuten erledigt. Sie tragen HTTP_PROXY ins Profil ein, fügen ein paar Adressen zu den Ausnahmen hinzu, starten den Dienst neu und gehen Kaffee trinken. In Kubernetes endet so ein Szenario anders: Die Hälfte der Services antwortet plötzlich mit 502, der Health-Check lässt Pods durchfallen, und der Zahlungsservice beschwert sich, dass er den benachbarten Pod nicht erreicht. Und dabei funktionieren die externen Anfragen, für die alles überhaupt gemacht wurde, eigentlich ganz gut.
Warum ist das so? Weil im Cluster in einem einzigen Prozess gleichzeitig zwei Welten der Netzwerkkommunikation leben. Die erste ist das externe Internet: Partner-APIs, Marktplätze, Datenquellen fürs Scraping, öffentliche Dienste. Die zweite ist das interne Netz des Clusters: andere Services über ClusterIP, der Kubernetes-API-Server, DNS, Datenbanken, Queues, Metriken. Auf einem normalen Server ist die zweite Welt fast leer, deshalb bricht eine globale Proxy-Variable nichts. Im Cluster ist die zweite Welt riesig, und jeder Proxy, der eine Anfrage an eine Adresse wie payments.default.svc.cluster.local bekommt, kann sie einfach nicht auflösen. Der Proxy steht draußen und weiß nichts über Ihr internes DNS.
Erschwerend kommen die Eigenheiten von Kubernetes selbst hinzu. Pod-Umgebungsvariablen beeinflussen weder kubelet noch die Container-Runtime. Images bringen häufig ihre eigene Konfiguration mit, die die Umgebung ignoriert. SDKs unterschiedlicher Sprachen verstehen unter einem Proxy-Ausschluss jeweils etwas anderes. Und all das vor dem Hintergrund, dass die ausgehende IP eines Pods für die Außenwelt gar nicht sichtbar ist: Nach draußen geht die Adresse der Node oder des NAT-Gateways der Cloud.
In dieser Anleitung betrachten wir den ausgehenden Traffic von Kubernetes „Schicht für Schicht“: Wie verlässt ein Paket physisch den Pod, wo genau lässt sich ein Proxy einbauen, warum wird NO_PROXY im Cluster von einer Formalität zu einem kritischen Architekturelement, wie übergibt man Credentials sicher, wann ist ein Sidecar gerechtfertigt und wann überflüssig, und wie debuggt man Fehler, ohne zu raten. Die grundlegende Syntax der Umgebungsvariablen erzählen wir nicht noch einmal – hier geht es nur um die Cluster-Spezifika. Alle Manifest-Fragmente sind funktionsfähig und können als Basis dienen.
Grundlagen: Wie ausgehender Pod-Traffic physisch aufgebaut ist
Bevor man einen Proxy setzt, ist es hilfreich zu verstehen, welchen Weg ein Paket ohne ihn nimmt. Sonst wird die Diagnose zum Raten.
Vom Container bis zur Node
Jeder Pod erhält eine eigene IP aus dem Pod-CIDR, zum Beispiel 10.244.3.17. Innerhalb eines Pods teilen sich alle Container denselben Netzwerk-Namespace: eine IP, ein Satz Interfaces, gemeinsames localhost. Das ist der entscheidende Fakt für den Sidecar-Ansatz, auf den wir später zurückkommen. Ein Paket aus dem Container verlässt den Pod über ein virtuelles Interface zur Node, wo sich das CNI-Plugin darum kümmert. Danach gibt es zwei Szenarien.
- Ziel innerhalb des Clusters. Wenn die Anwendung eine ClusterIP eines Service anspricht (zum Beispiel 10.96.14.2), ersetzt kube-proxy oder ein eBPF-Datapath des CNI die Zieladresse durch die IP eines konkreten Pod-Backends. Der Traffic geht über das Overlay oder direkt über L3 zur passenden Node und verlässt niemals den Cluster.
- Ziel außerhalb. Wenn die Adresse weder zum Pod-CIDR noch zum Service-CIDR gehört, verlässt das Paket die Node über die Default-Route. Beim Verlassen der Node greift fast immer SNAT: Die Quelladresse 10.244.3.17 wird durch die IP der Node ersetzt. In Clouds kann darüber hinaus ein NAT-Gateway arbeiten, und dann sieht die Außenwelt bereits dessen Adresse.
Daraus folgt die erste praktische Erkenntnis: Ein externer Service sieht niemals die IP eines Pods. Er sieht die IP der Node oder der Cloud-NAT. Wenn Sie Node-Autoscaling betreiben, ändern sich die ausgehenden IPs zusammen mit den Nodes. Genau deshalb funktioniert die Autorisierung am Proxy per IP-Adresse ohne dedizierte Egress-Node im Cluster schlecht – dazu unten mehr.
DNS innerhalb des Clusters
Pods nutzen das Cluster-DNS (üblicherweise CoreDNS). In der resolv.conf des Pods stehen Suchdomains: namespace.svc.cluster.local, svc.cluster.local, cluster.local. Dadurch löst sich der Name payments zu payments.default.svc.cluster.local auf. Ein externer Proxy hat von diesem Schema keine Ahnung. Er bekommt eine Anfrage an den Host payments, versucht ihn über das öffentliche DNS aufzulösen und gibt einen Fehler zurück. Das ist der fundamentale Grund, warum NO_PROXY im Cluster wichtiger ist als auf jedem einzelnen Server.
Was „einen Proxy setzen“ im Cluster überhaupt bedeutet
Unter dieser Formulierung verstehen Ingenieure drei verschiedene Dinge, und sie zu verwechseln ist gefährlich:
- Eine konkrete Anwendung dazu bringen, nach draußen über einen Proxy-Server zu gehen (zum Beispiel über dedizierte Adressen von Proxeon), ohne andere Services anzufassen.
- Eine stabile und vorhersehbare ausgehende IP für einen ganzen Cluster oder einen Teil davon bereitstellen – eine Aufgabe für das Unternehmensnetz, Compliance, Auditing.
- Die Steuerung des externen Traffics zentralisieren: Logging, Limits, ein einheitlicher Ausgangspunkt.
Die erste Aufgabe wird auf Pod-Ebene gelöst. Die zweite und dritte auf Cluster-Ebene. Die Vermischung der Ebenen erzeugt den Großteil der Probleme, über die wir sprechen werden.
Drei Ebenen, auf denen sich ein Proxy setzen lässt: Container, Sidecar, Egress-Gateway
Stellen Sie sich den Cluster wie ein Bürogebäude vor. Ein Proxy kann jedem Mitarbeiter persönlich ausgehändigt werden (Container), ein Assistent neben jedem Arbeitsplatz sitzen (Sidecar) oder es gibt eine einzige Pforte am Ausgang des Gebäudes (Egress-Gateway). Jedes Schema hat seinen Preis.
Ebene 1: der Container selbst
Die Anwendung erhält die Proxy-Adresse über Umgebungsvariablen, eine Konfigurationsdatei oder Start-Flags und leitet ihren Traffic selbst darüber. Das ist die einfachste und verbreitetste Methode.
Vorteile:
- Keine zusätzliche Infrastruktur: Änderungen nur im Deployment-Manifest.
- Granularität: Ein Service braucht den Proxy, der Nachbar nicht.
- Der Proxy „versteht“ das Protokoll: Ein HTTP-Proxy sieht den Zielhost, kann loggen und Regeln anwenden.
Nachteile:
- Funktioniert nur, wenn Code oder Bibliothek die Einstellungen respektieren. Java, einige Node.js-Clients und manche gRPC-Implementierungen benötigen eine separate Konfiguration.
- Vollständige Abhängigkeit von einem korrekten NO_PROXY: Ein Fehler – und die clusterinterne Kommunikation ist gebrochen.
- Die Proxy-Credentials landen in der Prozessumgebung und sind für jeden zugänglich, der exec im Container ausführen kann.
- Rotation des Proxys oder Adresswechsel erfordern das Neuerstellen der Pods.
Ebene 2: Sidecar-Container
Neben der Anwendung läuft im selben Pod ein zweiter Container – ein lokaler Forward-Proxy. Die Anwendung sendet Traffic an localhost:3128 (oder er wird durch iptables-Regeln abgefangen), und das Sidecar entscheidet, wohin und wie weitergeleitet wird: über welchen Upstream-Proxy, mit welcher Autorisierung, mit welchem Timeout.
Vorteile:
- Die Anwendung kennt die externen Credentials nicht: Sie leben nur im Sidecar.
- Ein zentraler Punkt für Logik: Rotation, Wiederholungen, Auswahl eines Adresspools, Metriken – ohne den Anwendungscode zu ändern.
- Der transparente Modus über iptables funktioniert auch mit Bibliotheken, die HTTP_PROXY ignorieren.
- Die Aktualisierung der Proxy-Logik ist die Aktualisierung eines Sidecar-Images.
Nachteile:
- Ein zusätzlicher Container in jedem Pod: Speicher, CPU, ein weiteres Objekt für das Monitoring.
- Transparentes Abfangen erfordert NET_ADMIN im Init-Container, was nicht überall durch Sicherheitsrichtlinien erlaubt ist.
- Schwieriger zu debuggen: zwei Stellen, an denen etwas brechen kann.
Ebene 3: Egress-Gateway des Clusters
Eine dedizierte Gruppe von Pods (oder Nodes) wird zum einzigen Ausgangspunkt. Die übrigen Pods gehen entweder explizit über den internen Service des Gateways oder werden durch Netzwerkpolitiken und Routing dazu gezwungen. Das Gateway geht nach draußen dann direkt oder über einen externen Proxy.
Vorteile:
- Eine stabile ausgehende IP (oder eine kleine Gruppe davon) für den gesamten Cluster. Ideal für die IP-Autorisierung auf Seiten des Proxy-Anbieters und für Whitelists von Partnern.
- Zentralisierte Logs, Limits, Auditing.
- Anwendungen speichern keine Credentials: Sie sehen nur die interne Adresse des Gateways.
- Die Rotation externer Adressen erfolgt an einer Stelle.
Nachteile:
- Single Point of Failure: Das Gateway muss ausfallsicher sein (mehrere Replicas, PodDisruptionBudget, Anti-Affinity).
- Ein zusätzlicher Netzwerk-Hop und potenzieller Engpass bei der Durchsatzrate.
- Erfordert durchdachte NetworkPolicies, sonst bleibt ein „Loch“ mit direktem Ausgang am Gateway vorbei.
- Schwierigere Granularität: Wenn verschiedene Teams unterschiedliche externe Adressen brauchen, müssen mehrere Gateways betrieben werden.
Wie wählen
Eine praktische Regel, die wir beim Design verwenden:
- Ein bis zwei Anwendungen, Code unter Kontrolle, Standard-HTTP-Clients – Container-Ebene.
- Viele Services mit unterschiedlichen Stacks, Anforderungen an Rotation und Wiederholungen, keine Lust, Credentials zu verteilen – Sidecar.
- Compliance-Anforderungen, IP-Autorisierung, Auditing des gesamten externen Traffics, Dutzende Teams – Egress-Gateway, oft kombiniert mit Sidecar für feine Logik.
Beachten Sie: Die Ebenen schließen sich nicht aus. Ein typisches ausgereiftes Schema ist ein Egress-Gateway als verpflichtende Grenze plus Umgebungsvariablen oder Sidecar dort, wo Spezifika nötig sind.
Umgebungsvariablen im Manifest: HTTP_PROXY, HTTPS_PROXY, NO_PROXY und warum ohne korrektes NO_PROXY der Cluster bricht
Die Syntax der Variablen selbst kennen Sie. Uns interessiert, was mit ihnen genau im Pod passiert und warum der Standardwert von NO_PROXY „localhost,127.0.0.1“ hier katastrophal zu wenig ist.
Anatomie eines Ausfalls
Betrachten wir die Kette. Der Service orders ruft payments über die Adresse http://payments:8080/charge auf. In der Umgebung ist HTTP_PROXY gesetzt. Der HTTP-Client schaut auf den Zielhost, findet ihn nicht in NO_PROXY und öffnet statt einer TCP-Verbindung zu payments eine Verbindung zum Proxy und sendet ihm die Anfrage GET http://payments:8080/charge. Der Proxy versucht, den Namen payments über seinen Resolver aufzulösen. Den Namen gibt es nicht. Der Proxy gibt 502 oder 503 zurück. Ihr Service erhält einen Fehler vom Proxy, aber in den Logs sieht es wie ein Fehler von payments aus. Das Team macht sich auf, payments zu reparieren, das völlig gesund ist.
Es gibt auch eine zweite, heimtückischere Variante. Wenn der Service eine ClusterIP direkt über eine Adresse wie 10.96.14.2 anspricht, erhält der Proxy eine Anfrage an eine private Adresse. Manche Proxys verwerfen solche Anfragen sofort, andere versuchen, sich in ihrem eigenen Netz mit 10.96.14.2 zu verbinden. Im schlimmsten Fall gibt es dort einen anderen Host mit dieser Adresse.
Die dritte Variante betrifft Kubernetes selbst. Client-Bibliotheken, die mit dem API-Server kommunizieren (Operatoren, Controller, Anwendungen mit Leader Election), verwenden die Adresse kubernetes.default.svc oder die IP aus der Variable KUBERNETES_SERVICE_HOST. Ohne Ausnahme gehen auch sie über den Proxy und verlieren die Verbindung zum Control Plane.
Was in NO_PROXY für einen Pod stehen muss
Der minimale Satz, den wir als Ausgangspunkt empfehlen. Ersetzen Sie die CIDRs durch Ihre eigenen – sie lassen sich aus der Cluster-Konfiguration oder aus Service-Annotationen ermitteln:
- localhost, 127.0.0.1, ::1 – lokale Adressen, einschließlich der Verbindung zum Sidecar.
- .svc, .svc.cluster.local, .cluster.local – alle Service-Namen. Der führende Punkt bedeutet „jede Subdomain“.
- kubernetes.default.svc, kubernetes.default.svc.cluster.local – explizit, für API-Clients.
- Service-CIDR, zum Beispiel 10.96.0.0/12 – direkte Zugriffe auf ClusterIPs.
- Pod-CIDR, zum Beispiel 10.244.0.0/16 – Headless-Services und Pod-zu-Pod-Zugriffe.
- Node-CIDR – wenn die Anwendung auf Node-Ports zugreift (Metriken, hostNetwork-Services).
- 169.254.169.254 – Cloud-Metadaten, falls genutzt.
- Interne Domains des Unternehmens, zum Beispiel .corp.local, wenn es einen direkten Weg aus dem Cluster dorthin gibt.
- Adressen interner Datenbanken und Queues, falls sie außerhalb des Clusters, aber in einem privaten Netz liegen.
Eine Falle, die nicht jeder kennt: CIDR wird nicht von allen Clients unterstützt
Hier beginnt die eigentliche Spezifik. Das Format von NO_PROXY ist nicht standardisiert. Jede Bibliothek parst es auf ihre Weise:
- Go (net/http) versteht CIDR-Notation, Suffixe mit führendem Punkt und einzelne Hosts. Das ist der beste Fall, und die meisten Kubernetes-Werkzeuge sind in Go geschrieben.
- curl unterstützt CIDR erst ab Version 7.86. In alten Images auf Basis veralteter Distributionen wird der Eintrag 10.96.0.0/12 einfach ignoriert.
- Python (urllib, requests) versteht CIDR überhaupt nicht. Der Vergleich erfolgt über den String-Suffix. Der Eintrag 10.96.0.0/12 wird nie funktionieren; der Eintrag 10.96. funktioniert als präfixähnliche Übereinstimmung nur für Hosts, die auf diesen String enden, hilft also ebenfalls nicht.
- Node.js ignoriert mit dem eingebauten HTTP-Client Umgebungsvariablen vollständig. Es braucht einen ProxyAgent in undici oder einen Drittanbieter-Agent, der dann entscheidet, wie NO_PROXY zu interpretieren ist.
- Java verwendet seine eigene Variable http.nonProxyHosts mit Wildcard-Syntax wie *.svc.cluster.local und einem senkrechten Strich als Trennzeichen.
Praktische Schlussfolgerung: Wenn im Cluster ein Sprachen-Zoo herrscht, ist die einzige zuverlässige Methode, internen Traffic per CIDR auszuschließen, sich nicht auf das NO_PROXY des Clients zu verlassen, sondern es auf Sidecar- oder Gateway-Ebene zu lösen, wo der Parser unter Ihrer Kontrolle ist. Für DNS-Namen funktionieren Suffixe mit Punkt fast überall, deshalb sollte man in Anwendungen Services über Namen ansprechen, nicht über ClusterIPs.
Groß-/Kleinschreibung der Variablen
Ein weiterer Punkt, der regelmäßig Stunden beim Debuggen kostet. curl liest http_proxy nur in Kleinschreibung: Das großgeschriebene HTTP_PROXY wird von ihm bewusst ignoriert, wegen einer historischen CGI-Schwachstelle. Gleichzeitig akzeptiert curl HTTPS_PROXY und NO_PROXY in beiden Schreibweisen. Go, Python und .NET lesen beide Varianten. Deshalb setzen wir im Manifest immer beide Formen – das ist billig und eliminiert eine ganze Klasse von Problemen.
Funktionierendes Fragment: ConfigMap mit NO_PROXY und Deployment
Verlagern Sie NO_PROXY in eine ConfigMap auf Namespace-Ebene. So ändern Sie die Liste an einer Stelle statt in zwanzig Deployments:
apiVersion: v1
kind: ConfigMap
metadata:
name: egress-proxy-common
namespace: crawlers
data:
NO_PROXY: "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,kubernetes.default.svc,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,169.254.169.254,.corp.local"
no_proxy: "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,kubernetes.default.svc,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,169.254.169.254,.corp.local"Und das Deployment, das sie nutzt. Die Proxy-Adresse steht hier noch ohne Credentials – wie man Credentials speichert, betrachten wir in einem eigenen Abschnitt:
apiVersion: apps/v1
kind: Deployment
metadata:
name: price-collector
namespace: crawlers
spec:
replicas: 3
selector:
matchLabels:
app: price-collector
template:
metadata:
labels:
app: price-collector
spec:
containers:
- name: app
image: registry.internal/price-collector:2.7.0
envFrom:
- configMapRef:
name: egress-proxy-common
env:
- name: HTTP_PROXY
value: "http://proxy-host.example:8080"
- name: http_proxy
value: "http://proxy-host.example:8080"
- name: HTTPS_PROXY
value: "http://proxy-host.example:8080"
- name: https_proxy
value: "http://proxy-host.example:8080"
readinessProbe:
httpGet:
path: /healthz
port: 8080Achten Sie auf das Schema in HTTPS_PROXY: Meist ist es http://, weil der Client sich mit dem Proxy selbst über normales TCP verbindet und erst darin einen CONNECT-Tunnel für HTTPS-Anfragen aufbaut. Der Wert https:// bedeutet hier TLS bis zum Proxy selbst, was nicht alle Clients und nicht alle Proxy-Server unterstützen. Prüfen Sie im Proxeon-Panel, welchen Verbindungstyp Ihr Tarif bietet.
Stellen Sie sicher, dass die Probe nicht über den Proxy geht
Liveness- und Readiness-Proben führt kubelet aus, nicht die Anwendung, deshalb sehen sie keine Umgebungsvariablen und Proxy spielt für sie keine Rolle. Wenn Ihre Anwendung aber im Handler /healthz selbst Abhängigkeiten über interne Adressen aufruft, fallen diese Aufrufe unter HTTP_PROXY. Genau so sieht das klassische Symptom aus: „Nach dem Hinzufügen des Proxys bestehen die Pods die Readiness nicht mehr.“
Was diese Variablen NICHT aufnimmt: kubelet, Images mit eigener Konfiguration, SDKs
Die Umgebungsvariablen eines Pods leben im Prozess der Anwendung. Alles, was außerhalb liegt oder die Umgebung bewusst ignoriert, erfordert eine separate Konfiguration. Betrachten wir drei Gruppen.
Kubelet und Container-Runtime
Die häufigste Frage beim Umzug eines Clusters hinter einen Proxy: „Warum werden Images nicht gezogen, wir haben doch HTTP_PROXY im Deployment gesetzt?“ Die Antwort: Images lädt nicht der Pod herunter, sondern die Container-Runtime (containerd oder CRI-O) auf der Node auf Befehl von kubelet. Das sind Systemdienste, die über systemd gestartet werden, und sie lesen nur ihre eigene Umgebung.
Die Konfiguration erfolgt über eine Drop-in-Datei für die systemd-Unit auf jeder Node. Für containerd sieht das so aus (Datei /etc/systemd/system/containerd.service.d/http-proxy.conf):
[Service]
Environment="HTTP_PROXY=http://proxy-host.example:8080"
Environment="HTTPS_PROXY=http://proxy-host.example:8080"
Environment="NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,.svc,.cluster.local,registry.internal"Danach – systemctl daemon-reload und systemctl restart containerd. In NO_PROXY muss unbedingt die interne Registry stehen, sonst holt die Runtime Ihre eigenen Images über den externen Proxy. Für kubelet ist ein separates Drop-in seltener nötig – nur wenn es selbst auf Cloud-APIs oder externe Webhooks zugreift.
In Managed Clusters kann der Zugriff auf die Nodes fehlen, und dann erfolgt die Runtime-Konfiguration über Mechanismen des Cloud-Anbieters oder über einen DaemonSet mit privilegiertem Container, der die Konfiguration auf dem Host anpasst. Die zweite Variante ist ein Kompromiss, der mit dem Sicherheitsteam abgestimmt werden sollte.
Images mit eigener Konfiguration
Viele fertige Images bringen eine Konfigurationsdatei mit, die die Umgebung überschreibt oder ignoriert:
- JVM-Anwendungen. Java liest HTTP_PROXY nicht. Es braucht System-Properties. Praktisch lassen sie sich über JAVA_TOOL_OPTIONS übergeben, die die JVM ohne Änderung der Start-Skripte aufnimmt:
env:
- name: JAVA_TOOL_OPTIONS
value: "-Dhttp.proxyHost=proxy-host.example -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy-host.example -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.svc|*.svc.cluster.local|*.cluster.local|10.*|192.168.*"- Images mit eigener HTTP-Client-Konfiguration (manche Monitoring-Agents, Konnektoren, ETL-Tools) lesen den Proxy aus ihrem eigenen YAML oder TOML. Sie sehen die Umgebung, nutzen sie aber nicht. Lesen Sie die Dokumentation des Images und suchen Sie nach Schlüsseln wie proxy_url.
- Images mit fest eingebautem DNS-Resolver innerhalb der Anwendung brechen die Idee eines NO_PROXY per Name: Die Anwendung löst den Namen selbst auf, bekommt eine IP und vergleicht dann die IP mit der Liste, in der bei Ihnen nur Domain-Suffixe stehen.
- Entrypoint-Skripte, die Variablen überschreiben. Kommt seltener vor, ist aber prüfenswert: kubectl exec pod -- cat /proc/1/environ | tr Nullbyte in Zeilenumbruch zeigt die tatsächliche Umgebung des Hauptprozesses.
Einzelne SDKs und Protokolle
Die Liste ist lang, hier das Häufigste:
- gRPC. Viele Implementierungen unterstützen HTTP_PROXY nur über CONNECT und mit Einschränkungen; ein Teil ignoriert NO_PROXY. Für gRPC-Traffic innerhalb des Clusters ist das doppelt gefährlich.
- Node.js. Das eingebaute fetch und das Modul http/https lesen die Umgebung nicht. Es braucht einen expliziten Agent. Die Bibliothek axios liest HTTP_PROXY über ihre Abhängigkeit, das native fetch aber nicht. In einer Anwendung können sich zwei Clients unterschiedlich verhalten.
- Cloud-SDKs. Die meisten respektieren HTTP_PROXY, aber der Zugriff auf den Metadaten-Endpoint (169.254.169.254) muss in den Ausnahmen stehen, sonst bricht das Abrufen temporärer Anmeldedaten.
- WebSocket und raw TCP. Clients für Datenbanken, Message-Broker, Redis arbeiten mit eigenen Protokollen über TCP und laufen gar nicht über einen HTTP-Proxy. Muss solcher Traffic zwingend über einen Proxy, braucht man SOCKS-Modus oder transparentes Abfangen auf Sidecar-Ebene.
- Eigene DNS-Clients in Anwendungen (Auflösung über eine separate Bibliothek) interagieren nicht mit dem Proxy und gehen direkt über UDP 53. Das ist normal, sollte aber beim Schreiben von NetworkPolicies bedacht werden.
Fazit des Abschnitts: Bevor Sie einen Proxy für einen Service ausrollen, erstellen Sie eine Karte seiner ausgehenden Verbindungen nach Protokollen und Bibliotheken. Eine halbe Stunde Inventarisierung spart einen Tag Debugging.
Secrets: Proxy-Credentials über ein Secret, nicht im Manifest
Eine Proxy-Adresse mit Login und Passwort im Deployment-Body ist ein Leck, das auf seine Stunde wartet. Manifeste leben in git, landen in CI-Logs, erscheinen in kubectl get deploy -o yaml für jeden mit Leserechten im Namespace. Machen wir es richtig.
Variante 1: vollständige Proxy-URL in einem Secret
Das einfachste Schema: Im Secret liegt der fertige String mit Credentials, die Anwendung erhält ihn als Umgebungsvariable. Das Secret erstellen wir außerhalb von git (über CI-Variablen, einen externen Secret-Manager oder manuell):
kubectl -n crawlers create secret generic egress-proxy-creds --from-literal=HTTP_PROXY="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=HTTPS_PROXY="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=http_proxy="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=https_proxy="http://user123:s3cretPass@proxy-host.example:8080"Im Deployment statt expliziter env – eine Referenz auf das Secret und auf die ConfigMap mit NO_PROXY:
containers:
- name: app
image: registry.internal/price-collector:2.7.0
envFrom:
- configMapRef:
name: egress-proxy-common
- secretRef:
name: egress-proxy-credsEin wichtiges Detail: Wenn Login oder Passwort Sonderzeichen enthalten (@, :, /, %), müssen sie URL-kodiert werden. Ein Passwort mit dem Zeichen @ ohne Kodierung zerreißt das URL-Parsing, und der Client versucht, sich mit dem falschen Host zu verbinden. Im Proxeon-Panel ist es praktisch, bei der Generierung der Zugänge gleich ein Passwort ohne Sonderzeichen zu wählen oder die fertige URL zu kopieren.
Variante 2: getrennte Felder und URL-Zusammenbau im Container
Wenn Login und Passwort getrennt gespeichert werden (zum Beispiel aus einem externen Secret-Store synchronisiert), wird die URL im Entrypoint oder in der Anwendung selbst zusammengebaut:
env:
- name: PROXY_USER
valueFrom:
secretKeyRef:
name: egress-proxy-creds
key: username
- name: PROXY_PASS
valueFrom:
secretKeyRef:
name: egress-proxy-creds
key: password
- name: PROXY_HOST
value: "proxy-host.example:8080"Diese Variante ist bequemer für die Passwort-Rotation: Es ändert sich ein Secret-Key statt vier fertige Strings.
Variante 3: Datei statt Variable
Prozessumgebungsvariablen werden aus /proc gelesen und landen oft in Crash-Dumps und Fehlerberichten. Bei strenger Sicherheitsrichtlinie mounten Sie das Secret als Datei mit Rechten 0400 und lesen sie in der Anwendung oder im Sidecar:
volumes:
- name: proxy-creds
secret:
secretName: egress-proxy-creds
defaultMode: 0400
containers:
- name: app
volumeMounts:
- name: proxy-creds
mountPath: /var/run/secrets/proxy
readOnly: trueVariante 4: Credentials ganz weglassen
Wenn Sie ein Egress-Gateway mit stabiler ausgehender IP betreiben, lässt sich die Autorisierung am Proxy von Login und Passwort auf eine IP-Whitelist umstellen. In diesem Fall wird im Cluster überhaupt nichts Geheimes gespeichert: Die Pods gehen über den internen Service des Gateways, das Gateway geht mit einer festen Adresse hinaus, der Anbieter lässt den Traffic über die Adresse durch. Proxeon unterstützt beide Autorisierungsmodi, und für einen Cluster mit dediziertem Egress empfehlen wir in der Regel den IP-Modus als sicherer und einfacher im Betrieb.
Hygiene rund um das Secret
- Aktivieren Sie die Verschlüsselung von Secrets at rest in etcd – standardmäßig ist sie in den meisten Self-Managed-Installationen ausgeschaltet.
- Beschränken Sie RBAC: Das Recht, Secrets eines Namespace zu lesen, sollte nur Service Accounts haben, die es wirklich brauchen. Eine Anwendung, die das Secret über envFrom erhält, sollte das Secret-Objekt nicht über die API lesen müssen.
- Loggen Sie die Umgebung nicht. Prüfen Sie, dass das Framework env im Debug-Modus beim Start nicht ausgibt.
- Vereinbaren Sie eine Rotation. Der Wechsel des Proxy-Passworts sollte eine Routineoperation sein: Secret aktualisiert, rollout restart, altes Passwort deaktiviert.
- Beachten Sie, dass bei der Nutzung von envFrom die Aktualisierung eines Secrets die Umgebung bereits laufender Pods nicht ändert. Es braucht einen rollout restart. Beim Mounten als Datei aktualisiert kubelet den Inhalt innerhalb von ein bis zwei Minuten, aber die Anwendung muss die Datei neu einlesen können.
Sidecar-Ansatz: Wann er gerechtfertigt ist und was er über den Proxy hinaus bietet
Ein Sidecar im Kontext von Egress ist ein lokaler Proxy-Container im Pod, der den Traffic der Anwendung annimmt und über einen externen Proxy weiterleitet. Er verwandelt die Frage „respektiert die Bibliothek HTTP_PROXY“ in die Frage „kann das Sidecar den Traffic abfangen“, und die zweite Frage ist leichter zu beantworten.
Wann ein Sidecar gerechtfertigt ist
- Die Anwendung ist in einem Stack geschrieben, in dem Proxy-Verwaltung unbequem ist (JVM mit einem Dutzend verschiedener Clients, Node.js mit einer Mischung aus fetch und axios, Legacy in C++).
- Adressen sollen planmäßig oder fehlerbedingt rotiert werden, ohne Code zu ändern: Das Sidecar hält einen Pool aus mehreren Proxeon-Proxys und wechselt selbst.
- Sie wollen Credentials aus der Anwendung herausziehen: Das Sidecar ist der Einzige, der das Passwort kennt, die Anwendung geht ohne Autorisierung an localhost.
- Sie brauchen eine einheitliche Politik für Wiederholungen, Timeouts und Request-Limits gegenüber der Außenwelt für den Service.
- Es wird nicht nur HTTP benötigt: SOCKS-Modus oder transparentes TCP-Abfangen.
Wann ein Sidecar überflüssig ist
- Ein einzelner Go- oder Python-Service mit einem HTTP-Client. Umgebungsvariablen reichen.
- Harte Ressourcenlimits: Tausende kleiner Pods, bei denen zusätzliche 30–50 MB pro Sidecar zu Tonnen an Speicher werden.
- Es gibt bereits ein Egress-Gateway mit der nötigen Logik, und sie in jedem Pod zu duplizieren ergibt keinen Sinn.
Native Sidecar-Container
Mit Kubernetes 1.29 erschien und mit 1.33 stabilisierte sich der Mechanismus nativer Sidecars: ein Init-Container mit restartPolicy: Always. Ein solcher Container startet vor dem Hauptcontainer, lebt die gesamte Lebensdauer des Pods und beendet sich korrekt nach dem Hauptcontainer. Das löst einen alten Schmerz: Früher konnte die Anwendung vor dem Proxy starten und bei den ersten Anfragen connection refused bekommen, und ein Job mit Sidecar beendete sich nie, weil das Sidecar weiterlief.
Funktionierendes Fragment: Pod mit Sidecar-Proxy
Hier ist das Sidecar ein kleiner Forward-Proxy (hypothetisches Image egress-sidecar), der auf 127.0.0.1:3128 lauscht, die Upstream-Credentials aus einem Secret liest und weiß, welche Adressen direkt durchgelassen werden:
apiVersion: apps/v1
kind: Deployment
metadata:
name: feed-importer
namespace: integrations
spec:
replicas: 2
selector:
matchLabels:
app: feed-importer
template:
metadata:
labels:
app: feed-importer
spec:
initContainers:
- name: egress-sidecar
image: registry.internal/egress-sidecar:1.3.0
restartPolicy: Always
env:
- name: LISTEN_ADDR
value: "127.0.0.1:3128"
- name: DIRECT_CIDRS
value: "10.96.0.0/12,10.244.0.0/16,192.168.10.0/24"
- name: DIRECT_SUFFIXES
value: ".svc,.cluster.local,.corp.local"
envFrom:
- secretRef:
name: egress-upstream-creds
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 96Mi
readinessProbe:
tcpSocket:
port: 3128
containers:
- name: app
image: registry.internal/feed-importer:4.1.0
envFrom:
- configMapRef:
name: egress-proxy-common
env:
- name: HTTP_PROXY
value: "http://127.0.0.1:3128"
- name: HTTPS_PROXY
value: "http://127.0.0.1:3128"
- name: http_proxy
value: "http://127.0.0.1:3128"
- name: https_proxy
value: "http://127.0.0.1:3128"Beachten Sie: NO_PROXY wird für die Anwendung trotzdem gebraucht. Das Sidecar kann internen Traffic direkt durchlassen, aber warum sollte man ihn über einen zusätzlichen Hop auf localhost schicken? Außerdem liegt das CIDR-Parsing im Sidecar unter Ihrer Kontrolle, in der Anwendung nicht. Doppelter Schutz ist hier angebracht.
Transparenter Modus über iptables
Wenn die Anwendung die Umgebung vollständig ignoriert, kommt ein Init-Container hinzu, der den gesamten ausgehenden TCP-Traffic des Pods – außer internen Netzen und dem Traffic des Sidecars selbst – auf den Sidecar-Port umleitet. Das Sidecar muss dabei im transparenten Proxy-Modus arbeiten und die ursprüngliche Zieladresse wiederherstellen. Der Init-Container braucht die Capability NET_ADMIN, und das Sidecar muss unter einer separaten UID laufen, damit sein eigener Traffic nicht in eine Schleife gerät:
initContainers:
- name: egress-redirect
image: registry.internal/egress-init:1.0.0
securityContext:
capabilities:
add: ["NET_ADMIN"]
command: ["sh", "-c"]
args:
- |
iptables -t nat -N EGRESS_REDIRECT
iptables -t nat -A EGRESS_REDIRECT -d 10.96.0.0/12 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -d 10.244.0.0/16 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -d 127.0.0.0/8 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -m owner --uid-owner 1337 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -p tcp -j REDIRECT --to-ports 15001
iptables -t nat -A OUTPUT -p tcp -j EGRESS_REDIRECTHier ist 1337 die UID, unter der das Sidecar läuft, und 15001 sein transparenter Port. Das ist dasselbe Prinzip, das Service Meshes zum Abfangen von Traffic nutzen. Wenn im Cluster bereits ein Mesh läuft, reichen wahrscheinlich dessen Egress-Fähigkeiten aus und ein separates Sidecar ist nicht nötig.
Was ein Sidecar über den Proxy hinaus bietet
- Metriken des ausgehenden Traffics aufgeschlüsselt nach Zielhost, Antwortcode, Zeit: Die Anwendung weiß nichts davon, das Sidecar sieht alles.
- Circuit Breaker gegenüber externen APIs: Bei einer Serie von Fehlern hört das Sidecar auf, den Partner zu bombardieren, und gibt eine schnelle Absage zurück.
- Adresspool und Rotation: eine Liste von Proxeon-Proxys in der Config, Umschalten per Round-Robin-Strategie oder bei Fehler 407/403.
- Lokaler DNS-Cache für externe Namen, der die Last auf CoreDNS senkt.
- Rate Limiting per Destination, um Limit-Vorgaben von Partner-APIs einzuhalten und nicht unter deren Beschränkungen zu fallen.
Egress-Gateway des Clusters: ein einheitlicher Ausgangspunkt
Ein Egress-Gateway löst eine Aufgabe anderen Maßstabs: nicht „wie soll dieser Service über einen Proxy gehen“, sondern „über welche Adressen geht der Cluster überhaupt in die Welt“. Betrachten wir eine praktikable Umsetzung ohne Bindung an ein bestimmtes CNI.
Architektur
- Es wird eine Gruppe von Nodes mit festen externen IPs ausgewählt (in der Cloud mit reservierter öffentlicher Adresse oder hinter einem separaten NAT-Gateway). Die Nodes erhalten das Label node-role: egress und einen Taint, damit sich keine fremden Pods darauf ansiedeln.
- Auf diesen Nodes wird ein Deployment eines Forward-Proxys mit mehreren Replicas und Anti-Affinity ausgerollt. Davor steht ein Service vom Typ ClusterIP.
- Die Anwendungs-Pods erhalten HTTP_PROXY, das auf diesen Service zeigt. Ihr NO_PROXY enthält die internen Netze.
- NetworkPolicy verbietet den Anwendungs-Pods jeden Egress außerhalb des Clusters, außer DNS und Traffic zum Service des Gateways.
- Das Gateway selbst geht nach draußen entweder direkt (dann ist seine IP die ausgehende Adresse des Clusters) oder über einen externen Proxeon-Proxy mit IP-Autorisierung des Gateways.
Funktionierendes Fragment: Gateway-Deployment und NetworkPolicy
apiVersion: apps/v1
kind: Deployment
metadata:
name: egress-gateway
namespace: egress
spec:
replicas: 3
selector:
matchLabels:
app: egress-gateway
template:
metadata:
labels:
app: egress-gateway
spec:
nodeSelector:
node-role: egress
tolerations:
- key: dedicated
value: egress
effect: NoSchedule
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: egress-gateway
topologyKey: kubernetes.io/hostname
containers:
- name: proxy
image: registry.internal/egress-gateway:2.0.1
ports:
- containerPort: 3128
envFrom:
- secretRef:
name: egress-upstream-creds
---
apiVersion: v1
kind: Service
metadata:
name: egress-gateway
namespace: egress
spec:
selector:
app: egress-gateway
ports:
- port: 3128
targetPort: 3128Eine Policy für den Anwendungs-Namespace, die den direkten Ausgang ins Internet schließt:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-only-via-gateway
namespace: crawlers
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: egress
podSelector:
matchLabels:
app: egress-gateway
ports:
- protocol: TCP
port: 3128
- to:
- ipBlock:
cidr: 10.244.0.0/16
- ipBlock:
cidr: 10.96.0.0/12Eine solche Policy macht aus dem Gateway eine Empfehlung eine Verpflichtung: Ein Pod mit vergessenem HTTP_PROXY kommt nicht an ihm vorbei nach draußen, sondern bekommt einfach einen Timeout. Das fällt auf einer Testumgebung in Minuten auf, nicht im Produktivbetrieb nach Wochen.
Ausfallsicherheit und Kapazität
- Mindestens drei Replicas auf verschiedenen Nodes und ein PodDisruptionBudget mit minAvailable: 2.
- Monitoring der Verbindungen: Anzahl offener Verbindungen, Zeit bis zum CONNECT-Aufbau, Anteil der 5xx vom Upstream.
- Ein Limit für gleichzeitige Verbindungen von einem Client-Pod, damit ein fehlerhafter Service das Gateway nicht verstopft.
- Berücksichtigung von conntrack auf den Gateway-Nodes: Bei Zehntausenden Verbindungen pro Sekunde läuft die Tabelle voll, und Sie bekommen rätselhafte Paketverluste. Erhöhen Sie nf_conntrack_max im Voraus.
Diagnose im Cluster: Wie man die ausgehende IP aus einem Pod prüft und wo man Fehler sucht
Proxy-Probleme im Cluster sind heimtückisch, weil das Symptom weit entfernt von der Ursache auftritt. Im Folgenden eine Methode, mit der sich ein Fehler in wenigen Schritten lokalisieren lässt.
Schritt 1: Überzeugen Sie sich, dass die Umgebung tatsächlich angewendet wurde
kubectl -n crawlers exec deploy/price-collector -- env | grep -i proxyAchten Sie auf beide Schreibweisen, Vollständigkeit von NO_PROXY, keine überflüssigen Leerzeichen. Wenn die Anwendung über ein Entrypoint-Skript startet, prüfen Sie zusätzlich die Umgebung des Hauptprozesses über /proc/1/environ – sie kann von dem abweichen, was exec zeigt, das eine neue Shell startet.
Schritt 2: Prüfen Sie die ausgehende IP
Stellen Sie im Cluster oder auf einem externen Host des Unternehmens einen einfachen Echo-Service bereit, der die Adresse des Clients und die Header zurückgibt. Dann aus dem Pod:
kubectl -n crawlers exec deploy/price-collector -- curl -s https://echo.yourcompany.example/ip
kubectl -n crawlers exec deploy/price-collector -- curl -s --noproxy '*' https://echo.yourcompany.example/ipDer erste Befehl folgt den Regeln der Umgebung, der zweite geht zwangsweise am Proxy vorbei. Sind die Adressen gleich, wird der Proxy nicht genutzt. Gibt der erste die IP des Proxy-Anbieters und der zweite die IP der Node oder der Cloud-NAT zurück, funktioniert das Schema. Das ist auch die einzige zuverlässige Methode, um sicherzustellen, dass Sie genau die Adresse sehen, die das Proxeon-Panel ausgegeben hat, und nicht die Adresse der Node.
Schritt 3: Betrachten Sie den Dialog mit dem Proxy aus Sicht des Clients
kubectl -n crawlers exec deploy/price-collector -- curl -sv https://api.partner.example/status 2>&1 | head -30Suchen Sie in der Ausgabe nach der CONNECT-Zeile und dem Antwortcode des Proxys darauf. Typische Signale:
- 407 Proxy Authentication Required – Credentials kamen nicht an oder sind falsch. Prüfen Sie die URL-Kodierung des Passworts und ob der Proxy tatsächlich Basic-Authentifizierung erwartet.
- 403 vom Proxy auf CONNECT – Zielhost oder Port ist durch die Proxy-Politik verboten, oder Ihre IP steht bei IP-Autorisierung nicht auf der Whitelist.
- 502/503 auf CONNECT – der Proxy konnte das Ziel nicht erreichen. Ist das Ziel intern, ist das die Diagnose „Loch in NO_PROXY“.
- Connection refused zum Proxy selbst – falsche Adresse, falscher Port oder NetworkPolicy lässt den Traffic zum Gateway nicht durch.
- Timeout ohne Antwort – meist eine Netzwerkpolitik oder eine fehlende Route. Prüfen Sie, ob DNS überhaupt auflöst.
Schritt 4: Prüfen Sie clusterinterne Aufrufe
kubectl -n crawlers exec deploy/price-collector -- curl -sv http://payments.default.svc.cluster.local:8080/healthz 2>&1 | grep -iE "connected|proxy|HTTP/"Wenn in der Ausgabe eine Verbindung zum Proxy steht statt einer direkten Verbindung zur ClusterIP, hat NO_PROXY diese Adresse nicht abgedeckt. Wiederholen Sie dasselbe für den kurzen Namen payments und für die IP des Service – alle drei sollten direkt gehen.
Schritt 5: Ephemeral-Container für Images ohne Werkzeuge
In minimalen Images gibt es weder curl noch nslookup. Nutzen Sie kubectl debug, das einen Debug-Container in den Netzwerk-Namespace des Pods einhängt:
kubectl -n crawlers debug -it pod/price-collector-7d9f4-x2k8q --image=registry.internal/netdebug:latest --target=app -- shDarin stehen dieselben IPs, dieselbe resolv.conf und dasselbe Sidecar auf localhost zur Verfügung. Nur die Umgebung der Anwendung wird nicht vererbt: Prüfen Sie die Variablen über /proc/PID/environ des Hauptprozesses.
Schritt 6: Logs und Events
- Logs des Sidecars oder Gateways: Antwortcodes des Upstreams, CONNECT-Zeit, angesprochene Hosts. Genau hier sieht man den Versuch, eine interne Adresse nach draußen zu schicken.
- Pod-Events: kubectl describe pod zeigt ImagePullBackOff – ein Zeichen dafür, dass die Container-Runtime nicht für den Proxy konfiguriert ist oder die Registry nicht in ihrem NO_PROXY steht.
- CoreDNS-Logs im Debug-Modus: Ein Anstieg von NXDOMAIN bei externen Namen zeigt, dass die Anwendung externe Hosts selbst auflöst, statt sie dem Proxy zu übergeben.
- conntrack-Metriken auf den Egress-Nodes: Der Anstieg von nf_conntrack_count Richtung Limit ist ein Vorbote von Verlusten.
Schritt 7: Trennen Sie Proxy-Problem und Ziel-Problem
Derselbe 403 kann vom Proxy oder von der Zielseite kommen. Zur Unterscheidung hilft der Via-Header oder spezifische Anbieter-Header in der Antwort sowie der Vergleich mit einer Anfrage über --noproxy. Ist es über den Proxy 403 und direkt 200 – liegt das Problem in der Proxy-Konfiguration oder darin, dass die Zielressource den Traffic von Adressen dieses Pools einschränkt. Ist es in beiden Fällen 403 – liegt das Problem in der Anfrage selbst.
Typische Fehler
Fassen wir die Hürden zusammen, über die man am häufigsten stolpert. Viele haben wir bereits behandelt, hier eine Übersicht zur schnellen Prüfung.
- NO_PROXY vom Server kopiert. Der Wert localhost,127.0.0.1 bricht im Cluster garantiert die Service-Kommunikation. Nötig sind .svc, .cluster.local, Service- und Pod-CIDR.
- Verlass auf CIDR in NO_PROXY bei Python oder altem curl. Die Bibliothek ignoriert den Eintrag stillschweigend, Traffic zur ClusterIP geht über den Proxy. Sprechen Sie über DNS-Namen und nutzen Sie Suffixe.
- Nur großgeschriebene Variablen. curl liest HTTP_PROXY in Großschreibung nicht. Setzen Sie beide Formen.
- Credentials im Manifest. Passwort in git und in der Ausgabe von kubectl get -o yaml. Nur Secret, besser IP-Autorisierung über ein Gateway.
- Passwort mit Sonderzeichen ohne URL-Kodierung. Der Client parst die URL falsch, Sie bekommen connection refused zu einem nicht existierenden Host.
- Erwartung, dass kubelet und containerd die Pod-env übernehmen. Images werden nicht gezogen, alle suchen das Problem im Deployment, aber es braucht ein systemd-Drop-in auf den Nodes.
- Vergessene Registry im NO_PROXY der Runtime. Die interne Registry wird unerreichbar, weil containerd sie über den externen Proxy holt.
- Java mit HTTP_PROXY. Die JVM liest diese Variablen nicht. Es braucht System-Properties über JAVA_TOOL_OPTIONS.
- Sidecar als normaler Container statt natives Init mit restartPolicy: Always. Die Anwendung startet vor dem Proxy und scheitert an den ersten Anfragen, der Job wird nie beendet.
- Egress-Gateway ohne NetworkPolicy. Das Gateway existiert, aber die Hälfte der Pods geht daran vorbei, und Sie erfahren es, wenn der Partner eine Liste unbekannter IPs schickt.
- Secret aktualisiert und kein rollout restart. Alte Pods arbeiten weiter mit dem alten Passwort, das Sie im Panel bereits deaktiviert haben.
- Ignorieren von Cloud-Metadaten. 169.254.169.254 nicht in NO_PROXY – das Cloud-SDK kann temporäre Keys nicht abrufen, und die Anwendung fällt mit rätselhaften Autorisierungsfehlern aus.
- Überlastung einer Proxy-Adresse durch alle Replicas. Dreißig Replicas hämmern auf eine ausgehende Adresse, die Partner-API antwortet mit Limits. Verteilen Sie die Last über einen Sidecar oder ein Gateway auf einen Adresspool.
Werkzeuge und Ressourcen
Ein Set, das man bei der Arbeit mit ausgehendem Traffic im Cluster zur Hand haben sollte.
Eingebaute Kubernetes-Werkzeuge
- kubectl exec und kubectl debug – Prüfung von Umgebung und Netzwerkverbindungen im Pod, einschließlich Ephemeral-Containern für minimale Images.
- kubectl describe pod und Events – ImagePullBackOff, Probe-Fehler, Fehler beim Mounten von Secrets.
- NetworkPolicy – Erzwingung des Ausgangs über das Gateway und Testen von Hypothesen „wer geht daran vorbei“.
- Native Sidecar-Container (restartPolicy: Always in initContainers) – korrekter Lebenszyklus des Proxy-Helfers.
Netzwerkdiagnose
- Ein Debug-Image mit curl, dig, tcpdump, ss, iptables – bauen Sie sich ein eigenes auf Basis einer minimalen Distribution und halten Sie es in der internen Registry.
- Ein eigener Echo-Service, der die Client-IP und Header zurückgibt – die ehrlichste Methode, die ausgehende Adresse zu sehen.
- tcpdump auf der Node mit Filter nach Pod-IP – wenn man genau sehen muss, zu welcher Adresse eine Verbindung aufgebaut wird.
- CoreDNS-Logs im Debug-Modus – um zu verstehen, wer was auflöst.
Verwaltung von Secrets und Konfiguration
- Ein externer Secret-Manager mit Synchronisierung in ein Cluster-Secret – damit Proxy-Credentials nicht in git leben und zentral rotiert werden.
- Kustomize oder Helm mit gemeinsamer ConfigMap für NO_PROXY – eine einzige Wahrheit für alle Deployments eines Namespace.
- Admission-Controller, die das Vorhandensein von NO_PROXY in jedem Pod mit HTTP_PROXY prüfen – automatischer Schutz vor Copy-Paste „vom Server“.
Proxy-Anbieter
Auf Seiten des Proxys sind drei Dinge wichtig: die Möglichkeit der IP-Autorisierung (für das Schema mit Gateway), stabile Adressen mit klarer Bindungsdauer und eine API zur Verwaltung von Zugängen, damit die Rotation von Passwörtern aus der CI heraus automatisiert werden kann. Bei Proxeon gibt es diese Mechanismen im Panel und über die API, was es ermöglicht, den Anbieter in einen GitOps-Prozess einzubinden statt Verbindungsstrings manuell zu kopieren.
Fallbeispiele und Ergebnisse
Fall 1: ETL-Plattform und „kaputte“ interne Services
Das Datenteam fügte HTTP_PROXY in zwanzig Deployments hinzu, damit die Sammler von Marktplatz-Katalogen über dedizierte Adressen gehen. NO_PROXY wurde aus der Server-Dokumentation kopiert: localhost und 127.0.0.1. Innerhalb einer Stunde stieg der Anteil der 5xx zwischen internen Services von 0,1 % auf 34 %, die Readiness-Proben dreier Services begannen zu flackern. Nachdem .svc, .cluster.local und beide CIDRs in eine gemeinsame ConfigMap aufgenommen und ein rollout restart gemacht wurde, verschwanden die Fehler in fünf Minuten. Zusätzlicher Effekt: Die Verlagerung von NO_PROXY in die ConfigMap verkürzte die Zeit für die nächste Änderung der Liste von einem halben Tag (Bearbeitung von zwanzig Manifesten) auf einen einzigen Commit.
Fall 2: JVM-Integrationen und Sidecar
Ein Finanz-Integrator in Java mit sieben verschiedenen HTTP-Clients innerhalb einer Anwendung (teils über Bibliotheken, die System-Properties für den Proxy nicht lesen). Der Versuch, alles über JAVA_TOOL_OPTIONS zu konfigurieren, deckte fünf von sieben Clients ab. Die Lösung war ein natives Sidecar mit transparentem Abfangen über iptables. Alle sieben Clients gingen ohne Code-Änderungen über den Proxy, die Credentials verschwanden aus der Anwendung, und Metriken des ausgehenden Traffics gab es erstmals in der Geschichte des Service. Der Preis: 40 MB Speicher pro Pod, was bei zwölf Replicas unerheblich war.
Fall 3: Egress-Gateway und IP-Autorisierung
Ein Cluster aus sechzig Nodes mit Autoscaling griff auf mehrere Partner-APIs zu, die eine IP-Whitelist verlangen. Jede Skalierung der Nodes änderte den Satz ausgehender Adressen, die Partner blockierten den Traffic. Es wurde ein Egress-Gateway auf drei dedizierten Nodes mit reservierten Adressen ausgerollt, NetworkPolicy in allen Anwendungs-Namespaces eingeführt und die Autorisierung am Proxeon-Proxy auf den IP-Modus umgestellt. Ergebnis: Die Zahl der Vorfälle „Der Partner hat uns wieder blockiert“ sank von drei bis vier pro Monat auf null im Quartal, und aus dem Cluster verschwanden alle Secrets mit Proxy-Passwörtern. Nebeneffekt: Die zentralisierten Logs des Gateways deckten zwei Services auf, die externe APIs ansprachen, von denen niemand wusste.
Fall 4: containerd und ein rätselhaftes ImagePullBackOff
Nach dem Umzug des Clusters in ein Netz mit verpflichtendem Proxy funktionierten die Pods mit gesetztem HTTP_PROXY, aber neue Nodes konnten kein einziges Image ziehen. Ursache war das fehlende Drop-in für containerd. Nach dem Hinzufügen der Datei mit Proxy und interner Registry in NO_PROXY war das Problem gelöst. Die Lektion wurde als Prüfung in das Bootstrap-Skript der Nodes aufgenommen: Vor dem Beitritt zum Cluster prüft die Node, dass containerd ein Test-Image aus der internen und der externen Registry ziehen kann.
Checkliste für das Rollout
Gehen Sie die Liste durch, bevor Sie einen Proxy für einen Produktiv-Service ausrollen. Jeder Punkt ist der nächtliche Vorfall von jemandem.
Vorbereitung
- Eine Karte der ausgehenden Verbindungen des Service ist erstellt: externe Hosts, interne Services, Datenbanken, Queues, Cloud-Endpoints, Protokolle.
- Service-CIDR, Pod-CIDR, Node-CIDR des Clusters und die internen Domains des Unternehmens sind bekannt.
- Es ist geprüft, welche HTTP-Clients die Anwendung nutzt und wie jeder davon NO_PROXY interpretiert (CIDR, Suffixe, Schreibweise).
- Die Ebene ist gewählt: Container, Sidecar oder Gateway. Die Entscheidung ist dokumentiert.
Konfiguration
- NO_PROXY ist in eine ConfigMap des Namespace verlagert und enthält: localhost, 127.0.0.1, .svc, .svc.cluster.local, .cluster.local, kubernetes.default.svc, Service-CIDR, Pod-CIDR, 169.254.169.254, interne Domains.
- Die Variablen sind in beiden Schreibweisen gesetzt.
- Credentials liegen nur im Secret; das Passwort ist URL-kodiert; RBAC auf Secrets ist beschränkt.
- Für JVM ist JAVA_TOOL_OPTIONS mit nonProxyHosts gesetzt.
- Falls Sidecar – es ist nativ (initContainers mit restartPolicy: Always), mit readinessProbe und Ressourcenlimits.
- Falls Gateway – mindestens drei Replicas, Anti-Affinity, PDB, NetworkPolicy in den Anwendungs-Namespaces.
- Die Container-Runtime auf den Nodes ist separat konfiguriert; die interne Registry steht in ihrem NO_PROXY.
Prüfung auf der Testumgebung
- env im Pod zeigt die erwarteten Werte.
- Eine Anfrage an den Echo-Service gibt die Proxy-Adresse zurück; mit --noproxy die Adresse der Node.
- Anfragen an interne Services über kurzen Namen, vollständigen Namen und ClusterIP gehen direkt (sichtbar mit curl -v).
- Readiness und Liveness laufen mindestens zehn Minuten stabil durch.
- Der Kubernetes-API-Client (falls vorhanden) sieht das Control Plane.
- Die Logs des Sidecars oder Gateways enthalten keine Zugriffe auf interne Adressen.
Rollout und danach
- Schrittweiser Rollout mit maxUnavailable nicht über 25 %; Monitoring von 5xx zwischen Services und von 407/403 vom Proxy.
- Das Verfahren für die Passwort-Rotation ist dokumentiert: Secret aktualisieren, rollout restart, alten Zugang deaktivieren.
- Alerts sind eingerichtet auf den Anstieg des Fehleranteils vom Proxy und auf die Annäherung von conntrack an das Limit auf den Egress-Nodes.
- Nach einer Woche – Revision der Gateway-Logs: Sind unerwartete externe Adressaten aufgetaucht?
FAQ
Kann man HTTP_PROXY global für den ganzen Cluster setzen statt in jedem Deployment?
Einen direkten Mechanismus gibt es nicht: Umgebungsvariablen sind Teil der Pod-Spezifikation. Praktische Optionen sind eine gemeinsame ConfigMap mit envFrom in allen Manifesten über Helm oder Kustomize, oder ein Mutating Admission Controller, der Variablen automatisch zu Pods mit einer bestimmten Annotation hinzufügt. Die zweite Methode ist mächtiger, erfordert aber Sorgfalt: Sie muss auch NO_PROXY hinzufügen, sonst bricht die Automatisierung massenhaft Services.
Warum geht curl im Pod über den Proxy, die Python-Anwendung aber nicht, obwohl die Umgebung gleich ist?
Höchstwahrscheinlich liegt es an der Schreibweise oder am Parsing von NO_PROXY. curl liest http_proxy nur kleingeschrieben, Python in beiden Varianten. Andererseits versteht Python kein CIDR in NO_PROXY, ein aktuelles curl schon. Prüfen Sie beide Hypothesen: Setzen Sie Variablen in beiden Schreibweisen und verwenden Sie Domain-Suffixe statt CIDR.
Braucht man NO_PROXY, wenn der gesamte interne Traffic ohnehin über ein Sidecar mit Direct-Regeln läuft?
Technisch kann man darauf verzichten, aber wir empfehlen, es zu behalten. Erstens kostet ein zusätzlicher Hop über localhost für Tausende interne Anfragen nicht-null Latenzen und Last am Sidecar. Zweitens: Wenn das Sidecar ausfällt oder neu startet, kommuniziert die Anwendung mit korrektem NO_PROXY weiter mit internen Services, ohne es verliert sie alles.
Wie erkennt man, dass eine Anfrage an einen Partner tatsächlich von der Proxy-Adresse kam und nicht von der Node-Adresse?
Die einzige zuverlässige Methode ist eine Kontrollanfrage an einen Echo-Service, der die Client-IP zurückgibt, aus demselben Pod und mit derselben Umgebung. Vergleichen Sie die Antwort mit der Adresse, die im Proxeon-Panel für Ihren Zugang angezeigt wird, und mit der Antwort bei --noproxy. Auch die Logs des Partners helfen, sind aber meist nicht zur Hand.
Sollte man die Verbindung zum Proxy verschlüsseln (Schema https:// in HTTPS_PROXY)?
Für HTTPS-Traffic zu Zielseiten ist der Inhalt bereits end-to-end innerhalb des CONNECT-Tunnels verschlüsselt, der Proxy sieht nur den Zielhost. TLS bis zum Proxy schützt die Autorisierungs-Credentials und Hostnamen auf der Strecke vom Pod zum Proxy. Wenn diese Strecke durch ein nicht vertrauenswürdiges Netz führt, ist TLS bis zum Proxy gerechtfertigt, aber stellen Sie sicher, dass Client und Proxy es unterstützen: Manche Bibliotheken ignorieren das Schema https:// in HTTPS_PROXY. Innerhalb des Clusters zwischen Pod und Egress-Gateway reichen in der Regel Netzwerkpolitiken.
Was macht man mit Datenbank- und Broker-Traffic, der den Cluster verlassen muss?
Über einen HTTP-Proxy läuft er nicht – das ist kein HTTP. Optionen: SOCKS5-Modus des Proxys, wenn Anbieter und Client-Bibliothek ihn unterstützen; transparentes Abfangen auf Sidecar-Ebene mit TCP-Tunneling; oder ein Egress-Gateway, das auf Routing-Ebene arbeitet statt auf Protokollebene. Die Wahl hängt davon ab, ob Sie genau eine bestimmte externe IP brauchen oder ein zentralisierter Ausgang reicht.
Kann man denselben Proxy für containerd und für Anwendungen nutzen?
Technisch ja, aber meist sind das verschiedene Aufgaben. Die Runtime zieht Images aus öffentlichen Registries und braucht einen Proxy des Unternehmensperimeters. Anwendungen sprechen Partner-APIs an und brauchen dedizierte Adressen mit bestimmten Eigenschaften. Die Vermischung führt dazu, dass das Ziehen von Images Traffic-Limits verbraucht, die für Anwendungsaufgaben gedacht sind. Trennen Sie es.
Wie wechselt man das Proxy-Passwort sicher ohne Downtime?
Legen Sie einen zweiten Zugang beim Anbieter an, aktualisieren Sie das Secret auf die neuen Credentials, führen Sie einen rollout restart mit schrittweiser Aktualisierung der Replicas durch, vergewissern Sie sich anhand der Logs, dass keine 407 auftreten, und deaktivieren Sie erst dann den alten Zugang. Wenn Sie IP-Autorisierung über ein Gateway nutzen, verschwindet die Passwort-Rotation komplett aus Ihrer Aufgabenliste.
Wie beschränkt man den Proxy auf bestimmte externe Domains?
NO_PROXY funktioniert umgekehrt – es beschreibt, wohin der Proxy nicht gehen soll, und darauf lässt sich keine Whitelist aufbauen. Eine solche Logik wird im Sidecar oder Gateway umgesetzt: Regeln wie „Domains aus Liste A – über den Proxeon-Adresspool, alles andere – direkt oder verboten“. Dazu NetworkPolicy, damit ein direkter Ausgang ins Internet physisch nicht existiert.
Fazit
Ausgehender Traffic in Kubernetes unterscheidet sich vom Server-Traffic nicht in Details, sondern in der Struktur selbst: Jeder Pod hat zwei Welten – eine interne und eine externe – und der Proxy darf nur die zweite sehen. Daraus folgt alles Weitere. NO_PROXY wird von einer Formalität zu einem Architekturelement, das das gesamte interne Netz des Clusters beschreiben muss und berücksichtigen muss, dass verschiedene Bibliotheken es unterschiedlich interpretieren. Umgebungsvariablen wirken nur innerhalb des Prozesses – kubelet, Container-Runtime und eigensinnige SDKs erfordern separate Aufmerksamkeit. Credentials dürfen nicht in Manifeste gelangen und sollten im Idealfall dank eines Gateways mit IP-Autorisierung ganz aus dem Cluster verschwinden.
Die drei Ebenen des Proxy-Einbaus – Container, Sidecar, Egress-Gateway – sind keine Konkurrenten, sondern Werkzeuge für unterschiedliche Aufgaben. Die Container-Ebene ist schnell und granular. Das Sidecar nimmt die Abhängigkeit vom Verhalten der Bibliotheken und fügt Metriken, Rotation, Wiederholungen hinzu. Das Gateway gibt dem Cluster ein vorhersehbares Gesicht in der Außenwelt und macht den Ausgang über den Proxy nicht zu einer Empfehlung, sondern zu einer Regel, die durch Netzwerkpolitiken untermauert ist.
Was ist der nächste Schritt? Beginnen Sie mit der Inventarisierung: Erstellen Sie eine Karte der ausgehenden Verbindungen Ihrer Services und schauen Sie, wie viele davon intern sind. Verlagern Sie dann NO_PROXY in eine gemeinsame ConfigMap und prüfen Sie es auf der Testumgebung mit der Methode aus dem Diagnoseabschnitt. Wenn im Cluster mehr als ein Stack läuft oder Anforderungen an stabile Adressen bestehen – entwerfen Sie ein Egress-Gateway und verbinden Sie es mit dedizierten Proxeon-Adressen mit IP-Autorisierung. Und halten Sie die Rollout-Checkliste bereit: Sie zahlt sich schon beim ersten Vorfall aus, der nicht passiert ist.