Gateway API guide¶
Coxswain implements the Kubernetes Gateway API standard channel. It supports GatewayClass, Gateway, ListenerSet, HTTPRoute, GRPCRoute, TLSRoute, TCPRoute, and UDPRoute resources.
Supported resources¶
| Resource | API version | Support |
|---|---|---|
GatewayClass |
gateway.networking.k8s.io/v1 |
Full |
Gateway |
gateway.networking.k8s.io/v1 |
HTTP, HTTPS, TLS passthrough/terminate, TCP, and UDP listeners |
ListenerSet |
gateway.networking.k8s.io/v1 |
Attach listeners to a Gateway across namespaces — see the ListenerSet guide |
HTTPRoute |
gateway.networking.k8s.io/v1 |
Path, header, method, and query matching; weighted traffic split |
GRPCRoute |
gateway.networking.k8s.io/v1 |
Service and method matching; cleartext h2c backends |
TLSRoute |
gateway.networking.k8s.io/v1 |
SNI-keyed L4 passthrough and/or terminate — see below |
TCPRoute |
gateway.networking.k8s.io/v1 |
Raw TCP proxy, port-keyed — see below |
UDPRoute |
gateway.networking.k8s.io/v1 |
Session-tracked UDP datagram forwarding, port-keyed — see below |
ReferenceGrant |
gateway.networking.k8s.io/v1beta1 |
Cross-namespace backend and certificate access |
BackendTLSPolicy |
gateway.networking.k8s.io/v1 |
Upstream TLS configuration referencing a CA ConfigMap or Secret |
CoxswainBackendPolicy |
gateway.coxswain-labs.dev/v1alpha1 |
Coxswain-native per-Service connection policy: connect/idle timeouts, load-balancing algorithm, circuit breaker — see below |
CoxswainExternalAuth |
gateway.coxswain-labs.dev/v1alpha1 |
External authorization (ext_authz, HTTP or gRPC) as an HTTPRoute ExtensionRef filter or a Gateway-attached targetRefs policy — see below |
GatewayClass¶
A GatewayClass identifies a controller implementation. Coxswain claims the class whose spec.controllerName matches coxswain-labs.dev/gateway-controller.
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: coxswain
spec:
controllerName: coxswain-labs.dev/gateway-controller # must match --controller-name
Verifying the controller claimed it¶
kubectl get gatewayclass coxswain
# NAME CONTROLLER ACCEPTED
# coxswain coxswain-labs.dev/gateway-controller True
Advertised features¶
Coxswain writes the full list of supported Gateway API features to status.supportedFeatures on the GatewayClass object:
kubectl get gatewayclass coxswain \
-o jsonpath='{.status.supportedFeatures}' | tr ',' '\n'
Implementation-specific capabilities — such as RegularExpression path, header, and query matching — are not listed in supportedFeatures. The Gateway API spec does not define conformance flags for them; they are supported under Coxswain's own dialect. See Implementation-specific matching.
Gateway¶
A Gateway object defines one or more listeners, each binding a port and protocol to a set of allowed routes. Coxswain routes the HTTP, HTTPS, and TLS protocols. A listener declaring any other protocol is rejected: it gets Accepted=False, reason=UnsupportedProtocol with an empty supportedKinds, and the Gateway's own Accepted condition rolls up to reason=ListenersNotValid (status False when every listener is unsupported, True when at least one listener is still valid).
Dedicated proxy per Gateway
A Gateway can be opted into its own isolated proxy pool via spec.infrastructure.parametersRef pointing at a CoxswainGatewayParameters. See Dedicated proxy pools for the full walkthrough. A parametersRef targeting any other (unrecognized) kind is rejected with Accepted=False, reason=InvalidParameters.
Infrastructure metadata propagation (GEP-1867)
spec.infrastructure.labels and spec.infrastructure.annotations propagate onto the resources Coxswain provisions for the Gateway, in both deployment models. In dedicated mode they land on the per-Gateway Deployment, Service, and ServiceAccount; in shared mode they land on the per-Gateway VIP Service (e.g. cloud load-balancer annotations) and on a per-Gateway identity ServiceAccount provisioned in the Gateway's namespace. The four reserved GEP-1762 label keys (app.kubernetes.io/{name,instance,managed-by,component} and gateway.networking.k8s.io/gateway-name) cannot be overridden — a collision is dropped with a warning, since the Service/Deployment selectors depend on them.
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: coxswain
listeners:
- name: http
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: Same # see below
allowedRoutes.namespaces.from¶
Controls which namespaces may attach HTTPRoutes to this listener.
| Value | Behaviour |
|---|---|
Same (default) |
Only routes in the Gateway's own namespace can attach. |
All |
Routes from any namespace can attach. |
Selector |
Routes from namespaces matching namespaces.selector (a label selector) can attach. |
All and Selector cause the controller to automatically grant the dedicated proxy cluster-wide HTTPRoute reads. No extra fields on CoxswainGatewayParameters are required — the listener spec is the single source of truth. See the dedicated-mode guide for details.
Supported fields¶
| Field | Support |
|---|---|
spec.gatewayClassName |
Full |
spec.listeners[].name |
Full |
spec.listeners[].port |
Full |
spec.listeners[].protocol |
HTTP, HTTPS, TLS |
spec.listeners[].hostname |
Full (wildcard: any number of labels) |
spec.listeners[].allowedRoutes |
Full |
spec.listeners[].tls |
mode: Terminate (HTTPS) and mode: Passthrough (TLS) |
TLS¶
Add an HTTPS listener and reference a kubernetes.io/tls Secret in the same namespace. Coxswain reloads the certificate automatically when the Secret changes. See the TLS guide for cert-manager integration. For TLS passthrough (no termination at the proxy), see TLSRoute below.
spec:
gatewayClassName: coxswain
listeners:
- name: https
port: 443
protocol: HTTPS
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: my-gateway-tls # must exist in the same namespace
allowedRoutes:
namespaces:
from: Same
The referenced Secret must have type: kubernetes.io/tls with tls.crt and tls.key:
apiVersion: v1
kind: Secret
metadata:
name: my-gateway-tls
namespace: default
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded certificate>
tls.key: <base64-encoded private key>
To reference a Secret in a different namespace, create a ReferenceGrant in the namespace where the Secret lives:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-gateway-tls
namespace: certs-namespace # namespace of the Secret
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default # namespace of the Gateway
to:
- group: ""
kind: Secret
Listener hostnames¶
The hostname field on a listener filters which requests reach its attached routes. Gateway API wildcard matching allows any number of DNS labels: *.example.com matches both foo.example.com and foo.bar.example.com.
An empty hostname accepts requests for any hostname. For SNI-based TLS termination, the listener hostname is also used to select the correct certificate when multiple HTTPS listeners share the same port.
Note
Gateway API wildcards (both listener and HTTPRoute hostnames) match any number of labels. Classic Ingress is more restrictive: *.example.com on an Ingress matches only a single label (foo.example.com yes, foo.bar.example.com no). See the Ingress guide for the Ingress semantics.
Load balancer address¶
Set --status-address to the external IP or hostname of your load balancer. Coxswain writes it to status.addresses on the Gateway object. Without it, the address is left empty.
kubectl get gateway my-gateway
# NAME CLASS ADDRESS PROGRAMMED
# my-gateway coxswain 203.0.113.10 True
Requesting a static address¶
Set spec.addresses to ask coxswain to bind a specific address rather than letting the cluster auto-assign one (Gateway API GatewayStaticAddresses):
spec:
addresses:
- type: IPAddress
value: 10.96.0.42
Coxswain honors a requested IPAddress by provisioning that Gateway's VIP Service as a ClusterIP pinned to the requested address (overriding the default VIP type for that one Gateway). The apiserver assigns the address exactly when it is a free IP inside the cluster's Service CIDR, and rejects it otherwise — giving a deterministic accept/reject on every cluster. The outcome is reflected in the conditions:
| Requested address | Accepted |
Programmed |
|---|---|---|
| Supported type, bindable value | True |
True — the address appears in status.addresses |
| Supported type, value coxswain cannot bind (out of range / in use) | True |
False, reason AddressNotUsable — the value is not published |
Unsupported type (anything but IPAddress/Hostname) |
False, reason UnsupportedAddress |
False, reason Invalid |
A request for two distinct IPs is always AddressNotUsable — one Service binds a single clusterIP. Leaving value empty keeps the auto-assign behaviour (GatewayAddressEmpty).
Because a static-IP Gateway is bound to a
ClusterIP, its address is cluster-internal — coxswain cannot guarantee an arbitrary externally-routable IP across load-balancer providers. Use a static address when you need a stable, predictable in-cluster address; for external exposure, leavespec.addressesempty and let the cluster's load balancer assign one.
Status conditions¶
| Condition | True when |
|---|---|
Accepted |
The controller has claimed this Gateway |
Programmed |
All listeners are configured and the Gateway's address has resolved |
The controller does not stamp Programmed as processed for the current metadata.generation until the Gateway's own address has resolved into status.addresses. Until then Programmed stays False/Pending and its observedGeneration trails metadata.generation, so a client that waits for the latest conditions never observes Programmed claiming a generation while status.addresses is still empty — the same reconcile that flips Programmed=True also publishes the address. Accepted advances immediately. (A settled negative such as AddressNotUsable is not held back — it is a final answer for the current generation.)
Per-listener conditions are also written: Accepted, ResolvedRefs, and Programmed. Inspect them when a listener is not serving traffic:
kubectl describe gateway my-gateway
HTTPRoute¶
An HTTPRoute defines routing rules and attaches them to one or more Gateway listeners via parentRefs.
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-route
namespace: default
spec:
parentRefs:
- name: my-gateway # name of the Gateway in the same namespace
hostnames:
- app.example.com # only matched requests for this hostname
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-service
port: 8080
- matches:
- path:
type: PathPrefix
value: / # catch-all rule
backendRefs:
- name: frontend-service
port: 80
Supported fields¶
| Field | Support |
|---|---|
spec.parentRefs |
Full (including sectionName and port for targeting a specific listener) |
spec.hostnames |
Full (including wildcards) |
spec.rules[].matches[].path |
PathPrefix, Exact; RegularExpression is implementation-specific (see below) |
spec.rules[].matches[].headers |
Full |
spec.rules[].matches[].method |
Full |
spec.rules[].matches[].queryParams |
Full |
spec.rules[].filters |
See filter table below |
spec.rules[].backendRefs |
Service backends only |
spec.rules[].backendRefs[].weight |
Full |
spec.rules[].backendRefs[].filters |
RequestHeaderModifier, ResponseHeaderModifier only |
Supported filters¶
| Filter | Support |
|---|---|
RequestHeaderModifier |
Supported (rule-level and per-backendRef) |
ResponseHeaderModifier |
Supported (rule-level and per-backendRef) |
URLRewrite |
Supported (hostname and path rewrite) |
RequestRedirect |
Supported (scheme, hostname, port, path, status code) |
RequestMirror |
Supported — GEP-3171 fire-and-forget shadow traffic with optional percent or fraction sampling; multiple filters per rule for multiple mirrors |
ExtensionRef |
Supported (for RateLimit, PathRewriteRegex, IpAccessControl, BasicAuth, ExternalAuth, RequestSizeLimit, Compression, and JwtAuth Coxswain extensions) |
CORS |
Supported — GEP-1767 preflight short-circuit and response-header injection |
Attaching to a Gateway¶
parentRefs selects the Gateway (and optionally a specific listener by sectionName or port) the route attaches to:
parentRefs:
- name: my-gateway # attach to the whole Gateway
- name: my-gateway
sectionName: https # attach to the listener named "https" only
- name: my-gateway
port: 443 # attach to the listener on port 443 only
The route must be in the same namespace as the Gateway unless the listener's allowedRoutes.namespaces.from is set to All or Selector.
Path matching¶
type |
Behaviour |
|---|---|
PathPrefix |
Matches requests whose path starts with the given value |
Exact |
Matches only the exact path |
RegularExpression |
Anchored full-path match. Implementation-specific — see below. |
rules:
- matches:
- path:
type: PathPrefix
value: /api # matches /api, /api/users, /api/v2/...
backendRefs:
- name: api-service
port: 8080
- matches:
- path:
type: Exact
value: /healthz # matches only /healthz
backendRefs:
- name: health-service
port: 8080
Header matching¶
rules:
- matches:
- headers:
- name: X-Tenant
value: acme # only routes requests with this header value
backendRefs:
- name: acme-service
port: 80
Method matching¶
rules:
- matches:
- method: GET # only routes GET requests
backendRefs:
- name: read-service
port: 80
Implementation-specific matching¶
RegularExpression is supported for path, header, and query-parameter matching. These match types are not covered by the Gateway API conformance suite — the spec marks them as implementation-specific and defines no feature flag for them.
Dialect: Rust regex crate — RE2-like syntax. No backreferences, no lookaround. Patterns are case-sensitive by default.
Path regex — anchored to the full request path (^(?:pattern)$ internally). Does not match the query string.
rules:
- matches:
- path:
type: RegularExpression
value: "/item/[0-9]+" # matches /item/42, not /item/abc or /prefix/item/42
backendRefs:
- name: api-service
port: 8080
Header regex — tested against the full header value, unanchored (matches if the pattern appears anywhere in the value). Use ^ and $ to anchor explicitly.
rules:
- matches:
- headers:
- name: X-Tenant
type: RegularExpression
value: "^(acme|globex)$" # matches exactly "acme" or "globex"
backendRefs:
- name: multi-tenant-service
port: 80
Query param regex — same unanchored semantics as header regex.
rules:
- matches:
- queryParams:
- name: version
type: RegularExpression
value: "v[0-9]+" # matches v1, v2, v12, ...
backendRefs:
- name: versioned-service
port: 80
An HTTPRoute with a syntactically invalid regex pattern is rejected: Coxswain sets Accepted: False with reason UnsupportedValue on the affected parentRef.
Wildcard hostnames¶
*.example.com in spec.hostnames matches any number of leading DNS labels: both foo.example.com and foo.bar.example.com match. This is the same semantics applied to listener hostname fields — Gateway API treats wildcards uniformly across listeners and routes.
hostnames:
- "*.example.com" # matches foo.example.com and foo.bar.example.com
Note
Classic Ingress wildcards are more restrictive (single-label only). See the Ingress guide if you also use Ingress objects in the cluster.
Traffic splitting¶
Distribute traffic across multiple backends using weight. Weights are relative and do not need to sum to 100:
rules:
- backendRefs:
- name: service-v1
port: 80
weight: 90 # 90% of traffic
- name: service-v2
port: 80
weight: 10 # 10% of traffic
Cross-namespace backends¶
By default, an HTTPRoute can only reference backends in its own namespace. To allow access to a Service in another namespace, create a ReferenceGrant in the namespace where the Service lives:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-httproute-from-default
namespace: target-namespace # namespace of the Service
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: default # namespace of the HTTPRoute
to:
- group: ""
kind: Service
Routes that reference a backend without a matching ReferenceGrant are rejected with a ResolvedRefs: False condition.
Request mirroring¶
The RequestMirror filter (GEP-3171) sends a fire-and-forget copy of every matched request to a secondary backend while the primary response is returned normally to the client. The mirror response is discarded — mirror failures (connect error, timeout, bad response) are logged at WARN level and do not affect the primary.
rules:
- filters:
- type: RequestMirror
requestMirror:
backendRef:
name: echo-mirror
port: 3000
backendRefs:
- name: echo-primary
port: 3000
Multiple mirrors per rule — add one RequestMirror filter per shadow backend; each fires independently:
filters:
- type: RequestMirror
requestMirror:
backendRef: {name: shadow-a, port: 3000}
- type: RequestMirror
requestMirror:
backendRef: {name: shadow-b, port: 3000}
Sampling — use percent (integer, 0–100) or fraction (numerator / denominator) to mirror only a subset of requests:
requestMirror:
backendRef: {name: echo-mirror, port: 3000}
percent: 20 # mirror 20% of requests
requestMirror:
backendRef: {name: echo-mirror, port: 3000}
fraction:
numerator: 1
denominator: 5 # equivalent to 20%
Cross-namespace mirror backends require a ReferenceGrant in the target namespace, just like primary backends (see Cross-namespace backends).
Mirror traffic is visible in the proxy access log (mirror: true field) and counted by the coxswain_proxy_mirror_requests_total{route, upstream} Prometheus counter.
IP access control¶
IpAccessControl (gateway.coxswain-labs.dev/v1alpha1) restricts a route to a set of source-IP CIDR ranges. Attach it to an HTTPRouteRule with an ExtensionRef filter — the Gateway API surface for the Ingress ip-access-control annotation. It has no Gateway API standard equivalent; its merit anchor is Envoy's rbac CIDR-principal filter / Istio AuthorizationPolicy ipBlocks/notIpBlocks.
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: IpAccessControl
metadata:
name: office-only
spec:
deny: # evaluated FIRST
- 203.0.113.5/32
allow: # then the allow-list
- 203.0.113.0/24
- 2001:db8::/32
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
# ...
filters:
- type: ExtensionRef
extensionRef:
group: gateway.coxswain-labs.dev
kind: IpAccessControl
name: office-only
Semantics:
denyis evaluated beforeallow. A client inside anydenyrange gets403even whenallowwould admit it.allowrestricts to the listed ranges — a client outside everyallowrange gets403. An emptyallowlist imposes no allow-list restriction (onlydenyapplies); emptyallowand emptydenyperforms no filtering.- IPv4 and IPv6 CIDRs are both accepted; a bare address (
203.0.113.5) is treated as a host route (/32//128). Invalid CIDR tokens are logged and skipped rather than rejecting the whole policy. - A missing
IpAccessControlCR fails open (a WARN is logged; the route is not filtered).
The client IP is resolved through the same path as the rest of the data plane: the PROXY-protocol peer when a ClientTrafficPolicy enables PROXY protocol on the listener, otherwise the L4 downstream peer. There is no Gateway-side trusted-forwarded-header surface yet, so behind an L7 load balancer that terminates the connection, enable PROXY protocol so the real client IP reaches the filter.
Basic authentication¶
BasicAuth (gateway.coxswain-labs.dev/v1alpha1) validates Authorization: Basic credentials against an htpasswd Secret. Attach it to an HTTPRouteRule with an ExtensionRef filter — the Gateway API surface for the Ingress auth-basic-secret annotation. HTTP Basic auth is a browser/HTTP idiom, so this filter is not supported on GRPCRoute (gRPC clients authenticate with bearer tokens or mTLS instead).
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: BasicAuth
metadata:
name: office-only
spec:
secretRef:
name: office-htpasswd
namespace: default
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
# ...
filters:
- type: ExtensionRef
extensionRef:
group: gateway.coxswain-labs.dev
kind: BasicAuth
name: office-only
Semantics:
- The referenced
Secretmust carry the labelingress.coxswain-labs.dev/auth-basic: "true"and store the htpasswd file under the keyauth(nginx convention) — the same requirements as the Ingress annotation, so one Secret can back both surfaces. - Supported hash formats: bcrypt (
$2a$/$2b$/$2y$) and Apache SHA1 ({SHA}, accepted but logged as weak). - Valid credentials are forwarded; missing/invalid credentials get
401withWWW-Authenticate. - A missing, unlabeled, or unparseable Secret — or a missing
BasicAuthCR'ssecretRef— fails closed with503, distinct from a missingBasicAuthCR itself (which fails open: no auth enforced). - A
secretRefwhosenamespacediffers from theBasicAuthCR's namespace requires a matchingReferenceGrantin the Secret's namespace —fromaBasicAuth(gateway.coxswain-labs.dev) in the CR's namespace,toa coreSecret. Without the grant the reference fails closed (503); a tenant cannot bind another namespace's auth Secret. Same-namespace refs need no grant.
# In the Secret's namespace, to permit a BasicAuth in namespace `apps`:
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-basicauth-from-apps
spec:
from:
- group: gateway.coxswain-labs.dev
kind: BasicAuth
namespace: apps
to:
- group: ""
kind: Secret
JWT authentication¶
JwtAuth (gateway.coxswain-labs.dev/v1alpha1) validates a bearer token's signature against a JSON Web Key Set (JWKS) — the Coxswain implementation of Envoy's envoy.filters.http.jwt_authn JwtProvider / Istio's RequestAuthentication.jwtRules. Attach it to an HTTPRouteRule or GRPCRouteRule with an ExtensionRef filter — the Gateway API surface for the Ingress auth-jwt annotation. No Gateway API standard exists for in-proxy JWT validation (GEP-1494 covers delegated ext_authz, a different model). Unlike BasicAuth (an HTTP/browser idiom), bearer/JWT auth is a common gRPC pattern, so JwtAuth is supported on both route kinds.
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: JwtAuth
metadata:
name: my-api
spec:
issuer: https://issuer.example.com
audiences:
- my-api
jwks:
remote:
uri: https://issuer.example.com/.well-known/jwks.json
refreshInterval: 5m
claimToHeaders:
- claim: sub
header: x-user-id
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
# ...
filters:
- type: ExtensionRef
extensionRef:
group: gateway.coxswain-labs.dev
kind: JwtAuth
name: my-api
Semantics:
spec.jwksis exactly one of:remote.uri— a JWKS endpoint. Resolved by the controller, never the proxy (the Istio model, not Envoy's default proxy-side fetch): the read-only data plane never egresses to an identity provider.remote.refreshInterval(default5m) bounds the refetch cadence; a shorter upstreamCache-Control: max-ageis honored instead.inline.jwks— a JWKS object given directly in the spec (no controller fetch).- The bearer token is read from
Authorization: Bearer <token>by default, or fromfromHeaderswhen set. - Signature verification uses the key's own declared
alg, never the token header'salg(prevents algorithm-confusion attacks). Only asymmetric algorithms are supported (RS/PS/ES/EdDSA) — JWKS is inherently asymmetric. issmust matchspec.issuer.audis checked againstspec.audiencesonly whenaudiencesis non-empty.- On success,
claimToHeaderscopies named claims onto upstream request headers, andforwardPayloadHeader(if set) carries the base64url-encoded full claims payload. The original token is stripped from the upstream request unlessforward: true. - Missing/invalid/expired/wrong-issuer/wrong-audience tokens get
401withWWW-Authenticate: Bearer. - An unresolved JWKS (fetch not yet complete, fetch failing, or unparseable/empty) fails closed with
503— an operator who attached this filter expects enforcement. - A missing
JwtAuthCR fails open (no auth enforced), matching the otherExtensionRefauth resolvers.
External authorization (ext_authz)¶
CoxswainExternalAuth (gateway.coxswain-labs.dev/v1alpha1) delegates an allow/deny decision to an external authorization service before a request reaches its upstream — the Coxswain implementation of GEP-1494 and the Envoy / Istio / kgateway ext_authz model. The auth service is named by a backendRef (a Service + port), resolved to pod endpoints and load-balanced like any other backend; there is no URL form.
It is dual-surface:
- Route filter — reference it from an
HTTPRouteRulevia anExtensionReffilter (likeBasicAuth). - Gateway policy — attach it to a
Gatewayviaspec.targetRefs(likeClientTrafficPolicy), making it a default applied to every HTTPRoute on that Gateway.
Precedence is additive (GEP-713 override posture): when both a Gateway-attached policy and a route filter apply, the request must pass both checks, and the first hard-deny wins. A route filter can add checks but cannot remove a Gateway-level mandate — a platform-admin requirement is not weakenable by a tenant. Two policies targeting the same Gateway conflict: the older (by creationTimestamp, ties by name) wins and the loser gets Accepted=False, reason=Conflicted in its status.ancestors[].
Two transports, selected by spec.protocol:
HTTP— forward-auth: the original method, Host, path, and client headers are replayed to the service (no body); 2xx allows, any other status is returned to the client.GRPC— the Envoyenvoy.service.auth.v3.Authorization/Checkproto: the request context is sent as aCheckRequest; anOKstatus allows (copyingallowedResponseHeadersfrom the OK response onto the upstream request), any other status denies with the denied response's HTTP status (default403), headers, and body.
CoxswainExternalAuth is HTTPRoute-only (a Gateway-attached policy covers the HTTPRoutes on the Gateway); GRPCRoute is not yet supported.
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: CoxswainExternalAuth
metadata:
name: oauth2
spec:
protocol: HTTP # or GRPC
backendRef:
name: oauth2-proxy
port: 4180
timeout: 250ms
failClosed: true # deny (503) on auth-service error/timeout (default)
allowedResponseHeaders: # copied onto the upstream request on allow
- x-auth-user
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
# ...
filters:
- type: ExtensionRef
extensionRef:
group: gateway.coxswain-labs.dev
kind: ExternalAuth
name: oauth2
To make the check a Gateway-wide mandate instead, add targetRefs and omit the route filter:
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: CoxswainExternalAuth
metadata:
name: gateway-authn
spec:
protocol: GRPC
backendRef:
name: ext-authz
port: 9000
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: my-gateway
Fail-closed and cross-namespace rules:
failClosed: true(the default) denies with 503 when the auth service is unreachable, errors, or times out;failClosed: falsefails open (request proceeds unauthorized). AbackendRefthat resolves to no ready endpoints — or an unsupported protocol — always fails closed, regardless offailClosed.- A
backendRefwhosenamespacediffers from the policy's namespace requires a matchingReferenceGrant—fromaCoxswainExternalAuth(gateway.coxswain-labs.dev)toa coreService. Without it the reference fails closed (503). Same-namespace refs need no grant.
Request size limit¶
RequestSizeLimit (gateway.coxswain-labs.dev/v1alpha1) caps the request body size for a route. Attach it to an HTTPRouteRule with an ExtensionRef filter — the Gateway API surface for the Ingress max-body-size annotation. Like BasicAuth/Compression, this filter is HTTPRoute-only and is not enforced on GRPCRoute (see below).
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: RequestSizeLimit
metadata:
name: small-uploads
spec:
maxSize: "8m"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
# ...
filters:
- type: ExtensionRef
extensionRef:
group: gateway.coxswain-labs.dev
kind: RequestSizeLimit
name: small-uploads
Semantics:
maxSizeaccepts a bare byte count or ak/m/g-suffixed size (binary multipliers, case-insensitive) — the same parser as the Ingressmax-body-sizeannotation.- On HTTP/1.x, requests exceeding the limit are rejected with
413 Payload Too Large, checked up front againstContent-Lengthwhen present and mid-stream for chunked/streaming bodies. - On HTTP/2, only the up-front
Content-Lengthcheck applies. A streaming HTTP/2 upload that omitsContent-Lengthis not capped — it fails open (see the note below on why mid-stream HTTP/2 enforcement is deferred). - A missing
RequestSizeLimitCR or an unparseablemaxSizefails open (no limit enforced).
Request size limit is not enforced on GRPCRoute¶
RequestSizeLimit attached to a GRPCRoute is accepted but not enforced — the reconciler skips it and logs a WARN line (as it does for BasicAuth/Compression). gRPC message sizes are instead governed by the backend's own max_recv_msg_size (gRPC servers reject oversized messages with RESOURCE_EXHAUSTED; the default receive cap is ~4 MB).
The reason is a pingora-proxy limitation: a request_body_filter rejection over HTTP/2 is swallowed by pingora's h2 proxy loop and never delivered to the client, deadlocking the request. gRPC never sends Content-Length, so the up-front check that guards HTTP/2 elsewhere cannot apply. Faithful edge enforcement for gRPC/HTTP/2 needs buffer-first rejection (as Envoy's buffer filter does) and is deferred until pingora ships request-body buffering.
Response compression¶
Compression (gateway.coxswain-labs.dev/v1alpha1) enables gzip/brotli response compression for a route. Attach it to an HTTPRouteRule with an ExtensionRef filter — the same CRD the Ingress compression annotation references (see Ingress annotations). gRPC compresses per-message at the gRPC framing layer (grpc-encoding), not via HTTP Content-Encoding, so this filter is not supported on GRPCRoute; the proxy also refuses to compress any response whose Content-Type starts with application/grpc, even on an HTTPRoute (a gRPC-over-HTTPRoute edge case), regardless of the CR's types allow-list.
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: Compression
metadata:
name: default-compression
spec:
gzip: true
brotli: true
level: 6
minSize: 1024
types:
- text/html
- text/plain
- text/css
- application/json
- application/javascript
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
# ...
filters:
- type: ExtensionRef
extensionRef:
group: gateway.coxswain-labs.dev
kind: Compression
name: default-compression
Semantics:
- At least one of
gzip/brotlimust betruefor the CR to have any effect; when both arefalse(the default) it is a no-op. - Brotli is preferred over gzip when both are enabled and the client advertises
brinAccept-Encoding. level(1–9, default6),minSize(bytes, default1024), andtypes(default:text/html,text/plain,text/css,application/json,application/javascript) are the same defaults applied when the Ingresscompressionannotation resolves this CR.- A missing
CompressionCR fails open (no compression).
Status conditions¶
| Condition | True when |
|---|---|
Accepted |
The route is attached to a Gateway listener |
Programmed |
The route is active in the data plane |
ResolvedRefs |
All backendRefs resolve to a reachable Service |
Inspect conditions when traffic is not flowing:
kubectl describe httproute my-route
GRPCRoute¶
A GRPCRoute routes gRPC traffic attached to a Gateway listener. gRPC is HTTP/2 POST /{ServiceName}/{MethodName}, so no special listener protocol is required — an ordinary HTTP listener on the Gateway accepts gRPC connections.
Backend requirements¶
gRPC backends must advertise cleartext HTTP/2 (h2c) by setting appProtocol: kubernetes.io/h2c on the Service port. Coxswain uses prior-knowledge h2c to connect to the backend, which preserves gRPC trailers (grpc-status, grpc-message).
apiVersion: v1
kind: Service
metadata:
name: my-grpc-service
spec:
selector:
app: my-grpc-app
ports:
- port: 50051
targetPort: 50051
appProtocol: kubernetes.io/h2c # required for gRPC backends
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: my-grpc-route
namespace: default
spec:
parentRefs:
- name: my-gateway
hostnames:
- grpc.example.com
rules:
- matches:
- method:
type: Exact
service: com.example.MyService
method: SayHello
backendRefs:
- name: my-grpc-service
port: 50051
Method matching¶
| Spec | Behaviour |
|---|---|
No matches (or empty matches) |
Routes all gRPC traffic on attached listeners |
method.type: Exact, service + method |
Routes /{service}/{method} exactly |
method.type: Exact, service only |
Routes any method under /{service}/ |
method.type: Exact, method only |
Routes the method name on any service |
method.type: RegularExpression |
service and method are RE2 patterns |
Header matching uses the same Exact and RegularExpression semantics as HTTPRoute.
Supported fields¶
| Field | Support |
|---|---|
spec.parentRefs |
Full (including sectionName and port) |
spec.hostnames |
Full (including wildcards) |
spec.rules[].matches[].method |
Exact and RegularExpression |
spec.rules[].matches[].headers |
Full |
spec.rules[].filters |
RequestHeaderModifier, ResponseHeaderModifier, ExtensionRef (RateLimit, IpAccessControl, JwtAuth) |
spec.rules[].backendRefs |
Service backends only |
spec.rules[].backendRefs[].weight |
Full |
GRPCRoute supports the protocol-agnostic ExtensionRef filters — RateLimit, IpAccessControl, and JwtAuth (bearer/JWT auth is a common gRPC pattern, unlike BasicAuth) — which apply identically to gRPC (HTTP/2) traffic. PathRewriteRegex is not supported: for gRPC the request path is the /{service}/{method} RPC address, so rewriting it is meaningless. BasicAuth and Compression are HTTP-only idioms and are not supported either — gRPC clients authenticate with bearer tokens or mTLS, and gRPC compresses per-message at the framing layer rather than via HTTP Content-Encoding. RequestSizeLimit is also not enforced on gRPC — a mid-stream body cap over HTTP/2 deadlocks the client under pingora, so gRPC message sizes are left to the backend's max_recv_msg_size (details). Any other ExtensionRef (and RequestMirror) is skipped with a WARN log line.
Status conditions¶
| Condition | True when |
|---|---|
Accepted |
The route is attached to a Gateway listener |
Programmed |
The route is active in the data plane |
ResolvedRefs |
All backendRefs resolve to a reachable Service |
kubectl describe grpcroute my-grpc-route
CoxswainBackendPolicy¶
CoxswainBackendPolicy configures how the proxy talks to the pods behind a Service — connection timeouts, the load-balancing algorithm, a circuit breaker, and sticky sessions. Create one, point it at a Service by name, and every route that sends traffic to that Service picks up the settings — whether that route is an HTTPRoute, a GRPCRoute, or a classic Ingress. You do not add anything to the route itself; the policy attaches to the Service and takes effect automatically.
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: CoxswainBackendPolicy
metadata:
name: api-backend-policy
spec:
targetRefs:
- group: ""
kind: Service
name: api # <- must be a Service in this same namespace
timeouts:
connect: 500ms
targetRefs is the only required field. timeouts, loadBalancer, circuitBreaker, and sessionPersistence are all independent and optional — set only the ones you need; anything you omit keeps the default connection behavior (immediate connect with no timeout override, weighted round-robin, no circuit breaker, no sticky sessions).
Why a separate resource, not a route annotation or filter?
These four settings all describe the connection to the upstream Service, not anything about how a request is routed there — so unlike filters (retry, rate limiting, compression), which attach per-route, CoxswainBackendPolicy attaches per-Service (GEP-713 direct policy attachment). Two consequences follow: (1) if two routes (say an Ingress and an HTTPRoute) both send traffic to the same Service, they share one connection policy — that's intentional, since connection pooling and circuit breaking are properties of the upstream, not the route; (2) none of loadBalancer/circuitBreaker has a stable Gateway API standard to converge toward — Gateway API v1.6.0 covers neither (its closest concept, BackendLBPolicy, was replaced by an experimental type that only handles retry budgets and session persistence) — so those two fields are intentionally modeled after Envoy's native load-balancing policies and outlier detection instead. sessionPersistence does mirror Gateway API's own (experimental) SessionPersistence shape, as closely as Coxswain's persistence mechanism supports (see below).
Fields¶
| Field | Required? | Description |
|---|---|---|
targetRefs[] |
Yes | The Service objects this policy applies to, in the same namespace as the policy. Each entry: { group: "", kind: Service, name: <service-name> }. |
timeouts.connect |
optional | Upstream TCP-connect timeout (GEP-2257 duration, e.g. 500ms, 5s). If the proxy can't establish a connection to a pod within this time, it fails the request with 502 instead of waiting indefinitely. |
timeouts.idle |
optional | How long an idle, already-established connection to a pod is kept open in the connection pool before being closed. |
loadBalancer.algorithm |
optional | Which algorithm picks a pod for each request. See Load-balancing algorithm below for the full list of values. |
circuitBreaker.threshold |
optional | Error rate (%, 1–100) that trips the breaker. This is the on/off switch: omit it (or set it out of range) and the circuit breaker is disabled entirely — the other circuitBreaker.* fields have no effect on their own. See Circuit breaker below. |
circuitBreaker.window |
optional | How far back the proxy looks when computing the error rate. Default 10s. |
circuitBreaker.openDuration |
optional | Once tripped, how long the breaker stays open before it lets a test request through. Default 5s. |
circuitBreaker.minRequests |
optional | Don't trip the breaker until at least this many requests have been observed in the window — protects low-traffic routes from tripping on one unlucky failure. Default 10. |
circuitBreaker.maxOpenDuration |
optional | If a pod keeps failing its recovery checks, each re-trip doubles the open duration up to this cap, instead of always waiting the same openDuration. Omit for a constant (non-growing) open duration. |
sessionPersistence.type |
optional | How to pin a client to one pod: Cookie or Header. See Session persistence below for guidance on which to pick. |
sessionPersistence.sessionName |
conditionally required | The cookie name (Cookie mode — optional, defaults to __coxswain_session) or the request header to key on (Header mode — required, no default). |
Full example¶
apiVersion: gateway.coxswain-labs.dev/v1alpha1
kind: CoxswainBackendPolicy
metadata:
name: api-backend-policy
spec:
targetRefs:
- group: ""
kind: Service
name: api
timeouts:
connect: 500ms
idle: 60s
loadBalancer:
algorithm: least_conn
circuitBreaker:
threshold: 50
window: 10s
openDuration: 5s
minRequests: 10
sessionPersistence:
type: Cookie
sessionName: my-session
Behaviour¶
- A backend
Servicewith no attached policy keeps the default connection behaviour (weighted round-robin, breaker disabled, no session persistence). - The per-backend
timeouts.connecttakes precedence over the Gateway APIHTTPRoute.timeouts.backendRequestfallback. - Invalid values fail open. An unparseable duration, an unrecognised
loadBalancer.algorithm, an out-of-rangecircuitBreaker.threshold, or an unrecognisedsessionPersistence.typeis logged as a warning and ignored — the backend falls back to the default (round-robin / breaker disabled / no persistence), never a connection-level error or a rejected resource. These fields are deliberately not schema-validated so the policy is accepted and the warning surfaces at reconcile time. - Conflicts. If two policies target the same
Service, the older one (bycreationTimestamp, ties broken by name) wins; the loser receivesAccepted=False, reason=Conflictedin itsstatus.ancestors[].
Load-balancing algorithm¶
loadBalancer.algorithm selects the algorithm used to pick an upstream endpoint for each request within the backend group of a route:
| Value | Description |
|---|---|
round_robin |
(default) Weighted round-robin using the GCD-reduced slot array. Zero per-request overhead. |
least_conn |
Routes to the endpoint with the fewest in-flight requests. Maintains an atomic in-flight counter per endpoint; the counter is incremented on selection and decremented when the response completes (or when a retry selects a different endpoint). |
ewma |
Routes to the endpoint with the lowest exponentially-weighted moving-average response latency (α = 1/8). Unsampled endpoints (active=0) are probed first. Latency is folded in at end-of-request. |
ip_hash |
Alias for hash:source-ip (backward-compatible). |
hash:uri |
Consistent hash on the full request URI (path + query string). Requests to the same URI always land on the same endpoint. Falls back to round-robin if the path is empty. |
hash:source-ip |
Consistent hash on the resolved client IP (see trust-forwarded-for for Ingress, or the equivalent Gateway API resolution). Requests from the same IP always land on the same endpoint; unlike cookie affinity, no state is injected into the response. Falls back to round-robin if the client IP is unavailable. |
hash:header=<name> |
Consistent hash on the value of the named request header (e.g. hash:header=x-user-id). An empty or absent header falls back to round-robin. |
hash:cookie=<name> |
Consistent hash on the value of the named cookie (e.g. hash:cookie=session). An absent or empty cookie falls back to round-robin. |
All hash:* values (and ip_hash) use rendezvous (HRW) hashing: when an endpoint is removed, only its keys are redistributed; all other keys remain on their existing endpoints. This is strictly better than modulo hashing, which reshuffles nearly every key on a membership change. Unknown values warn and fall back to round_robin; routing is never interrupted.
Mapping to Istio/Envoy — loadBalancer.algorithm corresponds to DestinationRule.trafficPolicy.loadBalancer:
| Coxswain value | Istio / Envoy equivalent |
|---|---|
round_robin |
ROUND_ROBIN |
least_conn |
LEAST_REQUEST |
ewma |
LEAST_REQUEST with latency-weighted selection |
ip_hash / hash:source-ip |
CONSISTENT_HASH (useSourceIp: true) |
hash:uri |
CONSISTENT_HASH (HTTP URI — closest analogue) |
hash:header=<name> |
CONSISTENT_HASH (httpHeaderName: <name>) |
hash:cookie=<name> |
CONSISTENT_HASH (httpCookie.name: <name>) |
Performance — all algorithms run on the hot path without locks. round_robin allocates nothing per request. least_conn and ewma perform a linear scan over the endpoint list (typically 1–10 pods per Service) using relaxed atomics, which is negligible compared to I/O. hash:* values extract and hash the relevant request attribute with FNV-1a, then perform a linear rendezvous scan — negligible. The hash:uri path allocates a single joined path?query string only when a query string is present; all other hash sources are allocation-free on the hot path.
Circuit breaker¶
The per-upstream-endpoint circuit breaker trips when a backend pod's error rate exceeds threshold, returning fail-fast 503 responses to clients until the pod shows signs of recovery. This is the Coxswain equivalent of Envoy/Istio outlier detection: a single degraded pod trips only its own breaker; healthy pods serving the same route keep accepting traffic.
The breaker is implemented with failsafe's EWMA (exponentially weighted moving average) success-rate policy. Breaker state is tracked per (route, endpoint-IP:port) pair — one state machine per upstream pod, per route.
State machine:
- Closed (initial) — requests flow normally; errors accumulate against the EWMA window.
- Open — error rate exceeded
thresholdafterminRequestssamples; requests fail-fast 503 without reaching the upstream. The breaker stays Open foropenDuration(or exponentially longer, up tomaxOpenDuration, on repeated trips). - HalfOpen — after
openDurationone probe request is let through. If it succeeds, the breaker closes; if it fails, it re-opens for anotheropenDuration.
Observability — three Prometheus series on the proxy admin /metrics endpoint:
coxswain_proxy_circuit_breaker_state{route, upstream}—0= closed,1= open,2= half-open.coxswain_proxy_circuit_breaker_rejected_total{route, upstream}— count of fail-fast 503s issued while the breaker was open.coxswain_proxy_circuit_breaker_transitions_total{route, upstream, to}— cumulative state transitions;tois"open","half_open", or"closed".
Fail-fast behaviour: when the breaker is Open, the proxy returns 503 immediately without connecting to the upstream. The client sees 503; other healthy pods serving the same route continue accepting traffic via load-balancing.
Session persistence¶
"Session persistence" (also called sticky sessions) means every request from the same client keeps landing on the same backend pod, instead of being spread across all of them by the load-balancing algorithm. Use it when a pod holds state a client needs to come back to — an in-memory session, a WebSocket connection, an in-progress upload. A backend with no sessionPersistence configured is unaffected: it uses loadBalancer.algorithm (or round-robin) as normal.
There's no server-side table of "which client goes to which pod" — the pin is recomputed from the request itself every time, so it works identically across proxy replicas with no coordination between them needed. There are two ways the proxy identifies which client is which:
type: Cookie— pick this for browser clients (regular web traffic). On a client's first request, the proxy picks a pod as usual and sets a cookie identifying it (Set-Cookie: <sessionName>=<token>; Path=/; HttpOnly). Every later request that carries the cookie goes back to that same pod. You don't have to do anything client-side — the browser sends the cookie back automatically.sessionNameis optional here (defaults to__coxswain_session); if you set one that isn't a valid cookie name, the proxy warns and falls back to the default rather than rejecting the policy.type: Header— pick this for API/service clients that already send a stable identifier of their own (an API key, a tenant ID, a session token) as a request header. The proxy hashes that header's value to consistently pick one pod — no cookie is set. UnlikeCookiemode,sessionNameis required here (it's the name of the header to key on); if you forget it, persistence is silently disabled for that policy (a warning is logged) and the route falls back to plain round-robin rather than breaking.
What happens when the pinned pod goes away: if the pod a client was pinned to gets scaled down or replaced, the next request from that client no longer finds it. Rather than failing, the proxy falls back to round-robin and (in Cookie mode) picks a new pod and re-pins with a fresh cookie.
What's intentionally not supported yet: Gateway API's own (experimental) SessionPersistence type also lets you set a timeout after which a session expires from inactivity (idleTimeout) or expires unconditionally (absoluteTimeout). Coxswain doesn't support either yet — since there's no server-side session table, there's nothing to time out. Adding real inactivity-based expiry would mean giving the proxy a way to track "when did I last see this client" per pinned session, which is real new work. Until then, a session stays pinned for as long as its pod keeps running.
Status¶
The controller writes one status.ancestors[] entry per targeted Service with an Accepted condition:
kubectl describe coxswainbackendpolicy api-backend-timeouts
TLSRoute¶
A TLSRoute routes raw TLS connections by SNI. Coxswain supports three modes, configured via tls.mode on the Gateway listener:
- Passthrough — the proxy peeks the ClientHello SNI and splices the still-encrypted byte stream directly to the backend. TLS is terminated at the backend pod (GEP-2643).
- Terminate — the proxy terminates TLS using the listener certificate, then L4-splices the decrypted stream to a plaintext TCP backend.
- Mixed — a single Gateway port carries both Passthrough and Terminate listeners, disambiguated by SNI hostname.
Gateway listener (Passthrough)¶
Use protocol: TLS with tls.mode: Passthrough on the listener. No certificateRefs are needed — the proxy never holds or inspects a certificate on this path.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: coxswain
listeners:
- name: passthrough
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: Same
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
name: my-tls-route
namespace: default
spec:
parentRefs:
- name: my-gateway
sectionName: passthrough
hostnames:
- app.example.com # matched against the TLS ClientHello SNI
rules:
- backendRefs:
- name: my-tls-service
port: 443
The backend Service receives the unmodified TLS stream; its pod terminates TLS and sees the client's original handshake.
SNI matching¶
| Hostname format | Behaviour |
|---|---|
app.example.com |
Exact SNI match |
*.example.com |
Wildcard: matches any number of labels (foo.example.com, a.b.example.com) |
| (omitted) | Catch-all: matches any SNI that no other rule handles |
Matching follows Gateway API hostname precedence: exact before wildcard before catch-all.
Note
Wildcard hostname semantics here are routing-only (not RFC 6125 cert validation — no cert is involved at the proxy). Any number of DNS labels are matched by *, consistent with Gateway API's HTTPRoute wildcard semantics.
Supported fields¶
| Field | Support |
|---|---|
spec.parentRefs |
Full (including sectionName and port) |
spec.hostnames |
Full (exact, wildcard, omitted catch-all) |
spec.rules[].backendRefs |
Service backends only |
spec.rules[].backendRefs[].weight |
Full |
Status conditions¶
| Condition | True when |
|---|---|
Accepted |
The route is attached to a TLS/Passthrough or TLS/Terminate listener |
ResolvedRefs |
All backendRefs resolve to a reachable Service |
kubectl describe tlsroute my-tls-route
Terminate mode¶
In terminate mode the proxy holds the TLS session. The listener must carry a certificateRefs entry pointing to a kubernetes.io/tls Secret. Coxswain selects the certificate by SNI using the same mechanism as HTTPS listeners. The TLSRoute backend receives a plaintext TCP stream; no TLS certificate is needed at the backend.
Gateway listener¶
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: coxswain
listeners:
- name: terminate
port: 443
protocol: TLS
hostname: app.example.com
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: my-tls-cert # must exist in the same namespace
allowedRoutes:
namespaces:
from: Same
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
name: my-terminate-route
namespace: default
spec:
parentRefs:
- name: my-gateway
sectionName: terminate
hostnames:
- app.example.com
rules:
- backendRefs:
- name: my-plaintext-service # backend receives decrypted TCP, no TLS required
port: 8080
The proxy performs an SNI peek on accept, looks up the certificate, completes the TLS handshake, then L4-splices the decrypted byte stream to the backend. HTTP-layer parsing does not occur — this is a raw TCP splice post-decryption, not an HTTPS proxy.
Mixed mode¶
A single Gateway port can carry both a Terminate and a Passthrough TLS listener simultaneously. The proxy disambiguates by SNI hostname: traffic whose SNI matches the Terminate listener's hostname is decrypted at the proxy; traffic whose SNI matches the Passthrough listener's hostname is forwarded encrypted to the backend. The two routing tables are isolated — a miss in one never leaks into the other.
Gateway listener¶
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-mixed-gateway
namespace: default
spec:
gatewayClassName: coxswain
listeners:
- name: tls-terminate
port: 443
protocol: TLS
hostname: terminate.example.com
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: my-tls-cert
allowedRoutes:
namespaces:
from: Same
- name: tls-passthrough
port: 443
protocol: TLS
hostname: passthrough.example.com
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: Same
Example¶
# Terminate route — backend is a plaintext TCP service
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
name: terminate-route
namespace: default
spec:
parentRefs:
- name: my-mixed-gateway
sectionName: tls-terminate
hostnames:
- terminate.example.com
rules:
- backendRefs:
- name: plaintext-service
port: 8080
---
# Passthrough route — backend terminates TLS itself
apiVersion: gateway.networking.k8s.io/v1
kind: TLSRoute
metadata:
name: passthrough-route
namespace: default
spec:
parentRefs:
- name: my-mixed-gateway
sectionName: tls-passthrough
hostnames:
- passthrough.example.com
rules:
- backendRefs:
- name: tls-backend
port: 8443
Note
Both listeners must use distinct hostnames on the shared port. An SNI that matches neither listener is dropped — the proxy never falls through from one table to the other.
TCPRoute¶
A TCPRoute routes raw TCP connections purely by listener port — there is no SNI or hostname dimension, and no HTTP-layer parsing. The proxy dials the bound backend on accept and splices the two byte streams together (GEP-1901).
Gateway listener¶
Use protocol: TCP. A TCP listener never shares a port with another protocol — Gateway API's own port-compatibility rules exclude the combination.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: coxswain
listeners:
- name: tcp-proxy
port: 5432
protocol: TCP
allowedRoutes:
namespaces:
from: Same
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: TCPRoute
metadata:
name: my-tcp-route
namespace: default
spec:
parentRefs:
- name: my-gateway
rules:
- backendRefs:
- name: my-tcp-service
port: 5432
Supported fields¶
| Field | Support |
|---|---|
spec.parentRefs |
Full (including sectionName and port) |
spec.rules[].backendRefs |
Service backends only |
spec.rules[].backendRefs[].weight |
Full |
The Standard channel constrains TCPRoute to exactly one rule with no matches; when two TCPRoutes bind the same listener port, the highest-precedence route (oldest creationTimestamp, then name) wins.
Status conditions¶
| Condition | True when |
|---|---|
Accepted |
The route is attached to a protocol: TCP listener |
ResolvedRefs |
All backendRefs resolve to a reachable Service |
kubectl describe tcproute my-tcp-route
UDPRoute¶
A UDPRoute forwards UDP datagrams purely by listener port — same port-keyed model as TCPRoute, no SNI or hostname dimension (GEP-2645).
UDP is connectionless, so the proxy can't reuse a dial-once-and-splice model. Instead, the first datagram from a client address picks a backend (via the same weighted load-balancing as every other route kind) and pins it for that client's session; a background task relays the backend's replies back to the client. A session with no activity for --proxy-udp-session-timeout (default 10s) is evicted — the next datagram from that client picks a backend afresh.
Gateway listener¶
Use protocol: UDP. Like TCP, a UDP listener never shares a port with another protocol.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: coxswain
listeners:
- name: udp-proxy
port: 5353
protocol: UDP
allowedRoutes:
namespaces:
from: Same
Example¶
apiVersion: gateway.networking.k8s.io/v1
kind: UDPRoute
metadata:
name: my-udp-route
namespace: default
spec:
parentRefs:
- name: my-gateway
rules:
- backendRefs:
- name: my-udp-service
port: 5353
Supported fields¶
| Field | Support |
|---|---|
spec.parentRefs |
Full (including sectionName and port) |
spec.rules[].backendRefs |
Service backends only |
spec.rules[].backendRefs[].weight |
Full — each new client session picks a backend independently, so weights converge across sessions rather than within a single client's traffic |
The Standard channel constrains UDPRoute to exactly one rule with no matches; when two UDPRoutes bind the same listener port, the highest-precedence route (oldest creationTimestamp, then name) wins.
Status conditions¶
| Condition | True when |
|---|---|
Accepted |
The route is attached to a protocol: UDP listener |
ResolvedRefs |
All backendRefs resolve to a reachable Service |
kubectl describe udproute my-udp-route