Supported Resources¶
This document details the feature support matrix for each Gateway API resource type in the Cloudflare Tunnel Gateway Controller.
Resource Overview¶
| Resource | API Version | Status |
|---|---|---|
| GatewayClass | gateway.networking.k8s.io/v1 | Supported |
| Gateway | gateway.networking.k8s.io/v1 | Supported |
| HTTPRoute | gateway.networking.k8s.io/v1 | Supported |
| GRPCRoute | gateway.networking.k8s.io/v1 | Supported |
| ReferenceGrant | gateway.networking.k8s.io/v1beta1 | Supported |
| ListenerSet | gateway.networking.k8s.io/v1 | Supported — see ListenerSet |
| TCPRoute | gateway.networking.k8s.io/v1alpha2 | Not supported |
| TLSRoute | gateway.networking.k8s.io/v1alpha2 | Not supported |
| UDPRoute | gateway.networking.k8s.io/v1alpha2 | Not supported |
GatewayClass¶
| Field | Supported | Notes |
|---|---|---|
spec.controllerName | Yes | Must match --controller-name flag |
spec.parametersRef | Yes | Via GatewayClassConfig CRD |
spec.description | Yes | Informational only |
Gateway¶
The Gateway resource is fully processed. Listeners are used for route binding, status reporting, and validation. TLS termination is handled by the Cloudflare edge: HTTPS listeners are served, and their certificate references are validated (including ReferenceGrant checks), but clients get the edge certificate.
| Field | Supported | Notes |
|---|---|---|
spec.gatewayClassName | Yes | Required; the referenced GatewayClass must have a matching spec.controllerName |
spec.listeners | Yes | Fully processed for route binding and status |
spec.listeners[].name | Yes | Used for route binding, status reporting, attached route counting |
spec.listeners[].port | Yes | Used for route binding when route specifies a port, and as the redirect port of a scheme-less RequestRedirect (redirect port) |
spec.listeners[].protocol | Yes | Used for route kind filtering (HTTP/HTTPS allow HTTPRoute/GRPCRoute) |
spec.listeners[].hostname | Yes | Routes must have intersecting hostnames; a host is served only by routes on the most specific listener that matches it (listener isolation) |
spec.listeners[].tls | Yes | certificateRefs validated with ReferenceGrant support, never served: clients get the Cloudflare edge certificate |
spec.listeners[].allowedRoutes | Yes | Namespace (Same/All/Selector) and kind filtering. A kind's group defaults to gateway.networking.k8s.io only when omitted; an explicit "" is the core group, which serves no route kinds, so that entry is reported as InvalidRouteKinds and admits nothing |
spec.tls.backend.clientCertificateRef | Yes | kubernetes.io/tls Secret only; same-namespace or via ReferenceGrant; presented during backend TLS handshake only when the target Service has a BackendTLSPolicy (no client cert is sent over plaintext) |
spec.tls.frontend | No | Refused: a Gateway that sets it is Accepted=False, Reason=Invalid and none of its routes is served, because clients complete TLS with the Cloudflare edge (client certificate validation) |
spec.addresses | Partial | Only the tunnel CNAME; other hostnames report AddressNotUsable, other types UnsupportedAddress |
spec.infrastructure.parametersRef | Yes | Opts the Gateway into a dedicated data plane (GatewayConfig, group cf.k8s.lex.la): its own proxy and tunnel |
spec.infrastructure.labels / .annotations | Yes | Propagated to the rendered per-Gateway resources, generated Secrets and pod template |
TLS Termination
Cloudflare Tunnel terminates TLS at Cloudflare's edge network. TLS certificate references on listeners are validated (a valid TLS Secret, ReferenceGrant for cross-namespace refs), but the actual TLS termination is handled by Cloudflare, not by the controller, and clients get the edge certificate rather than the listener's. The listener port and protocol fields are used for Gateway API route binding semantics, not for configuring network listeners.
HTTPRoute¶
All HTTPRoute matching and filter behavior is performed by the in-process L7 proxy that the chart deploys alongside the controller.
| Field | Supported | Notes |
|---|---|---|
spec.parentRefs | Yes | References to Gateway |
spec.parentRefs[].group | Yes | Omit it or set gateway.networking.k8s.io. Any other value, including an explicit "" (the core group), names some other resource: the route does not bind to this controller's Gateway or ListenerSet, is not counted in attachedRoutes and gets no new status entry. An entry this controller wrote earlier for that ref is removed from status.parents |
spec.parentRefs[].name | Yes | Gateway name |
spec.parentRefs[].namespace | Yes | Gateway namespace |
spec.parentRefs[].sectionName | Yes | Listener name (optional) |
spec.parentRefs[].port | Yes | Listener port (optional) |
spec.hostnames | Yes | Wildcard * supported |
spec.rules | Yes | Routing rules |
spec.rules[].name | Yes | Metadata only; preserved on the spec but not consulted during matching |
spec.rules[].matches | Yes | Full matching |
spec.rules[].matches[].path | Yes | PathPrefix, Exact, RegularExpression |
spec.rules[].matches[].headers | Yes | Exact and RegularExpression matchers |
spec.rules[].matches[].queryParams | Yes | Exact and RegularExpression matchers |
spec.rules[].matches[].method | Yes | All HTTP methods |
spec.rules[].filters | Yes | See Filters |
spec.rules[].backendRefs | Yes | Service, ServiceImport, or ExternalBackend — see Supported Backend Kinds |
spec.rules[].backendRefs[].name | Yes | Name of the Service / ServiceImport / ExternalBackend |
spec.rules[].backendRefs[].namespace | Yes | Cross-namespace refs require ReferenceGrant (keyed on the backend group/kind) |
spec.rules[].backendRefs[].port | Yes | Service / ServiceImport port; ignored for ExternalBackend (its spec.port wins) |
spec.rules[].backendRefs[].weight | Yes | True weighted traffic splitting across backends |
spec.rules[].backendRefs[].filters | Yes | Per-backend filters applied after rule-level filters |
spec.rules[].timeouts | Yes | Per-rule request and backend timeouts |
spec.rules[].retry | Yes | Experimental channel; see Retries |
Backend Protocol (appProtocol)¶
When an HTTPRoute references a Service whose target port sets appProtocol, the L7 proxy selects the upstream transport accordingly:
Service.spec.ports[].appProtocol | Supported | Notes |
|---|---|---|
kubernetes.io/h2c | Yes | Proxy speaks HTTP/2 cleartext (prior knowledge) to the backend |
| (unset) | Yes | Proxy speaks HTTP/1.1 to the backend (default) |
kubernetes.io/ws | Yes | WebSocket over cleartext: the proxy detects upgrade headers and routes through a dedicated upgrade path that dials the backend, forwards the handshake with the same Host the plain HTTP path would send, writes the 101 status, then bidirectionally pipes bytes after hijack |
kubernetes.io/wss | Yes | WebSocket over TLS: requires a matching BackendTLSPolicy (same precondition as appProtocol: https); see Backend Protocol notes |
| any other value | No | Logged with a warning; proxy falls back to default HTTP/1.1 |
A GRPCRoute backend is HTTP/2 by definition, so it is dialed h2c by default and only the TLS-vs-cleartext decision applies: a TLS appProtocol (https / HTTPS / kubernetes.io/wss) with no BackendTLSPolicy fails the backend closed (gRPC status UNAVAILABLE, ResolvedRefs=False, Reason=UnsupportedProtocol), as the HTTPRoute path does with HTTP 500. See gRPC backends and appProtocol.
Filters¶
The following HTTPRoute filters are supported:
| Filter Type | Supported | Notes |
|---|---|---|
RequestHeaderModifier | Yes | Add, set, or remove request headers |
ResponseHeaderModifier | Yes | Add, set, or remove response headers |
RequestRedirect | Yes | Redirect with scheme, hostname, port, path, status code |
URLRewrite | Yes | Rewrite hostname and/or path |
RequestMirror | Yes | Mirror traffic to one or more secondary backends. Per Gateway API only one of percent or fraction may be set on a filter; percent takes precedence if both appear. |
ExtensionRef | No | Not implemented |
Supported Service Types¶
| Service Type | Supported | Notes |
|---|---|---|
ClusterIP | Yes | Routes via cluster-local DNS |
NodePort | Yes | Routes via cluster-local DNS |
LoadBalancer | Yes | Routes via cluster-local DNS |
ExternalName | Yes | Routes directly to external hostname |
Supported Backend Kinds¶
A backendRef may target one of the following kinds (applies to both HTTPRoute and GRPCRoute):
| Group | Kind | Supported | Notes |
|---|---|---|---|
| (core) | Service | Yes | Default when group/kind are unset; all Service types above |
multicluster.x-k8s.io | ServiceImport | Yes | Multi-Cluster Services API; resolved via clusterset.local DNS, port validated against spec.ports. Dialed plaintext: a BackendTLSPolicy and a port appProtocol apply to core Services only, so a same-named local Service lends it neither — see Backend mTLS |
cf.k8s.lex.la | ExternalBackend | Yes | Out-of-cluster HTTP(S) URL; see ExternalBackend |
| (any other) | (any other) | No | ResolvedRefs=False, InvalidKind |
Cross-namespace ServiceImport/ExternalBackend references require a ReferenceGrant whose to entry names the matching group/kind, the same way a cross-namespace Service ref does.
GRPCRoute¶
GRPCRoute is served by the in-process L7 proxy. gRPC requests are HTTP/2 POSTs to /{service}/{method}, so the converter maps each method match onto the proxy's path matcher. The upstream hop is cleartext h2c by default; attaching a BackendTLSPolicy upgrades the hop to TLS with HTTP/2 negotiated via ALPN, and the Gateway's clientCertificateRef is presented on the handshake for mTLS.
| Field | Supported | Notes |
|---|---|---|
spec.parentRefs | Yes | Gateway or ListenerSet, same as HTTPRoute |
spec.hostnames | Yes | Wildcard * supported |
spec.rules[].matches[].method | Yes | Exact (service+method, service-only, method-only) and RegularExpression |
spec.rules[].matches[].headers | Yes | Exact and RegularExpression matchers (shared with HTTP) |
spec.rules[].backendRefs | Yes | Service backends; cleartext h2c by default, TLS + ALPN HTTP/2 when a BackendTLSPolicy targets the Service |
spec.rules[].filters | Partial | Core RequestHeaderModifier and extended ResponseHeaderModifier are served through the shared header-modifier pipeline (rule- and backend-scoped). RequestMirror and ExtensionRef are not served yet and fail closed (matching requests receive gRPC status UNAVAILABLE). |
BackendTLSPolicy / Gateway clientCertificateRef | Yes | When a BackendTLSPolicy targets the backend Service, the proxy dials TLS with HTTP/2 negotiated via ALPN. The Gateway's clientCertificateRef is presented for mTLS only when a policy is also attached (Gateway API spec — a client cert over plaintext is meaningless). See GRPCRoute docs. |
gRPC method matching maps to paths as follows:
| Method match | Proxy path rule |
|---|---|
Exact service + method | exact /{service}/{method} |
Exact service only | prefix /{service}/ (all methods) |
Exact method only | regex /[^/]+/{method} (any service) |
RegularExpression | regex /{service}/{method} (as written) |
Conformance runs through an edge-routing gRPC client
The upstream Gateway API gRPC conformance tests dial the Gateway address (*.cfargotunnel.com) directly, and that hostname resolves to Cloudflare's ULA range, which is not externally routable. The suite allows injecting a custom gRPC client, so the conformance run supplies one that dials the Cloudflare edge instead — the same approach the HTTP suite already uses via a custom RoundTripper. GRPCExactMethodMatching, GRPCRouteHeaderMatching, GRPCRouteNamedRule, GRPCRouteListenerHostnameMatching, and GRPCRouteWeight all run this way (the run pins the http2 tunnel transport, since gRPC trailers do not survive QUIC). gRPC routing is additionally validated end-to-end by the in-house e2e suite against a real tunnel.
ReferenceGrant¶
ReferenceGrant enables cross-namespace backend references in HTTPRoute and GRPCRoute resources.
| Field | Supported | Notes |
|---|---|---|
spec.from | Yes | Source routes (HTTPRoute, GRPCRoute) |
spec.from[].group | Yes | Must be gateway.networking.k8s.io |
spec.from[].kind | Yes | Must be HTTPRoute or GRPCRoute |
spec.from[].namespace | Yes | Namespace where routes are located |
spec.to | Yes | Target resources (Services) |
spec.to[].group | Yes | Must be "" (core) for Services |
spec.to[].kind | Yes | Must be Service |
spec.to[].name | Yes | Optional; if omitted, all Services allowed |
See ReferenceGrant for detailed examples.
Path Matching¶
All matching is performed by the in-process L7 proxy:
| Type | Behavior |
|---|---|
PathPrefix | Full prefix matching |
Exact | True exact matching |
RegularExpression | Full regex matching (RE2) |
gRPC Method Matching¶
gRPC methods are mapped to HTTP/2 paths using the standard format /package.Service/Method. All matching happens inside the in-process L7 proxy (see the GRPCRoute table above for the same mapping in the route context).
| Match Type | Example | Proxy path rule |
|---|---|---|
| Service only | service: mypackage.MyService | prefix /mypackage.MyService/ |
| Service + Method | service: mypackage.MyService, method: GetUser | exact /mypackage.MyService/GetUser |
| Method only | method: GetUser | regex /[^/]+/GetUser (any service) |
| No match | (empty) | Matches all gRPC traffic |
Weight and Traffic Splitting¶
True weighted traffic splitting across multiple backends is performed by the in-process L7 proxy.
- Default weight:
1(per Gateway API specification) - Zero weight: Backends with
weight: 0are disabled
An invalid backend ref fails its own traffic fraction
If a backend ref in a rule fails validation — cross-namespace denial (RefNotPermitted), a missing Service (BackendNotFound), an unsupported kind/port, or a BackendTLSPolicy the proxy cannot enforce (InvalidBackendTLSPolicy) — it stays in the weighted pool and the proxy returns HTTP 500 (gRPC: UNAVAILABLE) for its traffic fraction only; the other backends in the same rule keep serving their proportional share. The route status shows ResolvedRefs=False. Weighting is not failover: an invalid ref fails closed for its fraction rather than silently shifting that traffic to the healthy backends. Only when every backend in a rule is unavailable (or all carry weight: 0) does the rule return 500 (gRPC: UNAVAILABLE) for all of its traffic. A weight: 0 invalid ref carries no traffic, so it is dropped rather than marked. See Unavailable backends return a status, not a dial error.
Status Conditions¶
Gateway Conditions¶
| Type | Status | Reason | Description |
|---|---|---|---|
Accepted | True | Accepted | Gateway accepted by controller |
Accepted | True or False | ListenersNotValid | One or more own listeners are invalid (they conflict and carry Conflicted=True, use an unsupported protocol, or have an allowedRoutes.namespaces.selector that is missing or does not parse); False only when no listener is valid. The message names the conflicted listeners and the accepted ones |
Accepted | False | InvalidParameters | The configuration the Gateway depends on cannot be used: its GatewayClass parametersRef or GatewayClassConfig is missing or invalid, the managed GatewayClasses disagree on parametersRef, its spec.infrastructure.parametersRef cannot be resolved, a dedicated data plane has no proxy image, or its connector token claims a tunnel another Gateway holds or Cloudflare does not confirm. The message names the cause |
Accepted | False | DataPlaneQuotaExceeded | The namespace is at its cap of dedicated data planes (Programmed=False, NoResources) |
Accepted | False | UnsupportedAddress | spec.addresses holds an address type other than Hostname |
Accepted | False | Invalid | spec.tls.frontend is set (client certificate validation) |
Programmed | True | Programmed | Gateway configured. On the shared plane it is set whenever the Gateway is accepted, or has no valid listener of its own but an attached ListenerSet, and spec.addresses asks for no other hostname; a dedicated plane also needs a ready proxy replica (details) |
Programmed | False | Pending | The dedicated data plane has no ready proxy replica yet |
Programmed | False | AddressNotUsable | spec.addresses requests a hostname other than the tunnel CNAME; the Gateway stays Accepted and is served at the CNAME |
Programmed | False | Invalid / NoResources | The Gateway is Accepted=False for any of the reasons above (NoResources for the data-plane cap); with ListenersNotValid only while no ListenerSet is attached |
ResolvedRefs | False | InvalidClientCertificateRef / RefNotPermitted | spec.tls.backend.clientCertificateRef cannot be used, or crosses namespaces without a ReferenceGrant |
Gateway Listener Conditions¶
| Type | Status | Reason | Description |
|---|---|---|---|
Accepted | True | Accepted | Listener accepted |
Accepted | False | HostnameConflict / ProtocolConflict | Listener conflicts with another listener of the Gateway on the same port. Every listener of a conflicting set is refused; none wins. A listener whose protocol is not served (TCP, TLS, UDP) is refused as UnsupportedProtocol instead and conflicts with no other listener |
Accepted | False | UnsupportedValue | allowedRoutes.namespaces.from is Selector and the selector is missing or does not parse, so the listener admits no route. The message says the selector is invalid without quoting it |
Programmed | True | Programmed | Listener programmed |
Programmed | False | Invalid | Listener has unresolved references, or is not Accepted |
Programmed | False | HostnameConflict / ProtocolConflict | Listener conflicts with another listener of the Gateway |
Conflicted | True | HostnameConflict / ProtocolConflict | Listener clashes with another listener on hostname (same port + hostname) or protocol (different protocol on the same port) |
ResolvedRefs | True | ResolvedRefs | References resolved |
ResolvedRefs | False | InvalidCertificateRef | TLS certificate reference invalid |
ResolvedRefs | False | RefNotPermitted | Cross-namespace TLS ref denied by ReferenceGrant |
ResolvedRefs | False | InvalidRouteKinds | Invalid route kind in allowedRoutes |
InvalidCertificateRef and RefNotPermitted are reported only for a reference the controller could read. When a certificate Secret or the ReferenceGrants of its namespace cannot be read, or the ListenerSets attached to the Gateway cannot be listed, the controller leaves the Gateway's status as it was and retries.
A listener's attachedRoutes counts each Route attached to it that is Accepted for the Gateway, once per listener. A conflicted listener counts a Route when the same parentRef also matches a usable listener. A Route whose only matching listeners are conflicted is rejected, so it counts nowhere. A Route that is Accepted: False with reason Pending is not counted either, for example one on a dedicated Gateway whose tunnel claim Cloudflare has not confirmed, so the count can drop while the Cloudflare API is unavailable. When the controller cannot finish a count, for example because a Route's namespace cannot be read for a from: Selector listener, every listener keeps the count it had and the controller counts again.
HTTPRoute/GRPCRoute Conditions¶
| Type | Status | Reason | Description |
|---|---|---|---|
Accepted | True | Accepted | Route accepted and synced |
Accepted | False | NoMatchingParent | No listener or ListenerSet entry matches the parentRef's sectionName or port; every listener or entry it matches is conflicted; or the parent Gateway's allowedListeners refuses the parent ListenerSet, including when that selector is missing or does not parse |
Accepted | False | NoMatchingListenerHostname | Route hostnames don't intersect with the selected listeners, pinned or not |
Accepted | False | NotAllowedByListeners | Route namespace or kind not allowed by the selected listeners, pinned or not. A listener whose allowedRoutes.namespaces.selector does not parse is treated as not allowing the route and the other listeners are still evaluated; when none of them admits the route, the rejection message, whatever its reason, names that listener, and the parse error goes to the controller log |
Accepted | False | Conflicted | Route lost a cross-route-type conflict (HTTPRoute vs GRPCRoute on a shared Gateway with intersecting hostnames); the oldest Route by creationTimestamp is accepted |
ResolvedRefs | True | ResolvedRefs | Backend references resolved |
ResolvedRefs | False | RefNotPermitted | Cross-namespace reference denied |
ResolvedRefs | False | BackendNotFound | Backend Service not found |
When none of a route's parentRefs leads to a Gateway this controller manages any more, for example because the route now points at another controller's Gateway, its Gateway moved to another GatewayClass, its GatewayClass was deleted, or its ListenerSet now points at another Gateway, the controller removes the status.parents entries it wrote for that route and leaves other controllers' entries alone.