How-to Guides¶
Scope
Task recipes for Envoy Gateway v1.9: install, upgrade, routing, TLS, rate limiting, authentication, Ingress-NGINX migration, standalone mode, debugging, and troubleshooting. Commands assume EG v1.9.1 and Gateway API v1.6. Version facts are in Reference. Background is in Explanation.
Install with Helm¶
Install the chart from Docker Hub's OCI registry. It bundles the Gateway API CRDs (experimental channel) and the EG CRDs.
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.9.1 -n envoy-gateway-system --create-namespace
kubectl wait --timeout=5m -n envoy-gateway-system \
deployment/envoy-gateway --for=condition=Available
kubectl get pods -n envoy-gateway-system
kubectl get gatewayclass
Deploy the upstream quickstart (GatewayClass eg, Gateway, HTTPRoute, and a sample backend) to check the data path:
kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v1.9.1/quickstart.yaml -n default
Install CRDs Separately¶
Use this when a platform tool or the cloud provider manages Gateway API CRDs, or when you want the standard channel only.
helm template eg oci://docker.io/envoyproxy/gateway-crds-helm \
--version v1.9.1 \
--set crds.gatewayAPI.enabled=true \
--set crds.gatewayAPI.channel=standard \
--set crds.envoyGateway.enabled=true \
| kubectl apply --server-side -f -
helm install eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.9.1 -n envoy-gateway-system --create-namespace \
--set crds.enabled=false
helm template | kubectl apply --server-side avoids a Helm limitation with very large CRDs. If the provider already manages compatible Gateway API CRDs (for example GKE), set crds.gatewayAPI.enabled=false and install only the EG CRDs.
Gateway API version skew
Gateway API CRDs are cluster-scoped. EG v1.9 bundles v1.6.1, while Linkerd 2.20 documents 1.2.1 to 1.5.1. When a mesh shares the cluster, manage the CRDs outside the chart and pick a version every controller supports. See the Linkerd topic.
Enable Optional APIs¶
Backend, EnvoyPatchPolicy, and Lua are off by default:
helm upgrade eg oci://docker.io/envoyproxy/gateway-helm --version v1.9.1 \
-n envoy-gateway-system --reuse-values \
--set config.envoyGateway.extensionApis.enableBackend=true \
--set config.envoyGateway.extensionApis.enableEnvoyPatchPolicy=true
Warning
Enable EnvoyPatchPolicy only when you trust everyone who can create it. Patch authors can inject arbitrary Envoy configuration.
Upgrade Envoy Gateway¶
Helm does not upgrade CRDs, so upgrade them first, then the controller.
# 1. Gateway API and EG CRDs
helm template eg-crds oci://docker.io/envoyproxy/gateway-crds-helm \
--version v1.9.1 \
--set crds.gatewayAPI.enabled=true \
--set crds.envoyGateway.enabled=true \
| kubectl apply --force-conflicts --server-side -f -
# 2. Controller
helm upgrade eg oci://docker.io/envoyproxy/gateway-helm \
--version v1.9.1 -n envoy-gateway-system
Upgrading to v1.9
- Skip v1.9.0. Users on v1.8.x should go straight to v1.9.1. v1.9.0 changed SDS/RDS initial fetch timeouts, and the v1.9.1 revert can hit an Envoy bug on proxies still running during the upgrade.
- Gateway API v1.6 CRDs are required. EG v1.9 reconciles TCPRoute and UDPRoute as
gateway.networking.k8s.io/v1. Standard-channel users must change those manifests tov1before upgrading the CRDs, or traffic for those routes is dropped. - Lua is now opt-in. Set
extensionApis.enableLua: trueif you use Lua EnvoyExtensionPolicies. - OIDC sessions reset. v1.9.1 and v1.8.4 switch session cookies to AES-256-GCM, so logged-in users re-authenticate once.
After the CRD upgrade, migrate stored TCPRoute and UDPRoute objects to the new storage version:
kubectl get tcproutes.gateway.networking.k8s.io -A -o json | kubectl replace -f -
kubectl get udproutes.gateway.networking.k8s.io -A -o json | kubectl replace -f -
Expose an HTTP Service¶
A Gateway with an HTTP listener and an HTTPRoute that sends /api on app.example.com to a Service:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
spec:
gatewayClassName: eg
listeners:
- name: http
protocol: HTTP
port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: myapp-route
spec:
parentRefs:
- name: my-gateway
hostnames: ["app.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: myapp-svc
port: 8080
Check that the route was accepted and find the external address:
kubectl get gateway my-gateway -o jsonpath='{.status.addresses[0].value}'
kubectl get httproute myapp-route -o jsonpath='{.status.parents[0].conditions}'
Configure TLS Termination¶
Store the certificate as a kubernetes.io/tls Secret in the Gateway's namespace and reference it from an HTTPS listener:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: eg
spec:
gatewayClassName: eg
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: "app.example.com"
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: example-cert
For TLS passthrough, use a listener with protocol: TLS, tls.mode: Passthrough, and a TLSRoute (gateway.networking.k8s.io/v1 since Gateway API v1.5).
Split Traffic for a Canary¶
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: canary
spec:
parentRefs:
- name: my-gateway
rules:
- backendRefs:
- name: myapp-v1
port: 8080
weight: 90
- name: myapp-v2
port: 8080
weight: 10
Shift the weights step by step, and watch error rates on myapp-v2 before each step.
Add Rate Limiting¶
Local (per Envoy replica)¶
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: local-rate-limit
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: myapp-route
rateLimit:
local:
rules:
- clientSelectors:
- headers:
- name: x-user-id
value: one
limit:
requests: 3
unit: Hour
Global (shared across replicas)¶
First point EG at a Redis instance, which deploys envoy-ratelimit:
helm upgrade eg oci://docker.io/envoyproxy/gateway-helm \
--set config.envoyGateway.rateLimit.backend.type=Redis \
--set config.envoyGateway.rateLimit.backend.redis.url="redis.redis-system.svc.cluster.local:6379" \
--reuse-values -n envoy-gateway-system
Then attach a global rule:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
name: global-rate-limit
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: myapp-route
rateLimit:
global:
rules:
- limit:
requests: 100
unit: Minute
rateLimit.type: Global still works but is deprecated. Set global and/or local directly. Since v1.9, redis.urlRef can read the URL from a Secret in envoy-gateway-system.
Require a JWT¶
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: jwt-auth
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: myapp-route
jwt:
providers:
- name: auth0
issuer: https://my-tenant.auth0.com/
remoteJWKS:
uri: https://my-tenant.auth0.com/.well-known/jwks.json
claimToHeaders:
- claim: sub
header: x-subject
Target the Gateway instead of the HTTPRoute to protect every route on it. Add recomputeRoute: true to a provider when later route matches depend on headers derived from claims.
Delegate to an External Authorization Service¶
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: ext-auth
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: myapp-route
extAuth:
http:
backendRefs:
- name: http-ext-auth
port: 9002
headersToBackend: ["x-current-user"]
Use extAuth.grpc.backendRefs for a gRPC ext_authz server. To encrypt the hop to the auth service, attach a BackendTLSPolicy to its Service (next section). failOpen and statusOnError control behavior when the service is down.
Enable CORS¶
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: cors-example
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: myapp-route
cors:
allowOrigins:
- "https://*.example.com"
allowMethods: [GET, POST]
allowHeaders: ["x-header-1"]
allowCredentials: true
Encrypt Traffic to Backends¶
TLS with BackendTLSPolicy¶
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
name: enable-backend-tls
spec:
targetRefs:
- group: ""
kind: Service
name: tls-backend
sectionName: https
validation:
hostname: www.example.com
caCertificateRefs:
- name: example-ca
group: ""
kind: ConfigMap
subjectAltNames:
- type: Hostname
hostname: san.example.com
On Gateway API releases before v1.4 the API version was v1alpha3.
Mutual TLS with the Backend CRD¶
Requires extensionApis.enableBackend=true. The gateway presents gateway-client-cert and validates the backend against backend-ca.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: Backend
metadata:
name: mtls-backend
spec:
endpoints:
- fqdn:
hostname: secure-backend.default.svc.cluster.local
port: 443
tls:
clientCertificateRef:
kind: Secret
name: gateway-client-cert
caCertificateRefs:
- group: ""
kind: ConfigMap
name: backend-ca
Reference it from a route with backendRefs: [{group: gateway.envoyproxy.io, kind: Backend, name: mtls-backend}].
Migrate from Ingress-NGINX¶
Ingress-NGINX is retired (maintenance ended March 2026), so plan a parallel cut-over instead of an in-place swap.
- Install EG next to the existing controller (see Install with Helm).
-
Install ingress2gateway 1.0:
-
Convert Ingress objects and annotations, emitting EG policies where Gateway API has no equivalent:
-
Review the warnings. Snippet annotations (
configuration-snippet,server-snippet) and custom Lua have no automatic translation. Rebuild them with SecurityPolicy, BackendTrafficPolicy, HTTPRouteFilter, or, as a last resort, EnvoyPatchPolicy. - Set
gatewayClassName: egon the generated Gateways, apply them, and test each hostname against the new Gateway address withcurl --resolve. - Move DNS, or the load balancer, host by host. Remove the Ingress objects and the old controller after a quiet period.
Run in Standalone Mode¶
Experimental; upstream says not to use it in production. This runs EG on a host with the File provider and Envoy as a local process.
mkdir -p /tmp/envoy-gateway-test
envoy-gateway certgen --local
cat > standalone.yaml <<'EOF'
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyGateway
gateway:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
provider:
type: Custom
custom:
resource:
type: File
file:
paths: ["/tmp/envoy-gateway-test"]
infrastructure:
type: Host
host: {}
logging:
level:
default: info
extensionApis:
enableBackend: true
EOF
envoy-gateway server --config-path standalone.yaml
Drop Gateway API manifests into /tmp/envoy-gateway-test/ (the repository ships examples/standalone/quickstart.yaml). Each change triggers an update. Routes point at local endpoints through Backend resources.
Debug with egctl¶
# Offline: what xDS would this manifest produce?
egctl x translate --from gateway-api --to xds -t route -f my-routes.yaml
# Live: dump config from a proxy pod
egctl config envoy-proxy route -n envoy-gateway-system <envoy-pod>
egctl config envoy-proxy cluster -n envoy-gateway-system <envoy-pod>
# Status conditions across all resources
egctl x status all -A
# Controller view of translated resources (v1.8+)
egctl config envoy-gateway all -n envoy-gateway-system
Watch xdsNACKTotal (v1.9) and the Envoy envoy_sds_init_fetch_timeout counter to catch configs that Envoy rejected or never received.
Troubleshoot Common Issues¶
| Symptom | Diagnosis | Fix |
|---|---|---|
| Route not working | kubectl get httproute -o yaml, check status.parents[].conditions |
Fix parentRefs, sectionName, hostnames, or listener allowedRoutes |
| Route accepted but shadowed | RouteRulesOverlap warning condition (v1.9) |
Remove the duplicate match or make it more specific |
| TLS handshake errors | Gateway listener condition ResolvedRefs, Secret contents |
Put the cert and key in the Gateway's namespace, or add a ReferenceGrant |
| 503 responses | kubectl get endpointslice -l kubernetes.io/service-name=<svc> |
Fix backend readiness and Service selectors and ports |
| Policy has no effect | kubectl get backendtrafficpolicy <name> -o yaml, check ancestor conditions |
Check targetRefs; a more specific policy may override it |
| TCPRoute or UDPRoute ignored after upgrade | Gateway API CRD version | Install the v1.6 CRDs and use apiVersion: gateway.networking.k8s.io/v1 |
| Lua policy rejected after v1.9 upgrade | Policy status | Set extensionApis.enableLua: true |
| Config change not applied on proxies | xdsNACKTotal, egctl config envoy-proxy all |
Fix the policy that produced the rejected config; pinned Envoy image mismatches cause this too |
Sources¶
- Envoy Gateway Quickstart
- Install with Helm and Install with YAML (upgrade steps)
- v1.9 release announcement and v1.9.1 notes
- Global rate limit task, Local rate limit task
- CORS task, External authorization task, Backend TLS task
- Standalone Deployment Mode
- egctl
- ingress2gateway README and Ingress2Gateway 1.0 announcement