Skip to main content

Gateway API 1.4 Stabilizes BackendTLSPolicy: Encrypting the Gateway-to-Pod Hop Without a Service Mesh

10 min readDora NodaDora Noda
Share
On this page

Your gateway terminates TLS for every tenant custom domain at the edge — and then forwards the decrypted request in plaintext across the cluster network to the tenant's Pod. On a shared multi-tenant cluster, that second hop crosses a network that namespace boundaries don't encrypt: any host-level observer on the path sees request bodies, cookies, and authorization headers in the clear. Gateway API v1.4.0, released on October 6, 2025, finally closes that gap with a portable API: BackendTLSPolicy graduated to the Standard channel, so a gateway can originate and verify TLS to backends without annotations, vendor CRDs, or a full service mesh installed for one hop.

The shape of the fix is one policy object:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
  name: tenant-a-backend-tls
  namespace: tenant-a
spec:
  targetRefs:
    - group: ""
      kind: Service
      name: tenant-a-web
      sectionName: https
  validation:
    caCertificateRefs:
      - group: ""
        kind: ConfigMap
        name: tenant-a-backend-ca
    hostname: web.tenant-a.svc.cluster.local

That is the whole contract: traffic to the tenant-a-web Service now goes over TLS, validated against the CA bundle in the referenced ConfigMap, with the backend certificate required to match web.tenant-a.svc.cluster.local. The rest of this post covers what exactly shipped in 1.4, how each field behaves (including the fail-closed semantics), the complete end-to-end YAML, who supports it today, and what it changes for a self-hosted multi-tenant platform.

What Gateway API 1.4 actually shipped

The v1.4.0 announcement promotes three features to the Standard channel — Gateway API's GA channel — and adds three experimental ones. You don't need a newer Kubernetes for any of it: 1.4 works on any cluster running Kubernetes 1.26 or later, since Gateway API ships as CRDs, not as part of kube itself.

FeatureChannelWhat it is
BackendTLSPolicy (GEP-1897)StandardTLS configuration for the gateway-to-backend connection
supportedFeatures in GatewayClass status (GEP-2162)StandardImplementations declare capabilities; conformance tests auto-select from it
Named route rules (GEP-995)StandardOptional name on HTTPRouteRule, GRPCRouteRule, and friends
XMesh, default Gateways, externalAuth filterExperimentalMesh config surface, implicit parent gateways, ext-authz callouts

Two of the graduates matter directly for this story. Named rules let status conditions, metrics, and policies address one rule inside a route instead of the whole object — you'll see a named rule in the example below, and it's what future per-rule policy targeting keys off via sectionName. supportedFeatures is the discovery mechanism: before you rely on BackendTLSPolicy on a given GatewayClass, you can read .status.supportedFeatures (or the published conformance report, which the test suite now derives from that field automatically) instead of guessing from vendor docs. Seven implementations were already reporting v1.4.0 conformance at release time.

The headline graduate is BackendTLSPolicy, and the release notes state the motivation bluntly: prior to GEP-1897, there was no API specification at all that allowed encrypted traffic on the hop from gateway to backend. Every implementation invented its own mechanism — an Istio DestinationRule, OpenShift route TLS settings, Contour annotations, GKE backend-service config — and none of it transferred.

How BackendTLSPolicy works

BackendTLSPolicy is a direct-attached policy (spec.targetRefs plus spec.validation, short name btlspolicy) defined in GEP-1897. The mental model is small: it tells the gateway "connect to this Service using TLS, and validate what it serves." Field by field:

FieldRequiredBehavior
spec.targetRefsYesServices to encrypt (up to 16 entries; implementations are advised to support one for now). sectionName selects a single named Service port.
spec.validation.hostnameYesUsed as the TLS SNI when dialing the backend, and the served certificate must match it — unless subjectAltNames is set, in which case matching moves there.
spec.validation.caCertificateRefsOne of the two trust sources is requiredConfigMap (Core support) holding the CA bundle under the ca.crt key. Same-namespace references only.
spec.validation.wellKnownCACertificatesSet to "System" to trust the gateway's OS CA bundle instead. Implementation-specific — confirm support first.
spec.validation.subjectAltNamesNoExtended. Up to 5 SANs (Hostname or URI, including SPIFFE IDs) that replace hostname for certificate matching.
spec.optionsNoImplementation-specific extras (minimum TLS version, cipher suites) under domain-prefixed keys.

Three semantics deserve emphasis because they decide real rollout plans.

First, the policy never mints certificates. It only describes how the gateway dials and what it trusts. The backend Pod must already serve TLS — via cert-manager, a baked-in cert, or a mesh sidecar. If nothing on the backend speaks TLS, there is nothing to validate against, and the policy can't help. Certificate issuance stays a separate concern, which is exactly why the end-to-end example below starts there.

Second, failure is closed, not open. If the CA reference can't be resolved, the policy is invalid, or validation fails, the implementation must not silently fall back to plaintext. The connection fails and the client gets an HTTP 5xx, with Accepted / ResolvedRefs status conditions (InvalidCACertificateRef, NoValidCACertificate, Conflicted, and friends) explaining why. When two policies select the same target, the older one wins and the loser reports Conflicted. This is the behavior you want from a security control: a misconfigured policy breaks loudly instead of downgrading quietly.

Third, scope is TCP to a Service. Attaching to a UDP port sets Accepted: False, and only the HTTPRoute backendRef to Service path has Extended support. Using the policy for infrastructure services, external-auth backends, or mesh workload traffic is explicitly implementation-specific — portable today for gateway-to-app traffic, check-your-vendor beyond that.

The complete YAML: edge TLS plus an encrypted second hop

Here is the full chain for one tenant on a shared gateway: the backend serves TLS with a cert-manager certificate, the CA lands in a ConfigMap, the Service exposes a named https port, the policy binds them, and the route carries the tenant's public hostname.

Start with issuance. The backend needs a certificate whose SAN matches the internal hostname the gateway will dial and validate:

yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: tenant-a-web
  namespace: tenant-a
spec:
  secretName: tenant-a-web-tls
  dnsNames:
    - web.tenant-a.svc.cluster.local
  issuerRef:
    name: tenant-a-ca
    kind: Issuer

Mount tenant-a-web-tls into the workload and serve HTTPS on port 8443. Then publish the CA bundle where the gateway can read it — the ConfigMap key must be ca.crt (a trust-manager Bundle or cert-manager cainjector keeps this in sync on rotation):

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: tenant-a-backend-ca
  namespace: tenant-a
data:
  ca.crt: |
    -----BEGIN CERTIFICATE-----
    # CA bundle that signed the backend certificate
    -----END CERTIFICATE-----

The Service exposes the encrypted port under a name, because the policy's sectionName keys off the port name:

yaml
apiVersion: v1
kind: Service
metadata:
  name: tenant-a-web
  namespace: tenant-a
spec:
  selector:
    app: tenant-a-web
  ports:
    - name: https
      port: 443
      targetPort: 8443

The policy itself binds Service to trust bundle and hostname:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
  name: tenant-a-backend-tls
  namespace: tenant-a
spec:
  targetRefs:
    - group: ""
      kind: Service
      name: tenant-a-web
      sectionName: https
  validation:
    caCertificateRefs:
      - group: ""
        kind: ConfigMap
        name: tenant-a-backend-ca
    hostname: web.tenant-a.svc.cluster.local

And the route stays exactly as it is today — edge termination for the tenant's public domain, with a 1.4 named rule so status and observability can point at it precisely:

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: tenant-a-web
  namespace: tenant-a
spec:
  parentRefs:
    - name: shared-gateway
      namespace: gateway-system
  hostnames:
    - app.tenant-a.example.com
  rules:
    - name: web
      backendRefs:
        - name: tenant-a-web
          port: 443

Roll it out the way the status conditions invite: apply the backend TLS and CA first, then the policy, then watch kubectl describe backendtlspolicy tenant-a-backend-tls for Accepted: True and ResolvedRefs: True before sending traffic. Because failures surface as 5xx with a reason instead of silent plaintext, a canary route against the same Service is a cheap verification step.

One portability note: if your backends serve publicly-trusted certificates, wellKnownCACertificates: "System" replaces the whole ConfigMap — but it's implementation-specific, so verify your GatewayClass honors it before deleting the CA plumbing.

Who supports it, and the honest limits

The implementation picture is further along than a just-graduated API usually manages, because most vendors prototyped the v1alpha2/v1alpha3 shapes earlier. Envoy Gateway, Istio, and kgateway all originate backend TLS through BackendTLSPolicy, Traefik's Gateway API provider tracks v1.4.0 including the policy, and NGINX Gateway Fabric documents a backend-traffic how-to around it. The practical check is conformance: read the implementation's report (now auto-derived from supportedFeatures) for the BackendTLSPolicy feature rather than trusting a version number.

The sharpest comparison is against the mesh you no longer need for this one job. An Istio ambient install secures both legs — the gateway terminates ingress TLS and its outbound leg to backends rides mTLS over ztunnel — while a standalone gateway without the policy leaves the backend leg as plaintext over the cluster SDN. BackendTLSPolicy gives the standalone gateway the second leg without the mesh's control plane, per-pod proxies, and operational surface. That reframes the mesh decision: adopt one for pod-to-pod east-west identity and authorization, not merely to encrypt gateway-to-app traffic.

Honest limits, all worth knowing before you standardize on it:

  • The backend must serve TLS. The most common failed rollout will be a policy pointing at a backend that only speaks HTTP. There is no auto-encryption here; pair the policy with cert-manager issuance per workload.
  • No portable per-backend ALPN control yet. Upstream issue 4833 tracks expressing the ALPN list the gateway offers as a TLS client — relevant for gRPC (h2) backends behind an HTTPRoute. Until it lands, ALPN behavior is implementation-defined.
  • Multi-target status is still rough. Several targetRefs rolling up to one Gateway can't be fully represented in status today; the documented guidance is one target per policy.
  • Trust references are same-namespace. Cross-namespace CA sharing needs per-namespace copies (or a syncing controller), which shapes how a platform distributes its cluster CA.

What this means for a self-hosted multi-tenant PaaS

For a platform that terminates every tenant's custom domain at a shared gateway, the second hop was always the awkward paragraph in the security story: "encrypted in transit" was true for the internet leg and aspirational inside the cluster. A graduated, conformance-tested BackendTLSPolicy turns it into a per-tenant object the platform reconciles like any other — no DaemonSet of sidecars, no per-implementation annotation dialect. The rollout checklist writes itself:

  1. Per-tenant hostname discipline. hostname must match the served cert, so each tenant's internal Service DNS name (or a dedicated SAN) becomes part of the tenant's identity. Mint backend certs from the same pipeline that provisions the tenant.
  2. CA distribution per namespace. Refs can't cross namespaces, so the platform controller copies or syncs the cluster CA bundle into every tenant namespace — trust-manager exists precisely for this.
  3. Rotation without flag days. Backend cert rotation is the existing cert-manager story; the gateway re-reads the CA from the ConfigMap, so rotation stays a data-plane non-event as long as the bundle stays valid.
  4. Status-driven cutover. Gate traffic on Accepted/ResolvedRefs per policy. Oldest-wins conflict handling means a stale duplicate policy fails safe and visibly.
  5. Know where the policy ends. Pod-to-pod traffic between tenant services, identity-based authorization, and SPIFFE URI SAN matching (Extended support) are still mesh or per-implementation territory. Encrypt the gateway leg with the standard; reach for heavier machinery only past it.

Compliance teams will notice, too: frameworks that ask for encryption in transit generally mean every hop, and "the CNI overlay is trusted" has always been the weakest sentence in that audit answer. A policy object per tenant with machine-readable status is a far easier artifact to show an auditor than a network diagram with a trust boundary drawn around it.

The hallway finally gets a lock

Gateway API spent its early releases standardizing the front door — listeners, routes, attachment. v1.4 turns to the hallway behind it: the gateway-to-Pod hop that every shared cluster quietly left in plaintext. BackendTLSPolicy graduating to Standard means that encryption is now a portable, conformance-tested API instead of five vendors' worth of annotations, and the fail-closed semantics mean a misconfiguration pages you instead of downgrading you. If you run a shared gateway today, inventory which backend Services still receive plaintext, pick one low-risk tenant, and walk the five-manifest chain above. The mesh conversation can wait until you actually need east-west identity — this hop no longer requires it.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

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