ingress-nginx served the Kubernetes ecosystem for years, powering a majority of cloud-native deployments. As of March 24, 2026, it is retired. The repository is read-only. No security patches will be issued. Teams running it in production are accumulating unpatched CVE exposure with no upstream remediation path.

This guide covers the full production migration to Envoy Gateway: auditing your current configuration, translating resources with Ingress2Gateway 1.0, configuring cert-manager for Gateway API, handling advanced annotation migration, and cutting over without downtime. It incorporates real pitfalls documented during the CNCF’s own internal cluster migration in April 2026.

Why ingress-nginx Retirement Means You Must Migrate Now

On November 11, 2025, Kubernetes SIG Network announced ingress-nginx’s retirement with a March 2026 end-of-life date. The January 2026 follow-up statement from the Kubernetes Steering and Security Response Committees confirmed the timeline and cited two compounding problems: a maintainer base reduced to 1-2 engineers working on the project in their spare time, and architectural security concerns around the snippet annotation feature, which allows arbitrary NGINX configuration injection and has been the source of multiple CVEs.

Existing deployments continue to run. Helm charts and container images remain available. But “continues to run” is not the same as “is safe to run.” With no upstream maintainers and no security response process, any new vulnerability in the ingress-nginx codebase will remain unpatched indefinitely.

The migration window is now. Every week of delay is another week of unpatched exposure.

Teams running concurrent platform upgrades can reference the Kubernetes 1.36 Production Upgrade Guide and the Helm 4 Migration Guide alongside this guide.

What Is Gateway API and How Does It Replace Kubernetes Ingress?

Gateway API is the official Kubernetes successor to the Ingress API. As of v1.5, it reached production maturity with all core route types in the Standard channel:

Route TypeGA SincePurpose
HTTPRoutev0.5.0HTTP/HTTPS routing
GRPCRoutev1.1.0gRPC with method/service matching
TLSRoutev1.5.0SNI-based TLS passthrough
TCPRouteStandard channelRaw TCP routing

The most significant difference from Ingress is role-based separation of concerns across three layers:

graph LR
    subgraph "Ingress (Legacy)"
        A["Ingress Resource\nannotations-heavy\nflat model\n(single team owns all)"]
    end

    subgraph "Gateway API"
        B["GatewayClass\nInfra Provider"] --> C["Gateway\nCluster Operator"]
        C --> D["HTTPRoute\nApp Developer"]
        C --> E["GRPCRoute\nApp Developer"]
    end

Gateway API separates ownership: infrastructure providers manage GatewayClass, cluster operators manage Gateway, application teams manage their own HTTPRoutes. No application team can modify shared gateway configuration.

With Ingress, an application team annotating their Ingress resource can affect cluster-wide routing behavior through features like use-regex. With Gateway API, each layer is independently managed with RBAC at each boundary. Application developers create HTTPRoutes pointing to a Gateway they do not own or modify.

Why Envoy Gateway specifically: Envoy Gateway is the CNCF reference implementation of Gateway API. It runs Envoy proxy under the hood, provides full conformance across all route types, and is the implementation used in the CNCF’s own infrastructure migration. The current stable release is v1.7.2 (April 17, 2026).

Which Gateway API Implementation Should You Choose?

Before migrating, decide which implementation fits your environment:

ImplementationHTTPRouteGRPCRouteTLSRouteTCPRouteNotes
Envoy GatewayCNCF reference. Full route coverage. Best for vendor-neutral.
IstioNo extra component if you run Istio already.
AWS LB Controller-ALB (L7) and NLB (L4). WAF/Shield integration.
GKE Gateway Controller---HTTPRoute only. Requires Envoy Gateway for other routes.
Azure App GW for Containers--limitedManaged NGINX add-on EOL November 2026.
kgatewayKong-backed. Strong ingress-nginx feature parity.

The key decision: cloud provider flexibility. If your workloads run on one cloud and you want native load balancer integration, the cloud-native controller is the simpler operational choice. If you need portability across clouds or run on-prem, Envoy Gateway or Istio provides full route type coverage without lock-in.

GCP teams should note the GKE Gateway Controller supports only HTTPRoute. Any workload using TLSRoute or TCPRoute on GKE requires a third-party implementation deployed alongside the GKE controller.

Step 1: Audit Your ingress-nginx Configuration

Before running any migration tooling, understand exactly what you have. The Kubernetes blog documented five specific ingress-nginx behaviors that cause silent outages during migration. Two are critical to address before touching any tooling.

Problem 1: Regex matches are prefix-based and case-insensitive.

In ingress-nginx, enabling use-regex: "true" on a path like /[A-Z]{3} matches any request starting with three letters - case-insensitively. The path /uuid matches. /UUID matches. /uuid/some/other/path also matches. Envoy-based Gateway API implementations perform full case-sensitive matches by default. A direct translation of your regex patterns will produce 404s for traffic that previously routed correctly.

The HTTPRoute fix is explicit pattern adjustment:

# HTTPRoute with explicit case-insensitive prefix matching
rules:
  - matches:
      - path:
          type: RegularExpression
          value: "(?i)/[a-z]{3}.*"

Problem 2: use-regex applies globally across all Ingress resources for a hostname.

When any Ingress for api.example.com sets use-regex: "true", every path on that hostname - across every Ingress resource in any namespace - is treated as a regex pattern. Teams often set this in one Ingress and forget. The cross-Ingress side effect creates invisible routing dependencies that produce failures when you migrate individual routes.

Before translating anything, inventory your cluster:

# Find all ingress-nginx resources
kubectl get ingress --all-namespaces

# Find snippet annotations (untranslatable by Ingress2Gateway)
kubectl get ingress --all-namespaces -o json | \
  python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data['items']:
    name = item['metadata']['name']
    ns = item['metadata']['namespace']
    ann = item['metadata'].get('annotations', {})
    for k, v in ann.items():
        if 'snippet' in k:
            print(f'{ns}/{name}: {k}')
"

# Find all Ingresses with use-regex enabled
kubectl get ingress --all-namespaces -o json | \
  python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data['items']:
    ann = item['metadata'].get('annotations', {})
    if ann.get('nginx.ingress.kubernetes.io/use-regex') == 'true':
        ns = item['metadata']['namespace']
        name = item['metadata']['name']
        print(f'{ns}/{name}')
"

Any Ingress with a configuration-snippet or server-snippet annotation requires manual rewriting. Ingress2Gateway will warn on these, but cannot translate them.

Step 2: Translate with Ingress2Gateway 1.0

Ingress2Gateway 1.0, released March 20, 2026, supports 30+ ingress-nginx annotations with behavioral equivalence integration tests. It is the official migration tool under kubernetes-sigs.

# Install
go install github.com/kubernetes-sigs/[email protected]
# or
brew install ingress2gateway

The key flag for Envoy Gateway migrations is --emitter envoy-gateway. Without it you get generic Gateway API resources. With it you also get Envoy Gateway-specific CRDs (SecurityPolicy, BackendTrafficPolicy) for annotations that map to Envoy extensions.

# Convert from live cluster
ingress2gateway print \
  --namespace production \
  --providers=ingress-nginx \
  --emitter envoy-gateway > gateway-resources.yaml

# Convert from manifest files (for pre-staging review)
ingress2gateway print \
  --input-file my-ingress.yaml \
  --providers=ingress-nginx \
  --emitter envoy-gateway > gateway-resources.yaml

The full migration workflow:

flowchart TD
    A[Audit ingress-nginx\nAnnotations and Snippets] --> B[Run ingress2gateway print\n--emitter envoy-gateway]
    B --> C{Warnings\nGenerated?}
    C -->|Yes| D[Review each warning\nRewrite untranslatable config manually]
    C -->|No| E[Apply Gateway API Resources]
    D --> E
    E --> F[Deploy Envoy Gateway\nInstall GatewayClass]
    F --> G[Route test traffic\nto new gateway]
    G --> H{Validation\nPasses?}
    H -->|No| I[Debug via Gateway\nProgrammed status]
    I --> G
    H -->|Yes| J[Remove ownerRefs\nfrom Certificates]
    J --> K[DNS Cutover]
    K --> L[Delete old\nIngress Resources]

The certificate ownerReference removal step before DNS cutover is not optional - skip it and deleting Ingress resources cascade-deletes your TLS secrets.

Always check warnings before applying:

# Check for untranslatable configuration after conversion
grep -i "warning\|unsupported\|cannot" gateway-resources.yaml

Warnings are your action items. Each one represents configuration that must be manually implemented using native Gateway API resources or Envoy Gateway extension CRDs.

Step 3: Configure cert-manager for Gateway API

cert-manager has supported Gateway API natively since v1.15, with no feature flag required. Enable it via Helm:

helm upgrade --install cert-manager \
  oci://quay.io/jetstack/charts/cert-manager \
  --namespace cert-manager \
  --set config.enableGatewayAPI=true

Restart the cert-manager Deployment after enabling - it checks for Gateway API CRD availability only at startup.

The integration model changes significantly from Ingress-based cert-manager:

sequenceDiagram
    participant Dev as App Developer
    participant CM as cert-manager
    participant GW as Gateway (cluster operator)
    participant ACME as ACME Server

    Note over Dev,ACME: Ingress model
    Dev->>CM: Annotate Ingress resource
    CM->>ACME: HTTP-01 challenge via temp Ingress
    CM->>Dev: Certificate stored in same namespace as Ingress

    Note over Dev,ACME: Gateway API model
    Dev->>GW: Cannot annotate Gateway (cluster operator owns it)
    GW->>CM: Cluster operator annotates Gateway
    CM->>ACME: HTTP-01 challenge via temp HTTPRoute
    CM->>GW: Certificate stored in Gateway namespace
    Dev->>GW: ReferenceGrant required for cross-namespace secret access

In Gateway API mode, the annotation target shifts from the Ingress resource (owned by app teams) to the Gateway resource (owned by cluster operators). This changes who manages TLS certificate issuance and cross-namespace access.

Configure a ClusterIssuer with the Gateway API HTTP-01 solver:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
      - http01:
          gatewayHTTPRoute: {}

Annotate the Gateway resource (not HTTPRoute):

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: production
  namespace: envoy-gateway-system
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: eg
  listeners:
    - name: https-api
      hostname: api.example.com
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - name: api-example-com-tls

The certificate ownerReference trap - do not skip this. cert-manager sets ownerReferences on Certificate objects pointing to the Ingress that triggered their creation. When you delete an Ingress resource, Kubernetes cascade-deletes the Certificate and its Secret. Your TLS terminates immediately.

Before deleting any Ingress resource:

# Find Certificates with Ingress ownerReferences and patch them
kubectl get certificates --all-namespaces -o json | python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data['items']:
    name = item['metadata']['name']
    ns = item['metadata']['namespace']
    for ref in item['metadata'].get('ownerReferences', []):
        if ref.get('kind') == 'Ingress':
            print(f'kubectl patch certificate {name} -n {ns} --type=json '
                  f\"-p='[{{\\\"op\\\": \\\"remove\\\", \\\"path\\\": \\\"/metadata/ownerReferences\\\"}}}]'\")"  | bash

Once ownerReferences are cleared, deleting the old Ingress resources leaves the Certificates and their Secrets intact.

Step 4: Handle Advanced Configuration

BackendTLSPolicy for HTTPS Upstreams

The ingress-nginx annotation nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" is replaced by BackendTLSPolicy (GA in Gateway API v1.5):

apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
  name: api-backend-tls
  namespace: production
spec:
  targetRefs:
    - group: ''
      kind: Service
      name: api-service
      sectionName: https
  validation:
    caCertificateRefs:
      - name: backend-ca
        group: ''
        kind: ConfigMap
    hostname: api-service.production.svc.cluster.local

This is more secure than the annotation. You explicitly specify the CA to validate against and the hostname to verify - no implicit trust of whatever certificate the backend presents.

ReferenceGrant for Cross-Namespace Routing

ingress-nginx allows Ingress resources to reference TLS secrets from any namespace. Gateway API does not. Cross-namespace access requires explicit ReferenceGrant resources:

apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
  name: allow-gateway-tls
  namespace: cert-store        # namespace where the Secret lives
spec:
  from:
    - group: gateway.networking.k8s.io
      kind: Gateway
      namespace: envoy-gateway-system   # namespace where the Gateway lives
  to:
    - group: ""
      kind: Secret

Create one ReferenceGrant per source-target namespace pair. If your Gateway lives in envoy-gateway-system and TLS secrets live in cert-store, the ReferenceGrant goes in cert-store.

Rate Limiting with BackendTrafficPolicy

ingress-nginx rate limiting annotations (limit-rps, limit-connections) have no direct Gateway API equivalent. Envoy Gateway handles rate limiting through BackendTrafficPolicy:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: api-rate-limit
  namespace: production
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: HTTPRoute
    name: api-route
  rateLimit:
    type: Global
    global:
      rules:
        - clientSelectors:
            - headers:
                - name: x-api-key
                  type: Distinct
          limit:
            requests: 100
            unit: Minute

The semantics differ from nginx rate limiting. Envoy Gateway’s rate limiting is policy-based and applied per route, not per Ingress resource. Verify your specific rate limiting requirements against Envoy Gateway’s BackendTrafficPolicy documentation before migrating - the annotation-to-policy mapping is not one-to-one.

Path Rewrite (replacing nginx rewrite-target)

The ingress-nginx rewrite-target annotation maps to an HTTPRoute URLRewrite filter:

# ingress-nginx annotation
# nginx.ingress.kubernetes.io/rewrite-target: /

# HTTPRoute equivalent
rules:
  - matches:
      - path:
          type: PathPrefix
          value: /v2/
    filters:
      - type: URLRewrite
        urlRewrite:
          path:
            type: ReplacePrefixMatch
            replacePrefixMatch: /
    backendRefs:
      - name: api-service
        port: 8080

Step 5: Deploy and Cut Over

Install Envoy Gateway

helm install eg oci://docker.io/envoyproxy/gateway-helm \
  --version v1.7.2 \
  -n envoy-gateway-system --create-namespace

kubectl wait --timeout=5m \
  -n envoy-gateway-system \
  deployment/envoy-gateway \
  --for=condition=Available

Create a GatewayClass and Gateway:

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: production
  namespace: envoy-gateway-system
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      port: 80
      protocol: HTTP
    - name: https
      hostname: "*.example.com"
      port: 443
      protocol: HTTPS
      tls:
        mode: Terminate
        certificateRefs:
          - name: wildcard-example-tls

Configure EnvoyProxy with externalTrafficPolicy Fix

The CNCF migration hit a critical failure here. Envoy Gateway defaults to externalTrafficPolicy: Local. Cloud load balancers health-check all nodes - nodes without Envoy pods fail health checks and get marked unhealthy. Depending on your cluster size and pod scheduling, this leaves the load balancer with no healthy targets and drops all traffic.

Set this explicitly before cutover:

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
  name: production-proxy
  namespace: envoy-gateway-system
spec:
  provider:
    type: Kubernetes
    kubernetes:
      envoyService:
        type: LoadBalancer
        externalTrafficPolicy: Cluster   # prevents health check failures on cloud LBs
      envoyHpa:
        minReplicas: 2
        maxReplicas: 10

Dual-Stack Running Strategy

Run ingress-nginx and Envoy Gateway simultaneously during migration. Never attempt a hard cutover.

graph TD
    subgraph "Phase 1: Deploy"
        A[ingress-nginx\nall production traffic]
        B[Envoy Gateway\ndeployed, internal DNS only]
    end
    subgraph "Phase 2: Validate"
        C[ingress-nginx\nstill primary]
        D[Envoy Gateway\nreceiving test traffic\nvia internal routing]
    end
    subgraph "Phase 3: Cutover"
        E[Envoy Gateway\nall production traffic]
        F[ingress-nginx\ndeployed, idle]
    end
    subgraph "Phase 4: Cleanup"
        G[Envoy Gateway\nall production traffic]
        H[ingress-nginx\nremoved]
    end

    A --> C
    B --> D
    C --> E
    D --> E
    E --> G
    F --> H

Maintain ingress-nginx through Phases 1-3 for instant rollback if validation fails. The idle pods cost resources, but they are your safety net.

DNS cutover timing: Lower your TTL 24-48 hours before cutover. DNS propagation takes 15-20 minutes at standard TTLs. Monitor for the full TTL period after cutover before decommissioning ingress-nginx.

Validation Checklist Before DNS Cutover

# Check Gateway Programmed status
kubectl get gateway production \
  -n envoy-gateway-system \
  -o jsonpath='{.status.conditions}' | python3 -m json.tool

# Check HTTPRoute Accepted status
kubectl get httproute api-route \
  -n production \
  -o jsonpath='{.status.parents}' | python3 -m json.tool

# Verify certificate loading against your hostname
openssl s_client \
  -connect api.example.com:443 \
  -servername api.example.com 2>&1 | \
  grep -E "subject|issuer|Verify return"

If openssl s_client returns SSL_ERROR_SYSCALL, check: listener configuration on the Gateway, ReferenceGrant presence, and certificate loading status via kubectl describe certificate.

Cloud Provider Considerations

ProviderHTTPRouteGRPCRouteTLSRouteTCPRouteNotes
AWS LB Controller-ALB (L7), NLB (L4). Custom CRDs: TargetGroupConfiguration, LoadBalancerConfiguration, ListenerRuleConfiguration. WAF/Shield integration.
GKE Gateway Controller---HTTPRoute only. Use Envoy Gateway for other route types.
Azure App Gateway for Containers--limitedManaged NGINX add-on EOL November 2026.
Envoy Gateway (self-managed)Full coverage on any cloud.

AWS: The AWS Load Balancer Controller reached GA for Gateway API in 2026. HTTPRoute and GRPCRoute map to ALB; TCPRoute and UDPRoute map to NLB. Three custom CRDs provide ALB-specific configuration not expressible in standard Gateway API resources.

GCP: The GKE Gateway Controller supports only HTTPRoute as of April 2026. If your workloads use TLSRoute or TCPRoute on GKE, deploy Envoy Gateway alongside the GKE controller - use the GKE controller for HTTP load balancing and Envoy Gateway for everything else.

Azure: Application Gateway for Containers provides Gateway API v1 support. Microsoft’s managed NGINX add-on reaches EOL in November 2026. Azure teams are on the same migration clock - the Azure Architecture Blog published migration guidance covering the path from managed NGINX to App Gateway for Containers.

How Does Envoy Gateway Compare to ingress-nginx Operationally?

The most meaningful operational difference is configuration update behavior.

NGINX reloads its configuration by forking a new worker process and draining the old one. Under high connection counts, this introduces brief disruption. At scale, NGINX reloads become operationally disruptive enough that operators often rate-limit how frequently they allow configuration changes - which creates friction for teams doing frequent canary deployments or A/B routing changes.

Envoy uses the xDS protocol to receive configuration updates from the control plane. Updates apply to existing connections without restart. Running connections are not affected when you change routing rules, TLS configuration, or upstream definitions. For teams doing frequent deployments with routing changes, this difference has real operational impact.

On the operational visibility side, Envoy Gateway ships a web UI for runtime inspection of routing configuration, active listeners, upstream health status, and connection metrics. ingress-nginx provides no equivalent - operators were limited to reading NGINX configuration dumps and log output.

Envoy’s baseline memory footprint is higher than NGINX. For most production workloads the difference is not material, but it is a consideration for resource-constrained environments or clusters with many small deployments. Monitor actual memory usage in your environment during the validation phase rather than relying on generic estimates.

Frequently Asked Questions

Can I keep running ingress-nginx after the March 2026 retirement?

Existing deployments continue to function and Helm charts remain available, but there will be no security patches, bugfixes, or new releases. Any CVE discovered in ingress-nginx after March 2026 will remain unpatched indefinitely. For production environments handling sensitive workloads, this is an active security risk that compounds with each passing month - not a stable state you can defer indefinitely.

Does Ingress2Gateway 1.0 convert all ingress-nginx annotations?

It supports 30+ common annotations including CORS headers, backend TLS protocol, regex path matching, URL rewriting, request timeouts, and proxy body size limits. Annotations that cannot be translated generate explicit warnings - there are no silent failures. Custom NGINX configuration snippets (configuration-snippet and server-snippet) are the most common untranslatable category. They require manual implementation using Envoy Gateway extension policies such as SecurityPolicy or BackendTrafficPolicy.

Do I need to delete my old Ingress resources before creating Gateway API resources?

No. Running both simultaneously is the correct approach - ingress-nginx continues handling traffic while you validate the new Envoy Gateway configuration. The critical prerequisite before deleting any Ingress resource: remove ownerReferences from all cert-manager Certificate objects that point to those Ingress resources. Otherwise Kubernetes cascade-deletes the Certificate and its TLS Secret when you delete the Ingress, immediately terminating TLS for that hostname.

Why does my Envoy Gateway deployment fail health checks on cloud load balancers?

Envoy Gateway defaults to externalTrafficPolicy: Local, which configures NodePort to only listen on nodes actively running Envoy pods. Cloud load balancers health-check all nodes in the target group - nodes without Envoy pods fail health checks and get marked unhealthy. On smaller clusters or with restrictive pod scheduling, this can leave the load balancer with zero healthy targets. Fix by explicitly setting externalTrafficPolicy: Cluster in your EnvoyProxy resource before cutover.

Which Gateway API implementation should I choose for production?

Envoy Gateway is the CNCF reference implementation with full conformance across all route types (HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, UDPRoute). It is the right default for teams that need vendor-neutral portability across clouds or run on-prem. Teams already running Istio can use its built-in Gateway API support without adding another control plane. For cloud-native LB integration, AWS LB Controller offers the most complete coverage; GKE Gateway Controller is limited to HTTPRoute and requires Envoy Gateway alongside it for any other route types.