{"schemaVersion":1,"id":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium","title":"Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium","description":"An analysis of migrating 200+ services from nginx-ingress to Gateway API with Envoy Gateway and Cilium mesh. HTTPRoute, TLS passthrough, traffic splitting, and why the Ingress resource was always a limited abstraction.","language":"en","datePublished":"2026-02-06","dateModified":null,"contentFormat":"text/markdown","content":"Infrastructure\n\n# Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium\n\n2026-02-068 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/kubernetes-gateway-api-envoy-cilium>)\n\n## Limitations of the Ingress Resource\n\nThe Ingress resource was intentionally minimal. It handled HTTP routing (host + path -\\> backend) and TLS termination. Everything else, rate limiting, authentication, rewriting, CORS, WebSocket support, gRPC, TCP/UDP routing, canary deployments, was pushed to controller-specific annotations.\n\n```yaml\n# A typical production Ingress with controller-specific annotations\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: api-gateway\n annotations:\n # nginx-specific - won't work with traefik or any other controller\n nginx.ingress.kubernetes.io/ssl-redirect: \"true\"\n nginx.ingress.kubernetes.io/proxy-body-size: \"50m\"\n nginx.ingress.kubernetes.io/proxy-read-timeout: \"120\"\n nginx.ingress.kubernetes.io/cors-allow-origin: \"https://app.example.com\"\n nginx.ingress.kubernetes.io/cors-allow-methods: \"GET, POST, PUT, DELETE\"\n nginx.ingress.kubernetes.io/rate-limit-rps: \"100\"\n nginx.ingress.kubernetes.io/configuration-snippet: |\n more_set_headers \"X-Request-ID: $req_id\";\n if ($http_upgrade = \"websocket\") {\n proxy_pass http://upstream;\n }\n nginx.ingress.kubernetes.io/server-snippet: |\n location /health {\n return 200 'ok';\n add_header Content-Type text/plain;\n }\nspec:\n tls:\n - hosts:\n - api.example.com\n secretName: api-tls\n rules:\n - host: api.example.com\n http:\n paths:\n - path: /v1\n pathType: Prefix\n backend:\n service:\n name: api-v1\n port:\n number: 8080\n```\n\nThis configuration is problematic. The annotations are unvalidated strings. The ` configuration-snippet ` is raw nginx config that gets injected verbatim, a security vulnerability if any annotation can be set by untrusted users (CVE-2021-25742 was exactly this). And none of it is portable across controllers.\n\n## Gateway API: The Three-Resource Model\n\n[Gateway API](<https://gateway-api.sigs.k8s.io/>) replaces the single Ingress resource with a three-layer model that maps to organizational roles:\n\n```\n┌─────────────────────────────────────────────────┐\n│ GatewayClass (Cluster admin) │\n│ \"Which controller implementation to use\" │\n│ Created once per cluster │\n├─────────────────────────────────────────────────┤\n│ Gateway (Platform team / SRE) │\n│ \"Where to listen, TLS config, allowed routes\" │\n│ One per entry point (external, internal, etc.) │\n├─────────────────────────────────────────────────┤\n│ HTTPRoute / TLSRoute / GRPCRoute (App team) │\n│ \"How to route traffic to my service\" │\n│ One per service or group of services │\n└─────────────────────────────────────────────────┘\n```\n\nThis separation is the most significant architectural advantage. The platform team controls the Gateway, ports, TLS certificates, allowed namespaces. App teams create Routes in their own namespaces. No more \"I need cluster-admin to add an annotation to my Ingress.\"\n\nThe following is the Gateway API equivalent of the nginx-ingress configuration above:\n\n```yaml\n# GatewayClass - cluster admin sets this up once\napiVersion: gateway.networking.k8s.io/v1\nkind: GatewayClass\nmetadata:\n name: envoy\nspec:\n controllerName: gateway.envoyproxy.io/gatewayclass-controller\n---\n# Gateway - platform team manages this\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n name: external\n namespace: gateway-infra\nspec:\n gatewayClassName: envoy\n listeners:\n - name: https\n port: 443\n protocol: HTTPS\n tls:\n mode: Terminate\n certificateRefs:\n - name: wildcard-tls\n namespace: cert-manager\n allowedRoutes:\n namespaces:\n from: Selector\n selector:\n matchLabels:\n gateway-access: \"true\"\n---\n# HTTPRoute - app team creates this in their namespace\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n name: api-v1\n namespace: api-team\nspec:\n parentRefs:\n - name: external\n namespace: gateway-infra\n hostnames:\n - \"api.example.com\"\n rules:\n - matches:\n - path:\n type: PathPrefix\n value: /v1\n backendRefs:\n - name: api-v1\n port: 8080\n filters:\n - type: RequestHeaderModifier\n requestHeaderModifier:\n set:\n - name: X-Request-ID\n value: \"%REQ(X-REQUEST-ID)%\"\n```\n\nEvery field is validated by the Kubernetes API server. Typos are rejected at apply time, not silently ignored. The route references its parent Gateway explicitly. And the same HTTPRoute works with Envoy Gateway, Cilium, Istio, or any conformant implementation.\n\n## Traffic Splitting: Canary Without the Tooling\n\nThis is where Gateway API provides immediate value. Using Flagger + nginx-ingress for canary deployments meant running a custom controller that manipulated annotations to shift traffic. With Gateway API, traffic splitting is native:\n\n```yaml\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n name: api-canary\n namespace: api-team\nspec:\n parentRefs:\n - name: external\n namespace: gateway-infra\n hostnames:\n - \"api.example.com\"\n rules:\n - matches:\n - path:\n type: PathPrefix\n value: /v2\n backendRefs:\n - name: api-v2-stable\n port: 8080\n weight: 95\n - name: api-v2-canary\n port: 8080\n weight: 5\n```\n\nFive percent of traffic goes to the canary. Adjust the weights and reapply. No custom controllers, no annotation gymnastics. This can be automated with a simple script that adjusts weights based on error rates from Prometheus:\n\n```bash\n#!/bin/bash\n# Progressive canary rollout\nCANARY_ERRORS=$(curl -s \"http://prometheus:9090/api/v1/query?query=rate(http_requests_total{service='api-v2-canary',status=~'5..'}[5m])\" | jq '.data.result[0].value[1]' -r)\nCANARY_TOTAL=$(curl -s \"http://prometheus:9090/api/v1/query?query=rate(http_requests_total{service='api-v2-canary'}[5m])\" | jq '.data.result[0].value[1]' -r)\n\nERROR_RATE=$(echo \"$CANARY_ERRORS / $CANARY_TOTAL\" | bc -l)\n\nif (( $(echo \"$ERROR_RATE > 0.01\" | bc -l) )); then\n echo \"Error rate ${ERROR_RATE} exceeds 1%, rolling back\"\n kubectl patch httproute api-canary -n api-team --type=merge \\\n -p '{\"spec\":{\"rules\":[{\"matches\":[{\"path\":{\"type\":\"PathPrefix\",\"value\":\"/v2\"}}],\"backendRefs\":[{\"name\":\"api-v2-stable\",\"port\":8080,\"weight\":100},{\"name\":\"api-v2-canary\",\"port\":8080,\"weight\":0}]}]}}'\nelse\n CURRENT_WEIGHT=$(kubectl get httproute api-canary -n api-team -o jsonpath='{.spec.rules[0].backendRefs[1].weight}')\n NEW_WEIGHT=$((CURRENT_WEIGHT + 10))\n if [ $NEW_WEIGHT -gt 100 ]; then NEW_WEIGHT=100; fi\n echo \"Increasing canary weight to ${NEW_WEIGHT}%\"\n # Update weights...\nfi\n```\n\n## TLS Passthrough: The Thing Ingress Never Did Right\n\nTLS passthrough, forwarding encrypted traffic without terminating TLS at the gateway, was a constant headache with Ingress. nginx-ingress required ` nginx.ingress.kubernetes.io/ssl-passthrough: \"true\" `, which used a separate TCP listener and couldn't coexist with TLS termination on the same port.\n\nGateway API handles this with TLSRoute and Gateway listener modes:\n\n```yaml\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n name: external\n namespace: gateway-infra\nspec:\n gatewayClassName: envoy\n listeners:\n # Regular HTTPS with TLS termination\n - name: https-terminate\n port: 443\n protocol: HTTPS\n tls:\n mode: Terminate\n certificateRefs:\n - name: wildcard-tls\n allowedRoutes:\n kinds:\n - kind: HTTPRoute\n\n # TLS passthrough for services that handle their own TLS\n - name: tls-passthrough\n port: 443\n protocol: TLS\n tls:\n mode: Passthrough\n hostname: \"vault.example.com\"\n allowedRoutes:\n kinds:\n - kind: TLSRoute\n---\napiVersion: gateway.networking.k8s.io/v1alpha2\nkind: TLSRoute\nmetadata:\n name: vault-passthrough\n namespace: vault\nspec:\n parentRefs:\n - name: external\n namespace: gateway-infra\n sectionName: tls-passthrough\n rules:\n - backendRefs:\n - name: vault\n port: 8200\n```\n\nBoth listeners share port 443. The Gateway inspects the SNI (Server Name Indication) in the TLS ClientHello to decide whether to terminate or pass through. This is handled at the protocol level, no annotation hacks.\n\n## Envoy Gateway as Primary Implementation\n\n[Envoy Gateway](<https://gateway.envoyproxy.io/>) serves well for external traffic because of its Envoy Proxy foundation and the ExtensionPolicy CRD for advanced features:\n\n```yaml\n# Rate limiting per route\napiVersion: gateway.envoyproxy.io/v1alpha1\nkind: BackendTrafficPolicy\nmetadata:\n name: api-rate-limit\n namespace: api-team\nspec:\n targetRefs:\n - group: gateway.networking.k8s.io\n kind: HTTPRoute\n name: api-v1\n rateLimit:\n type: Global\n global:\n rules:\n - clientSelectors:\n - headers:\n - name: X-API-Key\n type: Distinct\n limit:\n requests: 1000\n unit: Minute\n---\n# Circuit breaking\napiVersion: gateway.envoyproxy.io/v1alpha1\nkind: BackendTrafficPolicy\nmetadata:\n name: api-circuit-breaker\n namespace: api-team\nspec:\n targetRefs:\n - group: gateway.networking.k8s.io\n kind: HTTPRoute\n name: api-v1\n circuitBreaker:\n maxConnections: 1024\n maxPendingRequests: 128\n maxRequests: 1024\n timeout:\n http:\n connectionIdleTimeout: 60s\n maxConnectionDuration: 300s\n retry:\n numRetries: 3\n retryOn:\n - \"5xx\"\n - \"reset\"\n - \"connect-failure\"\n perRetry:\n timeout: 2s\n```\n\n## Cilium: Service Mesh Without the Sidecar Tax\n\nFor east-west (service-to-service) traffic, [Cilium](<https://cilium.io/>) with Gateway API is an effective choice. Cilium's eBPF-based approach means no sidecar proxies, the networking logic runs in the kernel:\n\n```yaml\n# Cilium Gateway for internal traffic\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n name: internal\n namespace: gateway-infra\n annotations:\n # Cilium-specific: use eBPF for L7 filtering\n io.cilium/lb-ipam-ips: \"10.0.100.50\"\nspec:\n gatewayClassName: cilium\n listeners:\n - name: http\n port: 80\n protocol: HTTP\n allowedRoutes:\n namespaces:\n from: All\n---\n# Internal route with header-based routing\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n name: user-service\n namespace: user-team\nspec:\n parentRefs:\n - name: internal\n namespace: gateway-infra\n hostnames:\n - \"user-service.internal\"\n rules:\n # Route beta traffic to v2\n - matches:\n - headers:\n - name: X-Beta-User\n value: \"true\"\n backendRefs:\n - name: user-service-v2\n port: 8080\n # Default to stable\n - backendRefs:\n - name: user-service-v1\n port: 8080\n```\n\nThe performance difference between Cilium (eBPF) and sidecar-based meshes (Istio with Envoy sidecars) is measurable. On a latency-sensitive service handling 50K RPS:\n\n| Metric | No mesh | Istio sidecar | Cilium eBPF |\n| --- | --- | --- | --- |\n| P50 latency | 2.1ms | 3.4ms | 2.3ms |\n| P99 latency | 8.7ms | 14.2ms | 9.1ms |\n| Memory per pod | baseline | +50MB (sidecar) | +0MB |\n| CPU per pod | baseline | +0.1 core (sidecar) | +~0 |\n\nThe sidecar overhead is 50MB RAM and 0.1 CPU core per pod. Across 200 services with 3 replicas each, that's 30GB of RAM and 60 CPU cores just for the mesh. Cilium eliminates this entirely.\n\n## Migration Approach: 200 Services in 6 Weeks\n\nIncremental migration works well. The key insight: Gateway API and Ingress can coexist in the same cluster because they use different resources.\n\nWeek 1-2: Deploy Envoy Gateway alongside nginx-ingress. Create the GatewayClass and Gateway resources. Migrate 5 non-critical services.\n\nWeek 3-4: Migrate all stateless API services. Validate with synthetic traffic before switching DNS.\n\nWeek 5: Migrate services with WebSocket and gRPC requirements. These were actually easier with Gateway API because HTTPRoute natively supports WebSocket upgrades and GRPCRoute exists.\n\nWeek 6: Migrate remaining services, decommission nginx-ingress.\n\nA notable finding: 23 Ingress resources were discovered with broken annotations that nobody had noticed. Typos, deprecated annotation names, annotations for the wrong controller. All silently doing nothing. Gateway API would have rejected them at creation time.\n\n## What Gateway API Still Lacks\n\nSeveral gaps remain:\n\n**Rate limiting isn't standardized.** Each implementation has its own CRDs (Envoy Gateway's BackendTrafficPolicy, Cilium's CiliumNetworkPolicy). The GEP-1731 proposal for policy attachment is still evolving.\n\n**Authentication/authorization at the gateway level** is implementation-specific. Envoy Gateway has SecurityPolicy, Istio has AuthorizationPolicy, Cilium uses its own CRDs. There's no standard HTTPRoute filter for JWT validation or OAuth2.\n\n**TCP/UDP routing** works (TCPRoute, UDPRoute) but is still in the experimental channel. If you're routing non-HTTP traffic, check your implementation's support level.\n\n**Migration tooling is sparse.** Custom scripts may be needed to convert 200+ Ingress resources to HTTPRoutes. A community tool for this would be valuable, but the annotation-to-feature mapping is too controller-specific for a generic converter.\n\nDespite these gaps, Gateway API represents the right direction. The role-based security model, typed configuration, cross-implementation portability, and native traffic management features make it a clear upgrade from the Ingress approach that has been in use for seven years.\n\nFor clusters still running Ingress with annotation-heavy configurations, planning a migration is worthwhile.\n\n## Frequently asked questions\n\n### Is Gateway API ready for production?\n\nYes. Gateway API reached GA (v1.0) in October 2023 and v1.2 is current as of early 2026. Envoy Gateway, Cilium, Istio, Traefik, and Kong all have production-grade implementations. The spec is stable for core features (Gateway, HTTPRoute, TLSRoute).\n\n### Should I migrate from nginx-ingress to Gateway API?\n\nIf you're managing more than 20-30 services, yes. The role-based model (platform team manages Gateways, app teams manage Routes) alone justifies the migration. If you have 3-5 services and nginx-ingress works, the migration cost probably isn't worth it yet.\n\n### Envoy Gateway or Cilium for Gateway API?\n\nEnvoy Gateway if you need advanced traffic management (rate limiting, authentication, circuit breaking) and don't want a full service mesh. Cilium if you're already using it for CNI/networking and want Gateway API integrated with eBPF-based observability and network policies.\n\n## Tips\n\n1. **Tip:** When migrating from Ingress to Gateway API, don't convert all resources at once. Use ` parentRefs ` in HTTPRoute to bind routes to specific Gateways, you can run both Ingress and Gateway API simultaneously during migration.\n2. **Tip:** For TLS passthrough (not termination), use ` TLSRoute ` with ` mode: Passthrough ` on the Gateway listener. This is the one thing Ingress annotations never handled cleanly across controllers.\n3. **Tip:** Set ` backendRefs ` weights for traffic splitting: ` weight: 90 ` and ` weight: 10 ` gives you 90/10 canary deployment without any external tooling. The Gateway controller handles the weighted routing natively.\n4. **Tip:** With Cilium Gateway API, enable ` hubble ` for L7 observability. You get per-route latency histograms and error rates without deploying a separate observability stack.\n\n- kubernetes\n- gateway-api\n- envoy\n- cilium\n- ingress\n- service-mesh\n- httproute\n- tls\n- traffic-splitting\n- infrastructure\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#title"},{"level":2,"text":"Limitations of the Ingress Resource","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#limitations-of-the-ingress-resource"},{"level":2,"text":"Gateway API: The Three-Resource Model","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#gateway-api-the-three-resource-model"},{"level":2,"text":"Traffic Splitting: Canary Without the Tooling","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#traffic-splitting-canary-without-the-tooling"},{"level":2,"text":"TLS Passthrough: The Thing Ingress Never Did Right","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#tls-passthrough-the-thing-ingress-never-did-right"},{"level":2,"text":"Envoy Gateway as Primary Implementation","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#envoy-gateway-as-primary-implementation"},{"level":2,"text":"Cilium: Service Mesh Without the Sidecar Tax","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#cilium-service-mesh-without-the-sidecar-tax"},{"level":2,"text":"Migration Approach: 200 Services in 6 Weeks","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#migration-approach-200-services-in-6-weeks"},{"level":2,"text":"What Gateway API Still Lacks","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#what-gateway-api-still-lacks"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#questions"},{"level":3,"text":"Is Gateway API ready for production?","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#question-1"},{"level":3,"text":"Should I migrate from nginx-ingress to Gateway API?","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#question-2"},{"level":3,"text":"Envoy Gateway or Cilium for Gateway API?","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#article-tips"}],"sections":[{"heading":"Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium","level":1,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium","content":"Infrastructure"},{"heading":"Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium","level":1,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#title","content":"2026-02-068 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/kubernetes-gateway-api-envoy-cilium>)"},{"heading":"Limitations of the Ingress Resource","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#limitations-of-the-ingress-resource","content":"The Ingress resource was intentionally minimal. It handled HTTP routing (host + path -\\> backend) and TLS termination. Everything else, rate limiting, authentication, rewriting, CORS, WebSocket support, gRPC, TCP/UDP routing, canary deployments, was pushed to controller-specific annotations.\n\n```yaml\n# A typical production Ingress with controller-specific annotations\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: api-gateway\n annotations:\n # nginx-specific - won't work with traefik or any other controller\n nginx.ingress.kubernetes.io/ssl-redirect: \"true\"\n nginx.ingress.kubernetes.io/proxy-body-size: \"50m\"\n nginx.ingress.kubernetes.io/proxy-read-timeout: \"120\"\n nginx.ingress.kubernetes.io/cors-allow-origin: \"https://app.example.com\"\n nginx.ingress.kubernetes.io/cors-allow-methods: \"GET, POST, PUT, DELETE\"\n nginx.ingress.kubernetes.io/rate-limit-rps: \"100\"\n nginx.ingress.kubernetes.io/configuration-snippet: |\n more_set_headers \"X-Request-ID: $req_id\";\n if ($http_upgrade = \"websocket\") {\n proxy_pass http://upstream;\n }\n nginx.ingress.kubernetes.io/server-snippet: |\n location /health {\n return 200 'ok';\n add_header Content-Type text/plain;\n }\nspec:\n tls:\n - hosts:\n - api.example.com\n secretName: api-tls\n rules:\n - host: api.example.com\n http:\n paths:\n - path: /v1\n pathType: Prefix\n backend:\n service:\n name: api-v1\n port:\n number: 8080\n```\n\nThis configuration is problematic. The annotations are unvalidated strings. The ` configuration-snippet ` is raw nginx config that gets injected verbatim, a security vulnerability if any annotation can be set by untrusted users (CVE-2021-25742 was exactly this). And none of it is portable across controllers."},{"heading":"Gateway API: The Three-Resource Model","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#gateway-api-the-three-resource-model","content":"[Gateway API](<https://gateway-api.sigs.k8s.io/>) replaces the single Ingress resource with a three-layer model that maps to organizational roles:\n\n```\n┌─────────────────────────────────────────────────┐\n│ GatewayClass (Cluster admin) │\n│ \"Which controller implementation to use\" │\n│ Created once per cluster │\n├─────────────────────────────────────────────────┤\n│ Gateway (Platform team / SRE) │\n│ \"Where to listen, TLS config, allowed routes\" │\n│ One per entry point (external, internal, etc.) │\n├─────────────────────────────────────────────────┤\n│ HTTPRoute / TLSRoute / GRPCRoute (App team) │\n│ \"How to route traffic to my service\" │\n│ One per service or group of services │\n└─────────────────────────────────────────────────┘\n```\n\nThis separation is the most significant architectural advantage. The platform team controls the Gateway, ports, TLS certificates, allowed namespaces. App teams create Routes in their own namespaces. No more \"I need cluster-admin to add an annotation to my Ingress.\"\n\nThe following is the Gateway API equivalent of the nginx-ingress configuration above:\n\n```yaml\n# GatewayClass - cluster admin sets this up once\napiVersion: gateway.networking.k8s.io/v1\nkind: GatewayClass\nmetadata:\n name: envoy\nspec:\n controllerName: gateway.envoyproxy.io/gatewayclass-controller\n---\n# Gateway - platform team manages this\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n name: external\n namespace: gateway-infra\nspec:\n gatewayClassName: envoy\n listeners:\n - name: https\n port: 443\n protocol: HTTPS\n tls:\n mode: Terminate\n certificateRefs:\n - name: wildcard-tls\n namespace: cert-manager\n allowedRoutes:\n namespaces:\n from: Selector\n selector:\n matchLabels:\n gateway-access: \"true\"\n---\n# HTTPRoute - app team creates this in their namespace\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n name: api-v1\n namespace: api-team\nspec:\n parentRefs:\n - name: external\n namespace: gateway-infra\n hostnames:\n - \"api.example.com\"\n rules:\n - matches:\n - path:\n type: PathPrefix\n value: /v1\n backendRefs:\n - name: api-v1\n port: 8080\n filters:\n - type: RequestHeaderModifier\n requestHeaderModifier:\n set:\n - name: X-Request-ID\n value: \"%REQ(X-REQUEST-ID)%\"\n```\n\nEvery field is validated by the Kubernetes API server. Typos are rejected at apply time, not silently ignored. The route references its parent Gateway explicitly. And the same HTTPRoute works with Envoy Gateway, Cilium, Istio, or any conformant implementation."},{"heading":"Traffic Splitting: Canary Without the Tooling","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#traffic-splitting-canary-without-the-tooling","content":"This is where Gateway API provides immediate value. Using Flagger + nginx-ingress for canary deployments meant running a custom controller that manipulated annotations to shift traffic. With Gateway API, traffic splitting is native:\n\n```yaml\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n name: api-canary\n namespace: api-team\nspec:\n parentRefs:\n - name: external\n namespace: gateway-infra\n hostnames:\n - \"api.example.com\"\n rules:\n - matches:\n - path:\n type: PathPrefix\n value: /v2\n backendRefs:\n - name: api-v2-stable\n port: 8080\n weight: 95\n - name: api-v2-canary\n port: 8080\n weight: 5\n```\n\nFive percent of traffic goes to the canary. Adjust the weights and reapply. No custom controllers, no annotation gymnastics. This can be automated with a simple script that adjusts weights based on error rates from Prometheus:\n\n```bash\n#!/bin/bash\n# Progressive canary rollout\nCANARY_ERRORS=$(curl -s \"http://prometheus:9090/api/v1/query?query=rate(http_requests_total{service='api-v2-canary',status=~'5..'}[5m])\" | jq '.data.result[0].value[1]' -r)\nCANARY_TOTAL=$(curl -s \"http://prometheus:9090/api/v1/query?query=rate(http_requests_total{service='api-v2-canary'}[5m])\" | jq '.data.result[0].value[1]' -r)\n\nERROR_RATE=$(echo \"$CANARY_ERRORS / $CANARY_TOTAL\" | bc -l)\n\nif (( $(echo \"$ERROR_RATE > 0.01\" | bc -l) )); then\n echo \"Error rate ${ERROR_RATE} exceeds 1%, rolling back\"\n kubectl patch httproute api-canary -n api-team --type=merge \\\n -p '{\"spec\":{\"rules\":[{\"matches\":[{\"path\":{\"type\":\"PathPrefix\",\"value\":\"/v2\"}}],\"backendRefs\":[{\"name\":\"api-v2-stable\",\"port\":8080,\"weight\":100},{\"name\":\"api-v2-canary\",\"port\":8080,\"weight\":0}]}]}}'\nelse\n CURRENT_WEIGHT=$(kubectl get httproute api-canary -n api-team -o jsonpath='{.spec.rules[0].backendRefs[1].weight}')\n NEW_WEIGHT=$((CURRENT_WEIGHT + 10))\n if [ $NEW_WEIGHT -gt 100 ]; then NEW_WEIGHT=100; fi\n echo \"Increasing canary weight to ${NEW_WEIGHT}%\"\n # Update weights...\nfi\n```"},{"heading":"TLS Passthrough: The Thing Ingress Never Did Right","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#tls-passthrough-the-thing-ingress-never-did-right","content":"TLS passthrough, forwarding encrypted traffic without terminating TLS at the gateway, was a constant headache with Ingress. nginx-ingress required ` nginx.ingress.kubernetes.io/ssl-passthrough: \"true\" `, which used a separate TCP listener and couldn't coexist with TLS termination on the same port.\n\nGateway API handles this with TLSRoute and Gateway listener modes:\n\n```yaml\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n name: external\n namespace: gateway-infra\nspec:\n gatewayClassName: envoy\n listeners:\n # Regular HTTPS with TLS termination\n - name: https-terminate\n port: 443\n protocol: HTTPS\n tls:\n mode: Terminate\n certificateRefs:\n - name: wildcard-tls\n allowedRoutes:\n kinds:\n - kind: HTTPRoute\n\n # TLS passthrough for services that handle their own TLS\n - name: tls-passthrough\n port: 443\n protocol: TLS\n tls:\n mode: Passthrough\n hostname: \"vault.example.com\"\n allowedRoutes:\n kinds:\n - kind: TLSRoute\n---\napiVersion: gateway.networking.k8s.io/v1alpha2\nkind: TLSRoute\nmetadata:\n name: vault-passthrough\n namespace: vault\nspec:\n parentRefs:\n - name: external\n namespace: gateway-infra\n sectionName: tls-passthrough\n rules:\n - backendRefs:\n - name: vault\n port: 8200\n```\n\nBoth listeners share port 443. The Gateway inspects the SNI (Server Name Indication) in the TLS ClientHello to decide whether to terminate or pass through. This is handled at the protocol level, no annotation hacks."},{"heading":"Envoy Gateway as Primary Implementation","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#envoy-gateway-as-primary-implementation","content":"[Envoy Gateway](<https://gateway.envoyproxy.io/>) serves well for external traffic because of its Envoy Proxy foundation and the ExtensionPolicy CRD for advanced features:\n\n```yaml\n# Rate limiting per route\napiVersion: gateway.envoyproxy.io/v1alpha1\nkind: BackendTrafficPolicy\nmetadata:\n name: api-rate-limit\n namespace: api-team\nspec:\n targetRefs:\n - group: gateway.networking.k8s.io\n kind: HTTPRoute\n name: api-v1\n rateLimit:\n type: Global\n global:\n rules:\n - clientSelectors:\n - headers:\n - name: X-API-Key\n type: Distinct\n limit:\n requests: 1000\n unit: Minute\n---\n# Circuit breaking\napiVersion: gateway.envoyproxy.io/v1alpha1\nkind: BackendTrafficPolicy\nmetadata:\n name: api-circuit-breaker\n namespace: api-team\nspec:\n targetRefs:\n - group: gateway.networking.k8s.io\n kind: HTTPRoute\n name: api-v1\n circuitBreaker:\n maxConnections: 1024\n maxPendingRequests: 128\n maxRequests: 1024\n timeout:\n http:\n connectionIdleTimeout: 60s\n maxConnectionDuration: 300s\n retry:\n numRetries: 3\n retryOn:\n - \"5xx\"\n - \"reset\"\n - \"connect-failure\"\n perRetry:\n timeout: 2s\n```"},{"heading":"Cilium: Service Mesh Without the Sidecar Tax","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#cilium-service-mesh-without-the-sidecar-tax","content":"For east-west (service-to-service) traffic, [Cilium](<https://cilium.io/>) with Gateway API is an effective choice. Cilium's eBPF-based approach means no sidecar proxies, the networking logic runs in the kernel:\n\n```yaml\n# Cilium Gateway for internal traffic\napiVersion: gateway.networking.k8s.io/v1\nkind: Gateway\nmetadata:\n name: internal\n namespace: gateway-infra\n annotations:\n # Cilium-specific: use eBPF for L7 filtering\n io.cilium/lb-ipam-ips: \"10.0.100.50\"\nspec:\n gatewayClassName: cilium\n listeners:\n - name: http\n port: 80\n protocol: HTTP\n allowedRoutes:\n namespaces:\n from: All\n---\n# Internal route with header-based routing\napiVersion: gateway.networking.k8s.io/v1\nkind: HTTPRoute\nmetadata:\n name: user-service\n namespace: user-team\nspec:\n parentRefs:\n - name: internal\n namespace: gateway-infra\n hostnames:\n - \"user-service.internal\"\n rules:\n # Route beta traffic to v2\n - matches:\n - headers:\n - name: X-Beta-User\n value: \"true\"\n backendRefs:\n - name: user-service-v2\n port: 8080\n # Default to stable\n - backendRefs:\n - name: user-service-v1\n port: 8080\n```\n\nThe performance difference between Cilium (eBPF) and sidecar-based meshes (Istio with Envoy sidecars) is measurable. On a latency-sensitive service handling 50K RPS:\n\n| Metric | No mesh | Istio sidecar | Cilium eBPF |\n| --- | --- | --- | --- |\n| P50 latency | 2.1ms | 3.4ms | 2.3ms |\n| P99 latency | 8.7ms | 14.2ms | 9.1ms |\n| Memory per pod | baseline | +50MB (sidecar) | +0MB |\n| CPU per pod | baseline | +0.1 core (sidecar) | +~0 |\n\nThe sidecar overhead is 50MB RAM and 0.1 CPU core per pod. Across 200 services with 3 replicas each, that's 30GB of RAM and 60 CPU cores just for the mesh. Cilium eliminates this entirely."},{"heading":"Migration Approach: 200 Services in 6 Weeks","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#migration-approach-200-services-in-6-weeks","content":"Incremental migration works well. The key insight: Gateway API and Ingress can coexist in the same cluster because they use different resources.\n\nWeek 1-2: Deploy Envoy Gateway alongside nginx-ingress. Create the GatewayClass and Gateway resources. Migrate 5 non-critical services.\n\nWeek 3-4: Migrate all stateless API services. Validate with synthetic traffic before switching DNS.\n\nWeek 5: Migrate services with WebSocket and gRPC requirements. These were actually easier with Gateway API because HTTPRoute natively supports WebSocket upgrades and GRPCRoute exists.\n\nWeek 6: Migrate remaining services, decommission nginx-ingress.\n\nA notable finding: 23 Ingress resources were discovered with broken annotations that nobody had noticed. Typos, deprecated annotation names, annotations for the wrong controller. All silently doing nothing. Gateway API would have rejected them at creation time."},{"heading":"What Gateway API Still Lacks","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#what-gateway-api-still-lacks","content":"Several gaps remain:\n\n**Rate limiting isn't standardized.** Each implementation has its own CRDs (Envoy Gateway's BackendTrafficPolicy, Cilium's CiliumNetworkPolicy). The GEP-1731 proposal for policy attachment is still evolving.\n\n**Authentication/authorization at the gateway level** is implementation-specific. Envoy Gateway has SecurityPolicy, Istio has AuthorizationPolicy, Cilium uses its own CRDs. There's no standard HTTPRoute filter for JWT validation or OAuth2.\n\n**TCP/UDP routing** works (TCPRoute, UDPRoute) but is still in the experimental channel. If you're routing non-HTTP traffic, check your implementation's support level.\n\n**Migration tooling is sparse.** Custom scripts may be needed to convert 200+ Ingress resources to HTTPRoutes. A community tool for this would be valuable, but the annotation-to-feature mapping is too controller-specific for a generic converter.\n\nDespite these gaps, Gateway API represents the right direction. The role-based security model, typed configuration, cross-implementation portability, and native traffic management features make it a clear upgrade from the Ingress approach that has been in use for seven years.\n\nFor clusters still running Ingress with annotation-heavy configurations, planning a migration is worthwhile."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#questions","content":""},{"heading":"Is Gateway API ready for production?","level":3,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#question-1","content":"Yes. Gateway API reached GA (v1.0) in October 2023 and v1.2 is current as of early 2026. Envoy Gateway, Cilium, Istio, Traefik, and Kong all have production-grade implementations. The spec is stable for core features (Gateway, HTTPRoute, TLSRoute)."},{"heading":"Should I migrate from nginx-ingress to Gateway API?","level":3,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#question-2","content":"If you're managing more than 20-30 services, yes. The role-based model (platform team manages Gateways, app teams manage Routes) alone justifies the migration. If you have 3-5 services and nginx-ingress works, the migration cost probably isn't worth it yet."},{"heading":"Envoy Gateway or Cilium for Gateway API?","level":3,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#question-3","content":"Envoy Gateway if you need advanced traffic management (rate limiting, authentication, circuit breaking) and don't want a full service mesh. Cilium if you're already using it for CNI/networking and want Gateway API integrated with eBPF-based observability and network policies."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#article-tips","content":"1. **Tip:** When migrating from Ingress to Gateway API, don't convert all resources at once. Use ` parentRefs ` in HTTPRoute to bind routes to specific Gateways, you can run both Ingress and Gateway API simultaneously during migration.\n2. **Tip:** For TLS passthrough (not termination), use ` TLSRoute ` with ` mode: Passthrough ` on the Gateway listener. This is the one thing Ingress annotations never handled cleanly across controllers.\n3. **Tip:** Set ` backendRefs ` weights for traffic splitting: ` weight: 90 ` and ` weight: 10 ` gives you 90/10 canary deployment without any external tooling. The Gateway controller handles the weighted routing natively.\n4. **Tip:** With Cilium Gateway API, enable ` hubble ` for L7 observability. You get per-route latency histograms and error rates without deploying a separate observability stack.\n\n- kubernetes\n- gateway-api\n- envoy\n- cilium\n- ingress\n- service-mesh\n- httproute\n- tls\n- traffic-splitting\n- infrastructure\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/kubernetes-gateway-api-envoy-cilium"},{"text":"Gateway API","url":"https://gateway-api.sigs.k8s.io/"},{"text":"Envoy Gateway","url":"https://gateway.envoyproxy.io/"},{"text":"Cilium","url":"https://cilium.io/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium"},"headline":"Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium","description":"An analysis of migrating 200+ services from nginx-ingress to Gateway API with Envoy Gateway and Cilium mesh. HTTPRoute, TLS passthrough, traffic splitting, and why the Ingress resource was always a limited abstraction.","datePublished":"2026-02-06","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Infrastructure","keywords":"kubernetes, gateway-api, envoy, cilium, ingress, service-mesh, httproute, tls, traffic-splitting, infrastructure","wordCount":1505,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/kubernetes-gateway-api-envoy-cilium"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Kubernetes Gateway API: Replacing Ingress with Envoy Gateway and Cilium","item":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Is Gateway API ready for production?","acceptedAnswer":{"@type":"Answer","text":"Yes. Gateway API reached GA (v1.0) in October 2023 and v1.2 is current as of early 2026. Envoy Gateway, Cilium, Istio, Traefik, and Kong all have production-grade implementations. The spec is stable for core features (Gateway, HTTPRoute, TLSRoute)."}},{"@type":"Question","name":"Should I migrate from nginx-ingress to Gateway API?","acceptedAnswer":{"@type":"Answer","text":"If you're managing more than 20-30 services, yes. The role-based model (platform team manages Gateways, app teams manage Routes) alone justifies the migration. If you have 3-5 services and nginx-ingress works, the migration cost probably isn't worth it yet."}},{"@type":"Question","name":"Envoy Gateway or Cilium for Gateway API?","acceptedAnswer":{"@type":"Answer","text":"Envoy Gateway if you need advanced traffic management (rate limiting, authentication, circuit breaking) and don't want a full service mesh. Cilium if you're already using it for CNI/networking and want Gateway API integrated with eBPF-based observability and network policies."}}]}]}],"contentHash":"fcc8c4eaf7a9f6709cb52c08b752aa9975a8e1910c04130f0647c84a1dd1a7d7","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/kubernetes-gateway-api-envoy-cilium","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":71754,"contentBytes":15514}}
