OnCallReady

KubernetesNetworking · 5 min read

HTTPRoute not working: Accepted False, NotAllowedByListeners and RefNotPermitted

You applied an HTTPRoute, the Gateway returns 404, kubectl get looks fine. The answer is in the route's status conditions. How to read them and fix each reason.

The checkout team applied their HTTPRoute for checkout.lab an hour ago. The platform team says the shared Gateway is healthy. Both look right:

terminal
$ kubectl get gateway -A
NAMESPACE   NAME   CLASS   ADDRESS       PROGRAMMED   AGE
gw-shared   edge   nginx   10.64.0.240   True         60m
$ kubectl get httproute -A
NAMESPACE   NAME       HOSTNAMES          AGE
checkout    checkout   ["checkout.lab"]   60m

And users get a 404:

terminal
$ IP=$(kubectl get gateway edge -n gw-shared -o jsonpath='{.status.addresses[0].value}'); echo $IP
10.64.0.240
$ curl -si -H 'Host: checkout.lab' http://$IP/ | head -5
HTTP/1.1 404 Not Found
Server: nginx
Date: Tue, 22 Sep 2026 20:00:04 GMT
Content-Type: text/html
Content-Length: 146

kubectl get httproute shows no status column at all. "It exists" is all it tells you.

What is happening: a route has to be accepted by its parent

In Gateway API, three parties own three objects. The platform team owns the Gateway and decides which routes may attach to each listener. The app team owns the HTTPRoute and names the Gateway it wants (parentRefs). The owner of the backend Service decides who may point at it from another namespace (a ReferenceGrant).

A route only carries traffic when its parent Gateway accepts it, and only reaches its backend when every reference resolves. Both answers are written into the route's status, per parent, as conditions with a reason:

conditionreasonmeaning
Accepted=FalseNoMatchingParentthe parentRefs name, namespace or sectionName matches no Gateway or listener
Accepted=FalseNotAllowedByListenersthe listener's allowedRoutes does not admit this namespace or kind
Accepted=FalseNoMatchingListenerHostnamethe route's hostnames and the listener's hostname do not intersect
ResolvedRefs=FalseBackendNotFoundthe Service in backendRefs does not exist
ResolvedRefs=FalseRefNotPermittedthe backend is in another namespace and no ReferenceGrant there allows it
(no status at all)no controller handles the Gateway's class

The first False is your problem. Fixing it often reveals the next one, and this route had three.

Diagnosis

1. Read the route's conditions

One line per condition, without scrolling through describe:

terminal
$ kubectl get httproute checkout -n checkout -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=False NoMatchingParent
ResolvedRefs=True ResolvedRefs

describe gives the message too:

terminal
$ kubectl describe httproute checkout -n checkout | grep -E "Reason|Message|Section"
    SectionName: http
      Message: No listener with name "http" in Gateway gw-shared/edge
      Reason: NoMatchingParent
      Message: All references are resolved
      Reason: ResolvedRefs
      SectionName: http

The route asks for a listener called http. What does the Gateway have?

terminal
$ kubectl get gateway edge -n gw-shared -o jsonpath='{.spec.listeners[*].name}'; echo
web

Fix the route (the app team's object), or drop sectionName and attach to every listener that allows it:

terminal
$ kubectl patch httproute checkout -n checkout --type=json -p '[{"op":"replace","path":"/spec/parentRefs/0/sectionName","value":"web"}]'
httproute.gateway.networking.k8s.io/checkout patched
$ kubectl describe httproute checkout -n checkout | grep -E "Reason|Message"
      Message: Route is not allowed by any listener
      Reason: NotAllowedByListeners
      Message: Backend ref to Service payments/payments-api not permitted by any ReferenceGrant
      Reason: RefNotPermitted

Progress: two new reasons.

2. NotAllowedByListeners: the Gateway's admission rule

terminal
$ kubectl get gateway edge -n gw-shared -o jsonpath='{.spec.listeners[0].allowedRoutes}'; echo
{"namespaces":{"from":"Same"}}

from: Same is the default: a listener only admits routes from the Gateway's own namespace. A shared Gateway has to opt namespaces in. from: All opens it to everyone, so a label selector is the safer choice. This is the platform team's change:

terminal
$ kubectl patch gateway edge -n gw-shared --type=json -p '[{"op":"replace","path":"/spec/listeners/0/allowedRoutes","value":{"namespaces":{"from":"Selector","selector":{"matchLabels":{"gateway-access":"edge"}}}}}]'
gateway.gateway.networking.k8s.io/edge patched
$ kubectl label ns checkout gateway-access=edge
namespace/checkout labeled
$ kubectl get httproute checkout -n checkout -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=True Accepted
ResolvedRefs=False RefNotPermitted

The route is attached now. The Gateway's listener status counts it (kubectl get gateway edge -n gw-shared -o jsonpath='{.status.listeners[0].attachedRoutes}' went from 0 to 1). But the users' 404 has become something else:

terminal
$ curl -si -H 'Host: checkout.lab' http://$IP/ | head -5
HTTP/1.1 500 Internal Server Error
Server: nginx
Date: Tue, 22 Sep 2026 20:00:12 GMT
Content-Type: text/html
Content-Length: 170

The spec says a rule whose backend cannot be used answers 500. That is a useful signal on its own: a 500 from the Gateway with no request reaching the app usually means an invalid backend reference.

3. RefNotPermitted: the backend's namespace must agree

The route in checkout points at payments/payments-api. Cross-namespace references are refused unless the target namespace publishes a grant:

terminal
$ kubectl get referencegrant -A
No resources found

The payments team adds one, in payments:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: ReferenceGrant
metadata:
  name: from-checkout
  namespace: payments
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: HTTPRoute
    namespace: checkout
  to:
  - group: ""
    kind: Service

(ReferenceGrant is v1 since Gateway API 1.5. v1beta1 is still served, and older guides use it.)

terminal
$ kubectl apply -f payments-grant.yaml
referencegrant.gateway.networking.k8s.io/from-checkout created
$ kubectl get httproute checkout -n checkout -o jsonpath='{range .status.parents[*].conditions[*]}{.type}={.status} {.reason}{"\n"}{end}'
Accepted=True Accepted
ResolvedRefs=True ResolvedRefs
$ curl -s -H 'Host: checkout.lab' http://$IP/hostname; echo
payments-api-5hxbk9zv9j-k24zh

Three causes, three owners, one condition each. That is the point of status in Gateway API: every "the YAML looks fine" ends at a named reason.

When the conditions are all True and it still fails

Then the route is fine and the problem is further along. Check the Service's endpoints (a Service with no endpoints gives a 503), the request's Host header against hostnames, and the match order. In a service mesh, the sidecar's response flag tells you who said no. If the Gateway has no status at all, look at kubectl get gatewayclass: a class that no controller handles stays Pending for ever. For HTTPS listeners, the certificate is one more reference that has to resolve, and cert-manager has its own way of getting stuck.

Keeping it from coming back

  • Read status in the pipeline. After applying a route, run the jsonpath one-liner above and fail the deploy unless both Accepted and ResolvedRefs are True. A deploy that "succeeds" with Accepted=False is a silent outage.
  • Template the defaults. Give app teams a route template without sectionName unless they need one, and document the Gateway's admission label next to the Gateway.
  • Ship the ReferenceGrant with the Service, owned by the backend team, so cross-namespace access is reviewed where the data lives.
  • Coming from Ingress? An Ingress has none of these conditions, which is why the same mistakes there end as a plain 404 (or, on OpenShift, "Application is not available"). ingress2gateway converts the objects. The status is what you gain.

Practise it

The incident "the new checkout route is ignored" in the chapter Kubernetes: Ingress, Gateway API & Service Mesh is this exact route, with the three owners' objects to fix and a curl that must reach a payments pod. The drill The route is ignored - which object is wrong? builds a different broken route on every round, from a misspelled class to a missing grant.

OnCallReady is free, with no ads and no tracking. RSS · All posts