← All posts

Basic auth vanished on port 80 after an Ingress to Gateway API migration

Every status was green and my own curl returned 401. Yet the WordPress login was open to the world: Gateway API binds a policy to one listener, while Ingress annotations covered every port.

Two weeks after we moved Optimacros’ public marketing sites from ingress-nginx to Envoy Gateway — a migration verified host by host — the WordPress login page on all nine sites turned out to be reachable without credentials. Straight from a browser.

The uncomfortable part is that every check said otherwise:

  • every SecurityPolicy reported Accepted: True;
  • every HTTPRoute was Accepted and ResolvedRefs;
  • probing the Gateway with curl --resolve <host>:443:<gateway-ip> returned 401, exactly as intended.

The manifests were right, the statuses were green, and my own verification reproduced the “correct” answer. Only the browser disagreed.

The route lived on one listener

The protected route was attached like this:

parentRefs:
  - kind: Gateway
    name: <gateway>
    sectionName: https    # port 443 only

But this platform serves content on the :80 listener on purpose. An external nginx front terminates TLS and talks to the cluster over plain HTTP, and an unconditional redirect on :80 produced ERR_TOO_MANY_REDIRECTS — Gateway API filters cannot express “redirect only if the request was not HTTPS already”, because they do not read X-Forwarded-Proto.

So the real client path was port 80. On port 80 the login route did not exist at all. Requests fell through to the catch-all PathPrefix: / rule and reached WordPress without any authentication.

Coverage is not a 1:1 translation

This is the conceptual difference that caused it. ingress-nginx applied auth-* annotations to an Ingress regardless of port: one object covered both 80 and 443. Gateway API attaches a route — and therefore any policy that targets it — to a specific listener. Translating paths and annotations one to one does not translate coverage one to one.

And checking with --resolve …:443 re-tested exactly the path nobody uses.

The fix

A paired HTTPRoute on sectionName: http, with the same Exact match, the same backend, the same basic-auth secret and its own SecurityPolicy. The X-Forwarded-Proto: https header filter had to be copied too, otherwise WordPress, whose site URL is HTTPS, answers 301 and the front loops.

Precedence still works in our favour: Gateway API ranks Exact above PathPrefix, so the AJAX endpoint that public forms rely on can later get its own unauthenticated Exact route without opening the login page. The change went through Git — the GitOps controller reverts anything done by hand — and is purely additive, so a revert would not touch the working :443 path.

Takeaways

  • Accepted: True on a policy means it attached to its target, not that the target covers the traffic. No status field says “this listener has no protected route”.
  • Verify through the path real clients take. Behind a TLS-terminating front, --resolve to port 443 is the wrong probe. Add a control host you did not change.
  • After any Ingress to Gateway API migration, audit everything that used to come from a port-agnostic annotation: auth, source allowlists, rate limits, WAF rules.
  • Closing the login page does not close WordPress: xmlrpc.php authenticates on its own and must be covered on both listeners too.