Skip to main content

Ingress2Gateway 1.0 vs. a Real Ingress-NGINX Annotation Set: What Actually Translates

9 min readDora NodaDora Noda
Share
On this page

Ingress-NGINX's best-effort-only maintenance window closed in March 2026, the same month SIG Network shipped Ingress2Gateway 1.0 — a CLI purpose-built to convert Ingress resources, including ingress-nginx's own annotations, into Gateway API objects. The pitch is simple: point it at your Ingress manifests, get HTTPRoutes back. We ran it against the kind of annotation set a real multi-tenant, Cluster-API-provisioned fleet actually accumulates in production, not a toy example. About a third of it didn't survive the trip.

That's the number that matters more than the release announcement: of ten common ingress-nginx annotations we fed it, four translated cleanly to portable Gateway API core resources, three converted to non-portable, implementation-specific extension CRDs, and three had no equivalent at all and were dropped. Here's the annotation-by-annotation breakdown, and what it means for a self-hosted PaaS deciding whether to default new tenants onto Gateway API.

Why this matters now, not later

Ingress-NGINX isn't gone — existing deployments keep running and installation artifacts stay available. What ended in March 2026 is active maintenance: no new releases, no bugfixes, and critically, no fixes for newly discovered CVEs. For a platform running ingress-nginx under every tenant's app on a fleet of Cluster-API-managed nodes (CAPH on Hetzner, or any other infrastructure provider), that's not an outage — it's an accumulating liability with no patch coming. Gateway API, the CNCF-blessed successor, is the only actively developed path off it.

Ingress2Gateway existed since 2023 but was genuinely thin before 1.0 — it supported exactly three ingress-nginx annotations. The 1.0 release, shipped March 20, 2026, expanded that to more than 30, adding translation for header manipulation, gRPC, canary routing, path rewriting, timeouts, TLS and redirect handling, CORS, regex path matching, and IP access control. That expansion is why 1.0 is the first version worth actually running against production annotation sets instead of treating as a proof of concept.

The setup: a representative tenant annotation set

We didn't cherry-pick a clean example. Ten annotations below are the ones that show up across almost any multi-tenant ingress-nginx deployment fronting customer apps — the exact shape a self-hosted PaaS's ingress-config generator would be emitting today:

AnnotationWhat it does
nginx.ingress.kubernetes.io/ssl-redirect / force-ssl-redirectForce HTTP → HTTPS with a 301
nginx.ingress.kubernetes.io/rewrite-targetRewrite the request path before it hits the backend
nginx.ingress.kubernetes.io/use-regexTreat the Ingress path as a regex instead of a literal prefix
nginx.ingress.kubernetes.io/cors-allow-origin (+ cors-allow-methods)CORS headers for browser-based API clients
nginx.ingress.kubernetes.io/canary + canary-weight + canary-by-headerWeighted or header-gated traffic splitting for progressive rollouts
nginx.ingress.kubernetes.io/whitelist-source-rangeClient-IP allowlist
nginx.ingress.kubernetes.io/limit-rpsPer-client rate limiting
nginx.ingress.kubernetes.io/auth-basic / auth-urlBasic auth or external-auth-service gating
nginx.ingress.kubernetes.io/proxy-body-sizeMax upload size
nginx.ingress.kubernetes.io/configuration-snippetRaw nginx config injected into the generated server block

That's a realistic tenant load: TLS enforcement, a path rewrite for an API prefix, a canary rollout, an IP allowlist for an admin panel, rate limiting for a public endpoint, basic auth on a staging environment, and a snippet some engineer added eighteen months ago to fix a header edge case nobody remembers.

Running the tool

Ingress2Gateway 1.0 reads from a live cluster or from manifest files directly:

bash
ingress2gateway print \
  --input-file tenant-ingresses.yaml \
  --providers=ingress-nginx \
  --output yaml

The genuinely new part in 1.0 isn't just the wider annotation coverage — it's an annotation-tracking system that reports, per annotation, whether it was Parsed (translated), Unsupported (recognized but has no translation), or Unrecognized (the tool doesn't know it). That status output is what makes the tool usable for an audit instead of a guess: you get a definitive list of what needs manual follow-up, not a silent best-effort YAML dump.

The translation table

Running our ten-annotation set through the tool sorts cleanly into three buckets — and the middle bucket is the one migration guides tend to gloss over:

AnnotationTranslation outcomeWhere it lands
ssl-redirect / force-ssl-redirectCleanCore HTTPRoute RequestRedirect filter
rewrite-targetCleanCore HTTPRoute URLRewrite filter
use-regexCleanCore HTTPRoute RegularExpression path match
canary / canary-weight / canary-by-headerCleanWeighted backendRefs + HTTPHeaderMatch on core HTTPRoute
cors-allow-origin / cors-allow-methodsPartialCORS filter — spec'd but rollout depends on which Gateway API implementation you run
whitelist-source-rangePartialImplementation-specific extension policy (e.g. Envoy Gateway's ClientTrafficPolicy) — not a core Gateway API object
limit-rpsPartialImplementation-specific rate-limit policy CRD — no core Gateway API rate-limit primitive exists yet
auth-basic / auth-urlPartialImplementation-specific external-auth policy CRD
proxy-body-sizeNoneNo Gateway API equivalent — flagged Unsupported, silently absent from the output
configuration-snippetNoneNo equivalent by design — flagged Unsupported

Four of ten arrive as portable, core Gateway API resources you could hand to any conformant implementation. Four more arrive, but only as an extension CRD specific to whatever Gateway API implementation you've deployed — Envoy Gateway, Cilium, kgateway, and others each ship their own policy-attachment CRDs for rate limiting, IP filtering, and external auth, and they are not interchangeable. Two have nowhere to go at all.

What breaks in practice

The two outright failures are the ones worth understanding, because they're not bugs — they're the point.

configuration-snippet lets an operator inject arbitrary nginx directives into the generated server block. It's also the single annotation most frequently cited in ingress-nginx's own CVE history, because arbitrary config injection is a path to request smuggling and, in some configurations, RCE via nginx's Lua scripting. Gateway API doesn't have a "raw config" filter and isn't going to add one — the entire design intent is to replace free-form config injection with a fixed set of structured, auditable filter types (RequestHeaderModifier, URLRewrite, RequestRedirect, and so on). Whatever a snippet was doing has to be re-implemented as one of those filters, moved into the application itself, or replicated as an implementation-specific EnvoyPatchPolicy-style extension — there's no drop-in translation.

proxy-body-size is a quieter failure: a genuinely simple, common setting (raise the upload cap for a file-upload endpoint) with no Gateway API core field for it at all. It doesn't get flagged as dangerous, it's just missing from the spec, which means the CLI's Unsupported status list is your only signal that the translated HTTPRoute silently defaults to whatever body-size limit your new Gateway API implementation ships with — not the one your tenant configured.

The "Partial" bucket is the trap for anyone who reads a green checkmark from the CLI as "done." IP allowlisting, rate limiting, and external auth all produce output — but that output is tied to one specific Gateway API implementation's extension API. Translate a tenant's Ingress with --providers=ingress-nginx today and swap the underlying Gateway controller next year, and those three categories of config break again, even though nothing about the tenant's intent changed. A finished migration isn't "ingress2gateway produced YAML" — it's "ingress2gateway produced YAML for the specific Gateway API implementation you've committed to running for the next several years."

What a self-hosted PaaS's translation layer actually needs

For a platform that generates ingress config on a tenant's behalf — rather than a team hand-writing Ingress YAML once — running the CLI once and shipping the output isn't a migration strategy. Four things have to be true before defaulting new tenant deployments to Gateway API instead of Ingress:

  • An internal config model that isn't annotation-shaped. A platform whose ingress generator is a thin template over nginx.ingress.kubernetes.io/* annotations has encoded ingress-nginx's specific feature set as its own data model. That has to become an abstraction — "this route needs an IP allowlist" — that can compile down to either target, so the Ingress-vs-Gateway-API choice is a backend swap, not a rewrite.
  • A hard deny, not a best-effort translate, for snippet-style config. configuration-snippet shouldn't be a feature a platform tries to carry forward — it should be a feature the platform never exposed to tenants in the first place, or one it explicitly sunsets with a real structured alternative for each known use.
  • A defined fallback for the "no equivalent" bucket. Because unmodified ingress-nginx deployments keep functioning post-retirement, a tenant relying on proxy-body-size or a snippet can safely stay on the Ingress path while everything translatable moves to Gateway API — a platform needs that per-tenant, per-feature fallback logic rather than an all-or-nothing cutover.
  • A test matrix keyed to the chosen Gateway API implementation, not the spec. Because the "Partial" bucket's actual behavior depends on Envoy Gateway vs. Cilium vs. kgateway extension CRDs, "we migrated to Gateway API" is meaningless as a platform-level claim without naming which implementation backs it — and testing against that implementation specifically, not the Gateway API conformance suite alone.

Bex.co's own App resource takes the same route deliberately: ingress intent (host, path, TLS, redirect behavior) lives on the App CR as implementation-agnostic fields, with the controller responsible for compiling that into whatever the cluster's ingress layer actually understands — the exact seam a platform needs to move the compile target from Ingress to Gateway API without asking every tenant to rewrite anything.

The takeaway

Ingress2Gateway 1.0 is a real tool, not vaporware — the annotation-tracking system alone makes it worth running against production Ingress objects today, if only to get an honest Unsupported list instead of finding out at 2am which annotation didn't survive. But the CLI output is an audit, not a migration. Four of our ten annotations were done in one pass. Four needed a real decision about which Gateway API implementation a platform is committing to. Two needed a redesign. For a fleet built on Cluster API rather than a managed control plane, that's the actual timeline to budget for March's retirement deadline — not a weekend running a converter, but the design work of an ingress-config layer that can target either API on purpose.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with ingress config generated from a portable App spec instead of hand-maintained annotations. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex