Copyright © KC Green

Envoy Gateway and the Move from Traefik to Gateway API

 infrastructure 

Three shared gateways replace Traefik-style CRDs — with Headscale as a worked example (public API, private UI).

The lab used to run Traefik with its own CRDs (IngressRoute, Middleware, and friends). Gateway API standardises routes; Envoy Gateway is the controller here — one Helm install, three shared Gateways, and per-app HTTPRoute resources instead of Traefik-only objects.

Traefik is excellent at the edge, but its CRDs are controller-specific. Moving to Gateway API was less about feature envy and more about portability: the same HTTPRoute can be read by another implementation if you ever switch controllers. Envoy Gateway is the implementation here; the routes stay standard Kubernetes objects.

Traefik habits vs Gateway API

Traefik Gateway API
IngressRoute + entrypoints HTTPRoute + parentRefs on a Gateway listener
Per-app TLS / middleware CRDs TLS on the Gateway; filters on HTTPRoute; Envoy policies when needed
Traefik host rules hostnames and matches on gateway.networking.k8s.io/v1

Routes stay portable across controllers. Envoy-specific tuning uses gateway.envoyproxy.io (BackendTrafficPolicy, ClientTrafficPolicy, …).

Three shared gateways

Envoy Gateway (Helm)
  shared-gateway-external   *.this-is-fine.io, *.this-is-fine.social
                            Let's Encrypt, LB .0.1
  shared-gateway-internal   *.this-is-fine.internal
                            Vault PKI, LB .0.2
  shared-gateway-tailnet    *.tif.internal
                            Vault PKI, LB .0.3

Pick the gateway by who connects: the internet, lab browsers on the internal CA, or tailnet clients. Templates under k8s/templates/gateway-api/ repeat the same HTTPRoute shape for each class.

Static LoadBalancer IPs (.0.1, .0.2, .0.3) come from a small pool advertised with Cilium BGP. DNS and certificates differ per zone, but the dataplane pattern is the same: one Envoy deployment, several logical gateways, many HTTPRoutes attached to listeners.

Headscale example (public API, private UI)

Headscale disables Helm ingress; exposure is all Gateway API.

Surface Hostname Gateway Backend
Tailscale clients / API ts.this-is-fine.io shared-gateway-external :8080
Web UI (operators) ts.this-is-fine.internal shared-gateway-internal :8081 (UI sidecar)

The external route allows CORS from https://ts.this-is-fine.internal. Control traffic needs WebSocket upgrades; a BackendTrafficPolicy on that HTTPRoute enables tailscale-control-protocol and derp.

Splitting API and UI across external and internal DNS is deliberate. Tailscale clients and the public internet need ts.this-is-fine.io with a certificate browsers and devices trust. Operators can open the same logical service on ts.this-is-fine.internal with the lab’s internal CA, without publishing the admin UI to the open internet.

Public API (lab pattern):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: headscale-https-external
spec:
  parentRefs:
    - name: shared-gateway-external
      namespace: envoy-gateway-system
      sectionName: this-is-fine-io-https
  hostnames:
    - ts.this-is-fine.io
  rules:
    - backendRefs:
        - name: headscale
          port: 8080

Internal UI — same host label on the internal zone, different listener and port:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: headscale-ui-https-internal
spec:
  parentRefs:
    - name: shared-gateway-internal
      namespace: envoy-gateway-system
      sectionName: this-is-fine-internal-https
  hostnames:
    - ts.this-is-fine.internal
  rules:
    - backendRefs:
        - name: headscale
          port: 8081

Helm sets HEADSCALE_SERVER_URL=https://ts.this-is-fine.io so clients and the UI agree on the public URL while operators open the UI on the internal name.

TODO: connect Pocket ID for Headscale OIDC (HEADSCALE_OIDC_* is stubbed in chart values; not enabled yet). Same backlog as Zeroclaw UI and Vault.

Adding another app

  1. Expose the workload with a Service.
  2. Attach an HTTPRoute (or TCPRoute) to the right shared-gateway-* and hostname.
  3. Let cert-manager issue TLS via the gateway certificateRefs / ClusterIssuers.

IRC on port 6697 uses TCPRoute on the public gateway — IRC post. VIPs .0.1.0.3 are advertised with Cilium BGP. GitOps context: lab overview.