Repository: envoyproxy/gateway
Pull request #6092 (2025-05-16 05:15:05)

fix(tranlator): SubjectAltNames were being dropped from BackendTLSPolicy.validation
**What this PR does / why we need it**:

This PR adds support for configuring Subject Alternative Names (SANs) in the mTLS validation context for upstream connections, based on the `BackendTLSPolicy.validation.subjectAltNames` field as defined in the Gateway API ([reference](https://gateway-api.sigs.k8s.io/reference/spec/#backendtlspolicyvalidation)). 

Previously, only the `hostname` field was used to populate the Envoy config for upstream TLS validation. This change ensures that entries from `subjectAltNames` are also translated and included in the generated Envoy configuration, allowing for more flexible and secure mTLS setups (e.g., with Istio-managed sidecars or other backends requiring SAN matching).

**Release Notes**: Yes

```yaml
- Added support for configuring Subject Alternative Names (SANs) in upstream mTLS validation via `BackendTLSPolicy.validation.subjectAltNames`.
```


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `8.57143%` with `32 lines` in your changes missing coverage. Please review.
> Project coverage is 70.35%. Comparing base [(`3e1bce3`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/3e1bce315feba4b197f5c8ae520f029c210b51fd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`50038d4`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/50038d404a726bd95a6c51e0f693d00a6b018e91?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 6 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/xds/translator/translator.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?src=pr&el=tree&filepath=internal%2Fxds%2Ftranslator%2Ftranslator.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvdHJhbnNsYXRvci5nbw==) | 0.00% | [23 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |
| [internal/gatewayapi/backendtlspolicy.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?src=pr&el=tree&filepath=internal%2Fgatewayapi%2Fbackendtlspolicy.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZ2F0ZXdheWFwaS9iYWNrZW5kdGxzcG9saWN5Lmdv) | 27.27% | [7 Missing and 1 partial :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

:x: Your patch check has failed because the patch coverage (8.57%) is below the target coverage (60.00%). You can increase the patch coverage or adjust the [target](https://docs.codecov.com/docs/commit-status#target) coverage.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6092      +/-   ##
==========================================
- Coverage   70.43%   70.35%   -0.09%     
==========================================
  Files         217      217              
  Lines       36129    36162      +33     
==========================================
- Hits        25447    25440       -7     
- Misses       9166     9203      +37     
- Partials     1516     1519       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/6092?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
arkodg: thanks @ankushagarwal ! the PR looks good, added a comment
can you also add a test case in 
* gateway-api layer https://github.com/envoyproxy/gateway/tree/main/internal/gatewayapi/testdata
* xds-translator layer https://github.com/envoyproxy/gateway/tree/main/internal/xds/translator/testdata/in/xds-ir

`make testdata` is useful to generate testdata output
ankushagarwal: Thank you for the feedback, @arkodg

Addressed your comment and added tests in both the places 
ankushagarwal: (attempted to fix lint failure)
arkodg: still seeing them @ankushagarwal 
```
./internal/xds/translator/testdata/in/xds-ir/http-route-with-tlsbundle.yaml
  Error: 30:17 [indentation] wrong indentation: expected 18 but found 16

./internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.in.yaml
  Error: 134:9 [indentation] wrong indentation: expected 10 but found 8

internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.in.yaml
  Error: 134:9 [indentation] wrong indentation: expected 10 but found 8

internal/xds/translator/testdata/in/xds-ir/http-route-with-tlsbundle.yaml
  Error: 30:17 [indentation] wrong indentation: expected 18 but found 16
  ```
arkodg: running `make lint.fix-golint` may fix it
arkodg: nvm it won't, because the error is from `yamllint`
ankushagarwal: @arkodg : Is the failing e2e-test a flake or a true test failure
arkodg: its a flake, just waiting on a 2nd approval to merge this one

Patch:
diff --git a/internal/gatewayapi/backendtlspolicy.go b/internal/gatewayapi/backendtlspolicy.go
index 143bc749f..dc5c86b0f 100644
--- a/internal/gatewayapi/backendtlspolicy.go
+++ b/internal/gatewayapi/backendtlspolicy.go
@@ -207,9 +207,25 @@ func getBackendTLSPolicy(
 }
 
 func getBackendTLSBundle(backendTLSPolicy *gwapiv1a3.BackendTLSPolicy, resources *resource.Resources) (*ir.TLSUpstreamConfig, error) {
+	// Translate SubjectAltNames from gwapiv1a3 to ir
+	var subjectAltNames []ir.SubjectAltName
+	for _, san := range backendTLSPolicy.Spec.Validation.SubjectAltNames {
+		var subjectAltName ir.SubjectAltName
+		switch san.Type {
+		case "DNS":
+			subjectAltName.Hostname = ptr.To(string(san.Hostname))
+		case "URI":
+			subjectAltName.URI = ptr.To(string(san.URI))
+		default:
+			continue // skip unknown types
+		}
+		subjectAltNames = append(subjectAltNames, subjectAltName)
+	}
+
 	tlsBundle := &ir.TLSUpstreamConfig{
 		SNI:                 ptr.To(string(backendTLSPolicy.Spec.Validation.Hostname)),
 		UseSystemTrustStore: ptr.Deref(backendTLSPolicy.Spec.Validation.WellKnownCACertificates, "") == gwapiv1a3.WellKnownCACertificatesSystem,
+		SubjectAltNames:     subjectAltNames,
 	}
 	if tlsBundle.UseSystemTrustStore {
 		tlsBundle.CACertificate = &ir.TLSCACertificate{
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index b3759e45f..bd93162be 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -445,6 +445,20 @@ type TLSCACertificate struct {
 	Certificate []byte `json:"certificate,omitempty" yaml:"certificate,omitempty"`
 }
 
+// SubjectAltName holds the subject alternative name for the certificate
+// This is the internal representation of the SubjectAltName in the Gateway API
+// https://github.com/kubernetes-sigs/gateway-api/blob/6fbbd90954c2a30a6502cbb22ec6e7f3359ed3a0/apis/v1alpha3/backendtlspolicy_types.go#L177
+// +k8s:deepcopy-gen=true
+type SubjectAltName struct {
+	// Only one of the following fields should be set.
+	// Hostname contains Subject Alternative Name specified in DNS name format.
+	Hostname *string `json:"hostname,omitempty" yaml:"hostname,omitempty"`
+	// URI contains Subject Alternative Name specified in a full URI format.
+	// It MUST include both a scheme (e.g., "http" or "ftp") and a scheme-specific-part.
+	// Common values include SPIFFE IDs like "spiffe://mycluster.example.com/ns/myns/sa/svc1sa".
+	URI *string `json:"uri,omitempty" yaml:"uri,omitempty"`
+}
+
 func (t TLSCertificate) Validate() error {
 	var errs error
 	if len(t.Certificate) == 0 {
@@ -2855,6 +2869,7 @@ type TLSUpstreamConfig struct {
 	UseSystemTrustStore bool              `json:"useSystemTrustStore,omitempty" yaml:"useSystemTrustStore,omitempty"`
 	CACertificate       *TLSCACertificate `json:"caCertificate,omitempty" yaml:"caCertificate,omitempty"`
 	TLSConfig           `json:",inline"`
+	SubjectAltNames     []SubjectAltName `json:"subjectAltNames,omitempty" yaml:"subjectAltNames,omitempty"`
 }
 
 func (t *TLSUpstreamConfig) ToTLSConfig() (*tls.Config, error) {
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index eb5f63edb..73b24e66b 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -3213,6 +3213,31 @@ func (in *StringMatch) DeepCopy() *StringMatch {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SubjectAltName) DeepCopyInto(out *SubjectAltName) {
+	*out = *in
+	if in.Hostname != nil {
+		in, out := &in.Hostname, &out.Hostname
+		*out = new(string)
+		**out = **in
+	}
+	if in.URI != nil {
+		in, out := &in.URI, &out.URI
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubjectAltName.
+func (in *SubjectAltName) DeepCopy() *SubjectAltName {
+	if in == nil {
+		return nil
+	}
+	out := new(SubjectAltName)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TCPClientTimeout) DeepCopyInto(out *TCPClientTimeout) {
 	*out = *in
@@ -3588,6 +3613,13 @@ func (in *TLSUpstreamConfig) DeepCopyInto(out *TLSUpstreamConfig) {
 		(*in).DeepCopyInto(*out)
 	}
 	in.TLSConfig.DeepCopyInto(&out.TLSConfig)
+	if in.SubjectAltNames != nil {
+		in, out := &in.SubjectAltNames, &out.SubjectAltNames
+		*out = make([]SubjectAltName, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSUpstreamConfig.
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index 5118ffd32..bbe28719d 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -1043,6 +1043,30 @@ func buildXdsUpstreamTLSSocketWthCert(tlsConfig *ir.TLSUpstreamConfig) (*corev3.
 				},
 			},
 		}
+		for _, san := range tlsConfig.SubjectAltNames {
+			var sanType tlsv3.SubjectAltNameMatcher_SanType
+			var value string
+
+			// Exactly one of san.Hostname or san.URI is guaranteed to be set
+			if san.Hostname != nil {
+				sanType = tlsv3.SubjectAltNameMatcher_DNS
+				value = *san.Hostname
+			} else if san.URI != nil {
+				sanType = tlsv3.SubjectAltNameMatcher_URI
+				value = *san.URI
+			}
+			validationContext.DefaultValidationContext.MatchTypedSubjectAltNames = append(
+				validationContext.DefaultValidationContext.MatchTypedSubjectAltNames,
+				&tlsv3.SubjectAltNameMatcher{
+					SanType: sanType,
+					Matcher: &matcherv3.StringMatcher{
+						MatchPattern: &matcherv3.StringMatcher_Exact{
+							Exact: value,
+						},
+					},
+				},
+			)
+		}
 	}
 
 	tlsCtx := &tlsv3.UpstreamTlsContext{
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 818799641..d7a4f6586 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -10,6 +10,7 @@ security updates: |
 new features: |
   Added support for percentage-based request mirroring
   Add an option to OIDC authentication to bypass it and defer to JWT when the request contains an "Authorization: Bearer ..." header.
+  Added support for configuring Subject Alternative Names (SANs) for upstream TLS validation via `BackendTLSPolicy.validation.subjectAltNames`.
 
 bug fixes: |
 


Test patch:
diff --git a/internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.in.yaml b/internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.in.yaml
new file mode 100644
index 000000000..2b349c778
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.in.yaml
@@ -0,0 +1,137 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: Gateway
+    metadata:
+      name: gateway-btls
+      namespace: envoy-gateway
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: http
+          protocol: HTTP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+httpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1
+    kind: HTTPRoute
+    metadata:
+      name: httproute-btls
+      namespace: envoy-gateway
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-btls
+          sectionName: http
+      rules:
+        - matches:
+            - path:
+                type: Exact
+                value: "/exact"
+          backendRefs:
+            - name: http-backend
+              namespace: backends
+              port: 8080
+
+referenceGrants:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: ReferenceGrant
+    metadata:
+      name: refg-route-svc
+      namespace: backends
+    spec:
+      from:
+        - group: gateway.networking.k8s.io
+          kind: HTTPRoute
+          namespace: envoy-gateway
+        - group: gateway.networking.k8s.io
+          kind: Gateway
+          namespace: envoy-gateway
+      to:
+        - group: ""
+          kind: Service
+
+services:
+  - apiVersion: v1
+    kind: Service
+    metadata:
+      name: http-backend
+      namespace: backends
+    spec:
+      clusterIP: 10.11.12.13
+      ports:
+        - port: 8080
+          name: http
+          protocol: TCP
+          targetPort: 8080
+
+endpointSlices:
+  - apiVersion: discovery.k8s.io/v1
+    kind: EndpointSlice
+    metadata:
+      name: endpointslice-http-backend
+      namespace: backends
+      labels:
+        kubernetes.io/service-name: http-backend
+    addressType: IPv4
+    ports:
+      - name: http
+        protocol: TCP
+        port: 8080
+    endpoints:
+      - addresses:
+          - "10.244.0.11"
+        conditions:
+          ready: true
+
+configMaps:
+  - apiVersion: v1
+    kind: ConfigMap
+    metadata:
+      name: ca-cmap
+      namespace: backends
+    data:
+      ca.crt: |
+        -----BEGIN CERTIFICATE-----
+        MIIDJzCCAg+gAwIBAgIUAl6UKIuKmzte81cllz5PfdN2IlIwDQYJKoZIhvcNAQEL
+        BQAwIzEQMA4GA1UEAwwHbXljaWVudDEPMA0GA1UECgwGa3ViZWRiMB4XDTIzMTAw
+        MjA1NDE1N1oXDTI0MTAwMTA1NDE1N1owIzEQMA4GA1UEAwwHbXljaWVudDEPMA0G
+        A1UECgwGa3ViZWRiMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSTc
+        1yj8HW62nynkFbXo4VXKv2jC0PM7dPVky87FweZcTKLoWQVPQE2p2kLDK6OEszmM
+        yyr+xxWtyiveremrWqnKkNTYhLfYPhgQkczib7eUalmFjUbhWdLvHakbEgCodn3b
+        kz57mInX2VpiDOKg4kyHfiuXWpiBqrCx0KNLpxo3DEQcFcsQTeTHzh4752GV04RU
+        Ti/GEWyzIsl4Rg7tGtAwmcIPgUNUfY2Q390FGqdH4ahn+mw/6aFbW31W63d9YJVq
+        ioyOVcaMIpM5B/c7Qc8SuhCI1YGhUyg4cRHLEw5VtikioyE3X04kna3jQAj54YbR
+        bpEhc35apKLB21HOUQIDAQABo1MwUTAdBgNVHQ4EFgQUyvl0VI5vJVSuYFXu7B48
+        6PbMEAowHwYDVR0jBBgwFoAUyvl0VI5vJVSuYFXu7B486PbMEAowDwYDVR0TAQH/
+        BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAMLxrgFVMuNRq2wAwcBt7SnNR5Cfz
+        2MvXq5EUmuawIUi9kaYjwdViDREGSjk7JW17vl576HjDkdfRwi4E28SydRInZf6J
+        i8HZcZ7caH6DxR335fgHVzLi5NiTce/OjNBQzQ2MJXVDd8DBmG5fyatJiOJQ4bWE
+        A7FlP0RdP3CO3GWE0M5iXOB2m1qWkE2eyO4UHvwTqNQLdrdAXgDQlbam9e4BG3Gg
+        d/6thAkWDbt/QNT+EJHDCvhDRKh1RuGHyg+Y+/nebTWWrFWsktRrbOoHCZiCpXI1
+        3eXE6nt0YkgtDxG22KqnhpAg9gUSs2hlhoxyvkzyF0mu6NhPlwAgnq7+/Q==
+        -----END CERTIFICATE-----
+backendTLSPolicies:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: BackendTLSPolicy
+    metadata:
+      name: policy-btls
+      namespace: backends
+    spec:
+      targetRefs:
+        - group: ""
+          kind: Service
+          name: http-backend
+          sectionName: http
+      validation:
+        caCertificateRefs:
+          - name: ca-cmap
+            group: ""
+            kind: ConfigMap
+        hostname: example.com
+        subjectAltNames:
+          - type: URI
+            uri: spiffe://cluster.local/ns/istio-demo/sa/echo-v1
+          - type: DNS
+            hostname: subdomain.secondexample.com
diff --git a/internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.out.yaml b/internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.out.yaml
new file mode 100644
index 000000000..215860c4e
--- /dev/null
+++ b/internal/gatewayapi/testdata/backendtlspolicy-subjectaltnames.out.yaml
@@ -0,0 +1,190 @@
+backendTLSPolicies:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: BackendTLSPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-btls
+    namespace: backends
+  spec:
+    targetRefs:
+    - group: ""
+      kind: Service
+      name: http-backend
+      sectionName: http
+    validation:
+      caCertificateRefs:
+      - group: ""
+        kind: ConfigMap
+        name: ca-cmap
+      hostname: example.com
+      subjectAltNames:
+      - type: URI
+        uri: spiffe://cluster.local/ns/istio-demo/sa/echo-v1
+      - hostname: subdomain.secondexample.com
+        type: DNS
+  status:
+    ancestors:
+    - ancestorRef:
+        name: gateway-btls
+        namespace: envoy-gateway
+        sectionName: http
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-btls
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-btls
+    namespace: envoy-gateway
+  spec:
+    parentRefs:
+    - name: gateway-btls
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: http-backend
+        namespace: backends
+        port: 8080
+      matches:
+      - path:
+          type: Exact
+          value: /exact
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-btls
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-btls:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/gateway-btls/http
+        ports:
+        - containerPort: 10080
+          name: http-80
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-btls
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-btls
+      namespace: envoy-gateway-system
+xdsIR:
+  envoy-gateway/gateway-btls:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-btls
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-btls/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          name: httproute/envoy-gateway/httproute-btls/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 10.244.0.11
+              port: 8080
+            name: httproute/envoy-gateway/httproute-btls/rule/0/backend/0
+            protocol: HTTP
+            tls:
+              alpnProtocols: null
+              caCertificate:
+                certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
+                name: policy-btls/backends-ca
+              sni: example.com
+              subjectAltNames:
+              - uri: spiffe://cluster.local/ns/istio-demo/sa/echo-v1
+              - hostname: subdomain.secondexample.com
+            weight: 1
+        hostname: '*'
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-btls
+          namespace: envoy-gateway
+        name: httproute/envoy-gateway/httproute-btls/rule/0/match/0/*
+        pathMatch:
+          distinct: false
+          exact: /exact
+          name: ""
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-with-tlsbundle.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-with-tlsbundle.yaml
index 5eff08a35..b0a453e79 100644
--- a/internal/xds/translator/testdata/in/xds-ir/http-route-with-tlsbundle.yaml
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-with-tlsbundle.yaml
@@ -26,6 +26,9 @@ http:
                   certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
                   name: policy-btls/policies-ca
                 SNI: example.com
+                subjectAltNames:
+                  - uri: spiffe://cluster.local/ns/istio-demo/sa/echo-v1
+                  - hostname: subdomain.secondexample.com
               weight: 1
         hostname: '*'
         name: httproute/envoy-gateway/httproute-btls/rule/0/match/0/*
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tlsbundle.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tlsbundle.clusters.yaml
index 0ba4829c9..991b3dbba 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tlsbundle.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tlsbundle.clusters.yaml
@@ -29,6 +29,12 @@
               - matcher:
                   exact: example.com
                 sanType: DNS
+              - matcher:
+                  exact: spiffe://cluster.local/ns/istio-demo/sa/echo-v1
+                sanType: URI
+              - matcher:
+                  exact: subdomain.secondexample.com
+                sanType: DNS
             validationContextSdsSecretConfig:
               name: policy-btls/policies-ca
               sdsConfig: