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
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
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
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
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
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
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
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
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)