Envoy Gateway: Ein Ingress, drei Welten und OIDC für alle

Von Traefik-Ingress über die Gateway-API zu Envoy Gateway — drei Shared-Gateways für extern, intern und Tailnet, OIDC für Apps ohne SSO und Feintuning per Policy
Table of contents

Der Edge eines Clusters — die Stelle, an der externer Traffic auf die Workloads trifft — ist eine der Komponenten, die man besser einmal richtig baut. Bei mir hat dieser Punkt eine kleine Evolution hinter sich: von Traefik als klassischem Ingress-Controller über die Adoption der Gateway-API bis hin zu Envoy Gateway , das diese API heute exklusiv umsetzt. Dieser Beitrag ist eine ausführliche Tour durch den Aufbau.

Von Traefik zur Gateway-API

Angefangen hat alles mit Traefik als Ingress-Controller — die naheliegende Wahl, charmante Dashboard-UI, schnell aufgesetzt. Mit der Zeit stieß ich aber an die konzeptionellen Grenzen der Ingress-Ressource: Sie ist ein einziges, überladenes Objekt, dessen Lücken jeder Controller mit proprietären Annotations stopft. Routing-Logik, die eigentlich deklarativ sein sollte, verkam zu einer Sammlung anbieterspezifischer Magie in metadata.annotations.

Als Traefik begann, die Gateway-API zu unterstützen, bin ich umgestiegen — und schließlich ganz zur Gateway-API als alleinigem Modell übergegangen. Der entscheidende Unterschied ist die Rollentrennung: Die API zerlegt das Ingress-Problem in saubere, getrennt verantwortbare Ressourcen.

flowchart LR GC["GatewayClass<br/>(Infrastruktur-Anbieter)"] --> GW["Gateway<br/>(Cluster-Betreiber:<br/>Listener, TLS, IP)"] GW --> RT["HTTPRoute / TLSRoute<br/>(App-Team:<br/>Hosts, Pfade, Backends)"]

GatewayClass, Gateway und HTTPRoute haben jeweils einen klaren Eigentümer und ein klares Schema — kein Annotation-Wildwuchs mehr, sondern typisierte Felder, die jeder Controller gleich interpretiert. Genau diese Trennung macht den Edge im GitOps-Repo lesbar und portabel.

Warum Envoy Gateway

Envoy Gateway setzt die Gateway-API auf Basis des battle-tested Envoy-Proxy um — desselben Proxys, der unzählige Service-Meshes und Cloud-Load-Balancer antreibt. Was mich überzeugt hat, ist weniger die Datenebene (die ist über jeden Zweifel erhaben) als die Erweiterungspunkte: ClientTrafficPolicy, BackendTrafficPolicy, SecurityPolicy und EnvoyProxy heben genau die Envoy-Fähigkeiten in die Gateway-API, die man im Betrieb wirklich braucht — und an die man bei Traefik nur über Umwege kam.

Drei Welten, drei Shared-Gateways

Mein Edge ist nicht ein Gateway, sondern drei — je eines für eine Erreichbarkeits-Domäne, jeweils mit eigener GatewayClass, eigenem EnvoyProxy (zwei Replicas) und einer eigenen LoadBalancer-IP aus dem Cilium-BGP-Pool:

GatewayDomäneTLS-IssuerLB-IPErreichbar von
shared-gateway-external*.this-is-fine.io, *.this-is-fine.socialLet’s Encrypt…0.1Internet
shared-gateway-internal*.this-is-fine.internalVault-PKI…0.2LAN
shared-gateway-tailnet*.tif.internalVault-PKI…0.3Tailnet

Jeder Listener terminiert TLS mit einem Zertifikat, das cert-manager passend zur Domäne ausstellt — öffentlich per ACME für das externe Gateway, aus der internen Vault-PKI für die beiden privaten. Ein angehängter Redirect-Filter zwingt jeden :80-Request per 301 auf HTTPS:

- name: "${EXTERNAL_DOMAIN//./-}-https"
  protocol: HTTPS
  port: 443
  hostname: "*.${EXTERNAL_DOMAIN}"
  tls:
    certificateRefs:
      - { kind: Secret, name: "wildcard-${EXTERNAL_DOMAIN//./-}-tls" }

Wichtig ist die Abgrenzung zum gleichnamigen Nachbarn: shared-gateway-tailnet transportiert HTTP(S)-Anwendungen über das Tailnet. Die Kubernetes- und Talos-API dagegen läuft über das tailnet-gateway — zwei verschiedene Pfade, die man nicht verwechseln sollte.

flowchart TB subgraph EXT["Internet"] direction LR U1["Browser"] end subgraph LAN["LAN"] U2["Workstation"] end subgraph TN["Tailnet"] U3["Remote / ZeroClaw"] end U1 -->|"*.this-is-fine.io · LE"| GE["shared-gateway-external · …0.1"] U2 -->|"*.this-is-fine.internal · Vault-PKI"| GI["shared-gateway-internal · …0.2"] U3 -->|"*.tif.internal · Vault-PKI"| GT["shared-gateway-tailnet · …0.3"] GE & GI & GT --> APPS["HTTPRoutes → Workloads"]

OIDC für Apps, die kein SSO können

Das für mich wertvollste Feature ist die SecurityPolicy: Sie schaltet einem HTTPRoute ein vollständiges OIDC-Login vor, bevor der Request überhaupt das Backend erreicht. Damit bekommt selbst ein Dienst, der von Haus aus keinerlei Authentifizierung kennt, ein sauberes Pocket-ID-Login vorgesetzt — die Auth-Logik wandert aus der Anwendung heraus in den Edge:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
  name: grafana-oidc
spec:
  targetRefs:
    - { group: gateway.networking.k8s.io, kind: HTTPRoute, name: grafana-internal }
  oidc:
    provider:
      issuer: "https://auth.${EXTERNAL_DOMAIN}"
    clientID: "${ENVOY_GATEWAY_OIDC_CLIENT_ID}"
    clientSecret:
      name: grafana-oidc-secret
    redirectURL: "https://grafana.${INTERNAL_DOMAIN}/login/generic_oauth"
    logoutPath: "/logout"
    cookieDomain: "${INTERNAL_DOMAIN}"

Envoy übernimmt den kompletten Authorization-Code-Flow gegen Pocket-ID, setzt das Session-Cookie und reicht erst den authentifizierten Request weiter. Für ein Homelab voller kleiner Dienste, die SSO niemals selbst implementieren würden, ist das ein enormer Hebel: Ein einziges SecurityPolicy-Manifest, und der Dienst sitzt hinter zentralem SSO.

Feintuning per Policy

Zwei weitere Policy-Typen erlauben Eingriffe in die Datenebene, ohne den Proxy anzufassen. Eine ClientTrafficPolicy regelt das Verhalten zum Client hin — bei mir vor allem HTTP/2-Tuning und großzügige Idle-Timeouts, damit langlebige Verbindungen wie Server-Sent-Events oder Streaming nicht vorzeitig gekappt werden:

kind: ClientTrafficPolicy
spec:
  targetRef: { kind: Gateway, name: shared-gateway-external }
  timeout:
    http:
      idleTimeout: 3600s
      streamIdleTimeout: 900s
  http2:
    maxConcurrentStreams: 100

Eine BackendTrafficPolicy wirkt umgekehrt zum Backend hin. Das schönste Beispiel ist meine Zot-Registry: Image-Layer können hunderte Megabyte groß sein und Pushes entsprechend lange dauern. Hier hebe ich gezielt das Request-Buffer-Limit und die Timeouts an, was ich für keinen anderen Backend tun möchte:

kind: BackendTrafficPolicy
metadata:
  name: zot-timeouts
spec:
  targetRefs:
    - { kind: HTTPRoute, name: zot }
  requestBuffer:
    limit: 2Gi
  timeout:
    http:
      requestTimeout: 600s
      connectionIdleTimeout: 3600s

Diese chirurgische Präzision — eine spezielle Einstellung exakt an einem Backend, der Rest unberührt — ist genau das, was die alte annotationsbasierte Ingress-Welt so mühsam machte.

Eine Betriebs-Eigenheit am Rande: Das CRD-Bundle der Gateway-API ist mit rund 2 MiB größer als das 1-MiB-Limit eines Helm-Release-Secrets. Deshalb installiere ich die CRDs nicht über den Chart, sondern „vendore“ sie als eigene Datei ins Repo und lasse sie von einer separaten Flux-Kustomization anwenden — eine kleine, aber wichtige Reihenfolge-Abhängigkeit beim Bootstrap.

Fazit

Envoy Gateway ist bei mir weit mehr als ein Ingress-Ersatz: Es ist die zentrale Richtungsentscheidung am Edge. Drei sauber getrennte Erreichbarkeits-Domänen, zentrales OIDC für Dienste, die es selbst nie könnten, und backend-genaues Feintuning — alles in typisierten, GitOps-tauglichen Ressourcen statt in Annotation-Magie. Traefik hat mir den Weg in die Gateway-API geebnet; geblieben bin ich bei der API, nicht beim Controller.