DEV Community

Mateen Anjum
Mateen Anjum

Posted on

ingress-nginx Is Dead: How I Migrated to Gateway API Before It Became a Liability

ingress-nginx was archived on March 24, 2026 after a string of critical CVEs including a 9.8 CVSS unauthenticated RCE. Gateway API v1.4 is the CNCF-graduated replacement. I used ingress2gateway 1.0 to convert 40+ Ingress resources to HTTPRoutes, validated the output, and cut over with zero downtime. Here's exactly how I did it.

Why This Happened

In March 2025, CVE-2025-1974 (dubbed "IngressNightmare") dropped: a CVSS 9.8 unauthenticated remote code execution vulnerability in ingress-nginx's admission webhook. Any attacker with network access to the webhook could execute arbitrary code inside the controller pod, which typically has broad cluster permissions. That was bad enough on its own.

Then came 2026. Four more HIGH-severity CVEs landed in quick succession:

CVE Severity What It Does
CVE-2025-1974 CRITICAL 9.8 Unauthenticated RCE via admission webhook
CVE-2026-1580 HIGH Config injection leading to privilege escalation
CVE-2026-24512 HIGH Path injection through nginx config manipulation
CVE-2026-24513 HIGH Authentication bypass
CVE-2026-24514 HIGH Annotation abuse for unauthorized access

On March 24, 2026, the ingress-nginx repository was officially archived. Read-only. No more patches. No more CVE fixes. If you're still running it, you're running unpatched software with known critical vulnerabilities.

This wasn't a surprise deprecation. The Kubernetes community had been building Gateway API for years as the successor to the Ingress resource. But the CVE storm turned "migrate when convenient" into "migrate now."

Gateway API: What Actually Changed

Gateway API isn't just "Ingress v2." It fundamentally changes how traffic routing is modeled in Kubernetes by splitting responsibilities across three layers:

Layer 1: GatewayClass (Infrastructure Admin)

The infrastructure team defines what gateway implementation is available. Think of it as the "which load balancer technology" decision.

apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: production-gateway
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
Enter fullscreen mode Exit fullscreen mode

Layer 2: Gateway (Cluster Operator)

The platform team creates Gateway resources that bind to a GatewayClass. This is where you define listeners, ports, TLS certificates, and which namespaces can attach routes.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: main-gateway
  namespace: gateway-infra
spec:
  gatewayClassName: production-gateway
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - name: wildcard-tls
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels:
              gateway-access: "true"
    - name: http
      protocol: HTTP
      port: 80
Enter fullscreen mode Exit fullscreen mode

Layer 3: HTTPRoute (Application Developer)

Application teams define their own routing rules without touching the gateway configuration. They just reference the Gateway they want to attach to.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-api
  namespace: my-api
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-infra
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v1
      backendRefs:
        - name: api-service
          port: 8080
Enter fullscreen mode Exit fullscreen mode

This separation matters because it maps to how teams actually operate. Infrastructure admins pick the implementation. Platform engineers configure the gateway. App developers define their routes. Nobody steps on each other's toes, and RBAC enforces the boundaries.

Why This Is Better Than Annotations

With ingress-nginx, everything was shoved into annotations. Rate limiting, CORS, timeouts, rewrites, all of it crammed into nginx.ingress.kubernetes.io/* strings that were:

  • Non-standard: Every controller had its own annotation format
  • Unvalidated: Typo an annotation name? Silent failure
  • Unstructured: Complex configs as string values
  • Non-portable: Locked to one implementation

Gateway API uses typed CRD fields. Your IDE autocompletes them. The API server validates them. They work across implementations.

The Migration: Using ingress2gateway 1.0

On March 20, 2026, ingress2gateway 1.0 shipped with support for 30+ ingress-nginx annotations. This was the tool that made bulk migration practical.

Step 1: Install

brew install ingress2gateway
# or
go install github.com/kubernetes-sigs/ingress2gateway@v1.0.0
Enter fullscreen mode Exit fullscreen mode

Step 2: Scan and Convert

# Convert everything cluster-wide
ingress2gateway print --providers=ingress-nginx --all-namespaces > gwapi.yaml

# Or target a specific namespace
ingress2gateway print --namespace my-api --providers=ingress-nginx > gwapi.yaml

# If you've chosen your implementation, use emitter flags
ingress2gateway print --emitter envoy-gateway --providers=ingress-nginx --all-namespaces > gwapi.yaml
Enter fullscreen mode Exit fullscreen mode

Step 3: Review the Output

Here's what a typical translation looks like.

Before (Ingress with ingress-nginx annotations):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-api
  annotations:
    nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
    nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS"
    nginx.ingress.kubernetes.io/cors-enable: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /api/v[0-9]+/users
            pathType: ImplementationSpecific
            backend:
              service:
                name: users-service
                port:
                  number: 8080
Enter fullscreen mode Exit fullscreen mode

After (Gateway API HTTPRoute):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: my-api
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-infra
  hostnames:
    - "api.example.com"
  rules:
    - matches:
        - path:
            type: RegularExpression
            value: "/api/v[0-9]+/users"
      filters:
        - type: ResponseHeaderModifier
          responseHeaderModifier:
            set:
              - name: Access-Control-Allow-Origin
                value: "https://app.example.com"
              - name: Access-Control-Allow-Methods
                value: "GET, POST, OPTIONS"
      timeouts:
        backendRequest: 60s
      backendRefs:
        - name: users-service
          port: 8080
Enter fullscreen mode Exit fullscreen mode

The structure is cleaner. CORS headers are explicit. The regex path type is a first-class field instead of being toggled by an annotation. Timeouts are typed durations, not string-encoded integers.

What ingress2gateway Cannot Translate

The tool is good, but it's not magic. Watch for these:

Custom Lua snippets. If you used nginx.ingress.kubernetes.io/server-snippet or configuration-snippet with custom Lua or raw nginx config, those have no Gateway API equivalent. You'll need to reimplement that logic in your application or use implementation-specific policies.

Rate limiting. ingress-nginx rate limiting annotations don't map to standard Gateway API fields. Most implementations offer their own rate limiting CRDs (like Envoy Gateway's BackendTrafficPolicy).

ModSecurity / WAF rules. If you had ModSecurity enabled via annotations, you'll need a separate WAF solution or an implementation that supports it natively.

Session affinity. Cookie-based session affinity annotations need implementation-specific configuration in Gateway API.

Custom error pages. These were nginx-specific and need to be handled at the application level or through implementation extensions.

ingress2gateway will print warnings for annotations it can't convert. Read every warning. I found three services silently losing rate limiting configs that would have caused issues in production.

Choosing a Gateway API Implementation

Gateway API is a spec. You need an implementation. Here's how I evaluated the main options:

Implementation Backed By Best For Notes
Envoy Gateway Envoy Proxy / CNCF General purpose, feature-rich Strong community, good docs
kgateway Solo.io Advanced traffic management Commercial support available
Cilium Gateway Isovalent/Cisco eBPF-native networking Great if you already run Cilium CNI
NGINX Gateway Fabric F5/NGINX Familiar nginx users Uses nginx under the hood
Istio Waypoint Google/Solo.io Service mesh integration If you're already on Istio

I went with Envoy Gateway. It's CNCF-backed, has broad feature coverage, and doesn't require buying into a service mesh. The --emitter envoy-gateway flag in ingress2gateway generates implementation-specific extensions where needed, which saved manual work.

My Migration Checklist

Here's the checklist I followed. Steal it.

Pre-migration:
[ ] Inventory all Ingress resources: kubectl get ingress --all-namespaces
[ ] Document custom annotations per Ingress
[ ] Identify any custom nginx configs (ConfigMap, snippets)
[ ] Install Gateway API CRDs: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
[ ] Deploy chosen Gateway API implementation

Conversion:
[ ] Run ingress2gateway print and capture output
[ ] Review ALL warnings from ingress2gateway
[ ] Manually handle untranslatable annotations
[ ] Create GatewayClass and Gateway resources
[ ] Create ReferenceGrant resources for cross-namespace refs

Validation:
[ ] Apply HTTPRoutes to staging cluster
[ ] Test every endpoint (automated: curl + expected status codes)
[ ] Verify TLS termination works
[ ] Check CORS headers in browser dev tools
[ ] Validate regex paths match correctly
[ ] Load test to confirm no performance regression

Cutover:
[ ] Update DNS or switch load balancer target
[ ] Monitor error rates for 30 minutes
[ ] Keep old Ingress resources (don't delete yet)
[ ] After 48 hours stable: remove old Ingress resources
[ ] Uninstall ingress-nginx controller
Enter fullscreen mode Exit fullscreen mode

Results

After migrating 40+ Ingress resources across 12 namespaces:

Metric Before After
Known CVEs 5 (1 critical) 0
Annotation sprawl 180+ annotations 0 (typed fields)
Cross-namespace routing Manual workarounds Native ReferenceGrant
Downtime during migration N/A Zero
Time to complete N/A 3 days (including validation)

Lessons Learned

Don't wait for the archive notice. Gateway API has been stable since v1.0 (October 2023). I should have started earlier. The CVE pressure made this more stressful than it needed to be.

ingress2gateway is a starting point, not a finish line. It handled about 85% of our config automatically. The remaining 15% required understanding both the old nginx annotations and the new Gateway API model.

The three-layer model pays off immediately. Within a week of the migration, our app teams were creating their own HTTPRoutes without filing tickets to the platform team. That alone justified the effort.

Test regex paths carefully. The regex syntax between nginx and Gateway API implementations can differ subtly. I caught two path patterns that matched differently under Envoy than they did under nginx.

Keep the old Ingress resources around. Don't delete them the moment Gateway API routes are working. Give yourself a rollback window. I kept ours for 48 hours before cleanup.


Resources:

Top comments (0)