Repository: kubernetes-sigs/gateway-api

Issue:
BackendTLSPolicy conformance test - Consider labelling the tests as provisional
See https://github.com/kubernetes-sigs/gateway-api/pull/3212#discussion_r2206821326

Discussion:
@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@k8s-triage-robot: The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@youngnick: Not required now that BackendTLSPolicy is Standard.

Solution patch:
diff --git a/.custom-gcl.yml b/.custom-gcl.yml
new file mode 100644
index 00000000..84754359
--- /dev/null
+++ b/.custom-gcl.yml
@@ -0,0 +1,6 @@
+version: v2.2.1
+name: golangci-kube-api-linter
+destination: ./bin
+plugins:
+- module: 'sigs.k8s.io/kube-api-linter'
+  version: 'v0.0.0-20250715075424-4fab82d26a8e' # Pin to a commit while there's no tag
diff --git a/.github/workflows/kal.yml b/.github/workflows/kal.yml
new file mode 100644
index 00000000..d657969f
--- /dev/null
+++ b/.github/workflows/kal.yml
@@ -0,0 +1,25 @@
+name: PR golangci-lint
+
+on:
+  pull_request:
+    types: [opened, edited, synchronize, reopened]
+
+# Remove all permissions from GITHUB_TOKEN except metadata.
+permissions: {}
+
+jobs:
+  golangci:
+    name: kube-api-lint
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2
+      - name: Set up Go
+        uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # tag=v5.5.0
+      - name: Install Golang CI Lint
+        run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.2.1
+      - name: Build KAL
+        run: golangci-lint custom
+      - name: run api linter
+        run: ./bin/golangci-kube-api-linter run -c ./.golangci-kal.yml ./...
diff --git a/.golangci-kal.yml b/.golangci-kal.yml
new file mode 100644
index 00000000..aad23ab4
--- /dev/null
+++ b/.golangci-kal.yml
@@ -0,0 +1,36 @@
+version: "2"
+linters:
+  default: none
+  enable:
+    - kubeapilinter
+  settings:
+    custom:
+      kubeapilinter:
+        type: module
+        description: Kube API LInter lints Kube like APIs based on API conventions and best practices.
+        settings:
+          linters:
+            enable:
+              - "duplicatemarkers" # Ensure there are no exact duplicate markers. for types and fields.
+              - "jsontags" # Ensure every field has a json tag.
+              - "nofloats" # Ensure floats are not used.
+              - "nomaps" # Ensure maps are not used.
+              - "nophase" # Phase fields are discouraged by the Kube API conventions, use conditions instead.
+              - "statussubresource" # All root objects that have a `status` field should have a status subresource.
+              - "uniquemarkers" # Ensure that types and fields do not contain more than a single definition of a marker that should only be present once.
+            disable:
+              - "*"
+          lintersConfig: {}
+  exclusions:
+    generated: strict
+    paths:
+      - conformance/
+    paths-except:
+      - apis/
+      - apisx/
+issues:
+  max-issues-per-linter: 0
+  max-same-issues: 0
+run:
+  timeout: 5m
+  tests: false
diff --git a/apis/v1/httproute_types.go b/apis/v1/httproute_types.go
index e59ebf7b..157932e9 100644
--- a/apis/v1/httproute_types.go
+++ b/apis/v1/httproute_types.go
@@ -1590,7 +1590,6 @@ type HTTPBackendRef struct {
 	// +optional
 	// +kubebuilder:validation:MaxItems=16
 	// +kubebuilder:validation:XValidation:message="May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both",rule="!(self.exists(f, f.type == 'RequestRedirect') && self.exists(f, f.type == 'URLRewrite'))"
-	// +kubebuilder:validation:XValidation:message="May specify either httpRouteFilterRequestRedirect or httpRouteFilterRequestRewrite, but not both",rule="!(self.exists(f, f.type == 'RequestRedirect') && self.exists(f, f.type == 'URLRewrite'))"
 	// +kubebuilder:validation:XValidation:message="RequestHeaderModifier filter cannot be repeated",rule="self.filter(f, f.type == 'RequestHeaderModifier').size() <= 1"
 	// +kubebuilder:validation:XValidation:message="ResponseHeaderModifier filter cannot be repeated",rule="self.filter(f, f.type == 'ResponseHeaderModifier').size() <= 1"
 	// +kubebuilder:validation:XValidation:message="RequestRedirect filter cannot be repeated",rule="self.filter(f, f.type == 'RequestRedirect').size() <= 1"
diff --git a/apis/v1alpha3/backendtlspolicy_types.go b/apis/v1alpha3/backendtlspolicy_types.go
index 394fa46c..75655f5d 100644
--- a/apis/v1alpha3/backendtlspolicy_types.go
+++ b/apis/v1alpha3/backendtlspolicy_types.go
@@ -152,8 +152,6 @@ type BackendTLSPolicyValidation struct {
 	//
 	// 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066).
 	// 2. Hostname MUST be used for authentication and MUST match the certificate served by the matching backend, unless SubjectAltNames is specified.
-	//    authentication and MUST match the certificate served by the matching
-	//    backend.
 	//
 	// Support: Core
 	Hostname v1.PreciseHostname `json:"hostname"`
diff --git a/apis/v1alpha3/tlsroute_types.go b/apis/v1alpha3/tlsroute_types.go
index 60e2dc7b..82345f90 100644
--- a/apis/v1alpha3/tlsroute_types.go
+++ b/apis/v1alpha3/tlsroute_types.go
@@ -63,13 +63,13 @@ type TLSRouteSpec struct {
 	// attached to the Listener. For example:
 	//
 	// * A Listener with `test.example.com` as the hostname matches TLSRoutes
-	//   that have either not specified any hostnames, or have specified at
-	//   least one of `test.example.com` or `*.example.com`.
+	//   that have specified at least one of `test.example.com` or
+	//   `*.example.com`.
 	// * A Listener with `*.example.com` as the hostname matches TLSRoutes
-	//   that have either not specified any hostnames or have specified at least
-	//   one hostname that matches the Listener hostname. For example,
-	//   `test.example.com` and `*.example.com` would both match. On the other
-	//   hand, `example.com` and `test.example.net` would not match.
+	//   that have specified at least one hostname that matches the Listener
+	//   hostname. For example, `test.example.com` and `*.example.com` would both
+	//   match. On the other hand, `example.com` and `test.example.net` would not
+	//   match.
 	//
 	// If both the Listener and TLSRoute have specified hostnames, any
 	// TLSRoute hostnames that do not match the Listener hostname MUST be
diff --git a/config/crd/experimental/gateway.networking.k8s.io_backendtlspolicies.yaml b/config/crd/experimental/gateway.networking.k8s.io_backendtlspolicies.yaml
index 6a9e653a..a2a5506c 100644
--- a/config/crd/experimental/gateway.networking.k8s.io_backendtlspolicies.yaml
+++ b/config/crd/experimental/gateway.networking.k8s.io_backendtlspolicies.yaml
@@ -233,8 +233,6 @@ spec:
 
                       1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066).
                       2. Hostname MUST be used for authentication and MUST match the certificate served by the matching backend, unless SubjectAltNames is specified.
-                         authentication and MUST match the certificate served by the matching
-                         backend.
 
                       Support: Core
                     maxLength: 253
diff --git a/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml
index cf2b2746..fe5b7f03 100644
--- a/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml
+++ b/config/crd/experimental/gateway.networking.k8s.io_httproutes.yaml
@@ -1524,10 +1524,6 @@ spec:
                             maxItems: 16
                             type: array
                             x-kubernetes-validations:
-                            - message: May specify either httpRouteFilterRequestRedirect
-                                or httpRouteFilterRequestRewrite, but not both
-                              rule: '!(self.exists(f, f.type == ''RequestRedirect'')
-                                && self.exists(f, f.type == ''URLRewrite''))'
                             - message: May specify either httpRouteFilterRequestRedirect
                                 or httpRouteFilterRequestRewrite, but not both
                               rule: '!(self.exists(f, f.type == ''RequestRedirect'')
@@ -5144,10 +5140,6 @@ spec:
                             maxItems: 16
                             type: array
                             x-kubernetes-validations:
-                            - message: May specify either httpRouteFilterRequestRedirect
-                                or httpRouteFilterRequestRewrite, but not both
-                              rule: '!(self.exists(f, f.type == ''RequestRedirect'')
-                                && self.exists(f, f.type == ''URLRewrite''))'
                             - message: May specify either httpRouteFilterRequestRedirect
                                 or httpRouteFilterRequestRewrite, but not both
                               rule: '!(self.exists(f, f.type == ''RequestRedirect'')
diff --git a/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml
index 923fa49b..8843c2f0 100644
--- a/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml
+++ b/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml
@@ -837,13 +837,13 @@ spec:
                   attached to the Listener. For example:
 
                   * A Listener with `test.example.com` as the hostname matches TLSRoutes
-                    that have either not specified any hostnames, or have specified at
-                    least one of `test.example.com` or `*.example.com`.
+                    that have specified at least one of `test.example.com` or
+                    `*.example.com`.
                   * A Listener with `*.example.com` as the hostname matches TLSRoutes
-                    that have either not specified any hostnames or have specified at least
-                    one hostname that matches the Listener hostname. For example,
-                    `test.example.com` and `*.example.com` would both match. On the other
-                    hand, `example.com` and `test.example.net` would not match.
+                    that have specified at least one hostname that matches the Listener
+                    hostname. For example, `test.example.com` and `*.example.com` would both
+                    match. On the other hand, `example.com` and `test.example.net` would not
+                    match.
 
                   If both the Listener and TLSRoute have specified hostnames, any
                   TLSRoute hostnames that do not match the Listener hostname MUST be
diff --git a/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml b/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
index 8533381f..1824bd5e 100644
--- a/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
+++ b/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml
@@ -1191,10 +1191,6 @@ spec:
                             maxItems: 16
                             type: array
                             x-kubernetes-validations:
-                            - message: May specify either httpRouteFilterRequestRedirect
-                                or httpRouteFilterRequestRewrite, but not both
-                              rule: '!(self.exists(f, f.type == ''RequestRedirect'')
-                                && self.exists(f, f.type == ''URLRewrite''))'
                             - message: May specify either httpRouteFilterRequestRedirect
                                 or httpRouteFilterRequestRewrite, but not both
                               rule: '!(self.exists(f, f.type == ''RequestRedirect'')
@@ -3995,10 +3991,6 @@ spec:
                             maxItems: 16
                             type: array
                             x-kubernetes-validations:
-                            - message: May specify either httpRouteFilterRequestRedirect
-                                or httpRouteFilterRequestRewrite, but not both
-                              rule: '!(self.exists(f, f.type == ''RequestRedirect'')
-                                && self.exists(f, f.type == ''URLRewrite''))'
                             - message: May specify either httpRouteFilterRequestRedirect
                                 or httpRouteFilterRequestRewrite, but not both
                               rule: '!(self.exists(f, f.type == ''RequestRedirect'')
diff --git a/conformance/apis/v1/conformancereport.go b/conformance/apis/v1/conformancereport.go
index dcea92df..7f4f4d53 100644
--- a/conformance/apis/v1/conformancereport.go
+++ b/conformance/apis/v1/conformancereport.go
@@ -50,10 +50,6 @@ type ConformanceReport struct {
 	// SucceededProvisionalTests is a list of the names of the provisional tests that
 	// have been successfully run.
 	SucceededProvisionalTests []string `json:"succeededProvisionalTests,omitempty"`
-
-	// InferredSupportedFeatures indicates whether the supported features were
-	// automatically detected by the conformance suite.
-	InferredSupportedFeatures bool `json:"inferredSupportedFeatures"`
 }
 
 // Implementation provides metadata information on the downstream
diff --git a/conformance/base/manifests.yaml b/conformance/base/manifests.yaml
index a3163bf8..9380223e 100644
--- a/conformance/base/manifests.yaml
+++ b/conformance/base/manifests.yaml
@@ -138,7 +138,7 @@ spec:
     spec:
       containers:
       - name: infra-backend-v1
-        # From https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver
+        # Originally from https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver
         image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
         env:
         - name: POD_NAME
@@ -300,7 +300,7 @@ spec:
       volumes:
       - name: secret-volume
         secret:
-          secretName: tls-passthrough-checks-certificate
+          secretName: tls-checks-certificate
           items:
           - key: tls.crt
             path: crt
diff --git a/conformance/conformance.go b/conformance/conformance.go
index 12e58849..ce7a465c 100644
--- a/conformance/conformance.go
+++ b/conformance/conformance.go
@@ -29,14 +29,11 @@ import (
 	"sigs.k8s.io/gateway-api/conformance/tests"
 	conformanceconfig "sigs.k8s.io/gateway-api/conformance/utils/config"
 	"sigs.k8s.io/gateway-api/conformance/utils/flags"
-
 	"sigs.k8s.io/gateway-api/conformance/utils/suite"
 
 	"github.com/stretchr/testify/require"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-
 	clientset "k8s.io/client-go/kubernetes"
-
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/yaml"
@@ -66,7 +63,6 @@ func DefaultOptions(t *testing.T) suite.ConformanceOptions {
 
 	supportedFeatures := suite.ParseSupportedFeatures(*flags.SupportedFeatures)
 	exemptFeatures := suite.ParseSupportedFeatures(*flags.ExemptFeatures)
-
 	skipTests := suite.ParseSkipTests(*flags.SkipTests)
 	namespaceLabels := suite.ParseKeyValuePairs(*flags.NamespaceLabels)
 	namespaceAnnotations := suite.ParseKeyValuePairs(*flags.NamespaceAnnotations)
diff --git a/conformance/utils/http/http.go b/conformance/utils/http/http.go
index 18b5e1fe..e1aca27d 100644
--- a/conformance/utils/http/http.go
+++ b/conformance/utils/http/http.go
@@ -58,6 +58,10 @@ type ExpectedResponse struct {
 
 	// User Given TestCase name
 	TestCaseName string
+
+	// ServerName indicates the hostname to which the client attempts to connect,
+	// and which is seen by the backend.
+	ServerName string
 }
 
 // Request can be used as both the request to make and a means to verify
@@ -71,6 +75,7 @@ type Request struct {
 	UnfollowRedirect bool
 	Protocol         string
 	Body             string
+	SNI              string
 }
 
 // ExpectedRequest defines expected properties of a request that reaches a backend.
@@ -130,7 +135,7 @@ func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, sch
 	path, query, _ := strings.Cut(expected.Request.Path, "?")
 	reqURL := url.URL{Scheme: scheme, Host: CalculateHost(t, gwAddr, scheme), Path: path, RawQuery: query}
 
-	tlog.Logf(t, "Making %s request to %s", expected.Request.Method, reqURL.String())
+	tlog.Logf(t, "Making %s request to host %s via %s", expected.Request.Method, expected.Request.Host, reqURL.String())
 
 	req := roundtripper.Request{
 		T:                t,
@@ -172,7 +177,10 @@ func CalculateHost(t *testing.T, gwAddr, scheme string) string {
 		host, port, err = net.SplitHostPort(gwAddr)
 	}
 	if err != nil {
-		tlog.Logf(t, "Failed to parse host %q: %v", gwAddr, err)
+		// An address without a port causes an error, but it's fine for some cases.
+		if !strings.Contains(err.Error(), "missing port in address") {
+			tlog.Logf(t, "Failed to parse host %q: %v", gwAddr, err)
+		}
 		return gwAddr
 	}
 	if strings.ToLower(scheme) == "http" && port == "80" {
@@ -243,7 +251,7 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro
 			return false
 		}
 
-		if err := CompareRequest(t, &req, cReq, cRes, expected); err != nil {
+		if err := CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil {
 			tlog.Logf(t, "Response expectation failed for request: %+v  not ready yet: %v (after %v)", req, err, elapsed)
 			return false
 		}
@@ -253,14 +261,14 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro
 	tlog.Logf(t, "Request passed")
 }
 
-func CompareRequest(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected ExpectedResponse) error {
+func CompareRoundTrip(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected ExpectedResponse) error {
 	if roundtripper.IsTimeoutError(cRes.StatusCode) {
 		if roundtripper.IsTimeoutError(expected.Response.StatusCode) {
 			return nil
 		}
 	}
 	if expected.Response.StatusCode != cRes.StatusCode {
-		return fmt.Errorf("expected status code to be %d, got %d", expected.Response.StatusCode, cRes.StatusCode)
+		return fmt.Errorf("expected status code to be %d, got %d. CRes: %v", expected.Response.StatusCode, cRes.StatusCode, cRes)
 	}
 	if cRes.StatusCode == 200 {
 		// The request expected to arrive at the backend is
@@ -353,6 +361,10 @@ func CompareRequest(t *testing.T, req *roundtripper.Request, cReq *roundtripper.
 		if !strings.HasPrefix(cReq.Pod, expected.Backend) {
 			return fmt.Errorf("expected pod name to start with %s, got %s", expected.Backend, cReq.Pod)
 		}
+
+		if expected.ExpectedRequest.SNI != "" && expected.ExpectedRequest.SNI != cReq.TLS.ServerName {
+			return fmt.Errorf("expected SNI %q to be equal to %q", cReq.TLS.ServerName, expected.ExpectedRequest.SNI)
+		}
 	} else if roundtripper.IsRedirect(cRes.StatusCode) {
 		if expected.RedirectRequest == nil {
 			return nil
diff --git a/conformance/utils/kubernetes/certificate.go b/conformance/utils/kubernetes/certificate.go
index a0f70ddd..07c3c807 100644
--- a/conformance/utils/kubernetes/certificate.go
+++ b/conformance/utils/kubernetes/certificate.go
@@ -27,12 +27,14 @@ import (
 	"io"
 	"math/big"
 	"net"
+	"strings"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	kvalidation "k8s.io/apimachinery/pkg/util/validation"
 )
 
 const (
@@ -46,8 +48,25 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string,
 
 	var serverKey, serverCert bytes.Buffer
 
-	require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert), "failed to generate RSA certificate")
+	require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, nil, nil), "failed to generate RSA certificate")
 
+	return formatSecret(serverCert, serverKey, namespace, secretName)
+}
+
+// MustCreateCASignedCertSecret creates a CA-signed SSL certificate and stores it in a secret
+func MustCreateCASignedCertSecret(t *testing.T, namespace, secretName string, hosts []string, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) *corev1.Secret {
+	require.NotEmpty(t, hosts, "require a non-empty hosts for Subject Alternate Name values")
+
+	var serverCert, serverKey bytes.Buffer
+
+	require.NoError(t, generateRSACert(hosts, &serverKey, &serverCert, ca, caPrivKey), "failed to generate CA signed RSA certificate")
+
+	return formatSecret(serverCert, serverKey, namespace, secretName)
+}
+
+// formatSecret formats the server certificate, key, namespace, and secretName
+// and converts it to a Kubernetes Secret object.
+func formatSecret(serverCert bytes.Buffer, serverKey bytes.Buffer, namespace string, secretName string) *corev1.Secret {
 	data := map[string][]byte{
 		corev1.TLSCertKey:       serverCert.Bytes(),
 		corev1.TLSPrivateKeyKey: serverKey.Bytes(),
@@ -65,8 +84,9 @@ func MustCreateSelfSignedCertSecret(t *testing.T, namespace, secretName string,
 	return newSecret
 }
 
-// generateRSACert generates a basic self-signed certificate valid for a year
-func generateRSACert(hosts []string, keyOut, certOut io.Writer) error {
+// generateRSACert generates a basic self-signed certificate if ca and caPrivKey are nil,
+// otherwise it creates CA-signed cert with ca and caPrivkey input. Certs are valid for a year.
+func generateRSACert(hosts []string, keyOut, certOut io.Writer, ca *x509.Certificate, caPrivKey *rsa.PrivateKey) error {
 	priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
 	if err != nil {
 		return fmt.Errorf("failed to generate key: %w", err)
@@ -97,12 +117,20 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer) error {
 	for _, h := range hosts {
 		if ip := net.ParseIP(h); ip != nil {
 			template.IPAddresses = append(template.IPAddresses, ip)
-		} else {
+		} else if err = validateHost(h); err == nil {
 			template.DNSNames = append(template.DNSNames, h)
 		}
 	}
 
-	derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
+	if caPrivKey == nil {
+		caPrivKey = priv
+	}
+	// If ca is nil, we create a self-signed certificate, e.g. template is the parent.
+	if ca == nil {
+		ca = &template
+	}
+
+	derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca, &priv.PublicKey, caPrivKey)
 	if err != nil {
 		return fmt.Errorf("failed to create certificate: %w", err)
 	}
@@ -117,3 +145,117 @@ func generateRSACert(hosts []string, keyOut, certOut io.Writer) error {
 
 	return nil
 }
+
+// MustCreateCACertConfigMap will create a ConfigMap containing a CA Certificate, given a TLS Secret
+// for that CA certificate.  Also returns the CA certificate.
+func MustCreateCACertConfigMap(t *testing.T, namespace, configMapName string, hosts []string) (*corev1.ConfigMap, *x509.Certificate, *rsa.PrivateKey) {
+	require.NotEmpty(t, hosts, "require a non-empty hosts for Subject Alternate Name values")
+
+	var certData, keyData bytes.Buffer
+
+	ca, caBytes, caPrivKey, err := generateCACert(hosts)
+	if err != nil {
+		t.Errorf("failed to generate CA certificate and key: %v", err)
+		return nil, nil, nil
+	}
+
+	if err := pem.Encode(&certData, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil {
+		t.Errorf("failed creating cert: %v", err)
+		return nil, nil, nil
+	}
+
+	if err := pem.Encode(&keyData, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey)}); err != nil {
+		t.Errorf("failed creating key: %v", err)
+		return nil, nil, nil
+	}
+
+	// Store the certificate in a ConfigMap.
+	caConfigMap := &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      configMapName,
+		},
+		Data: map[string]string{
+			"ca.crt": certData.String(),
+			// Don't do this in production, this is just for conformance testing.
+			"key.crt": keyData.String(),
+		},
+	}
+	return caConfigMap, ca, caPrivKey
+}
+
+// generateCACert generates a CA and a CA-signed certificate valid for a year.
+func generateCACert(hosts []string) (*x509.Certificate, []byte, *rsa.PrivateKey, error) {
+	var caBytes []byte
+
+	// Create the CA certificate template.
+	ca := &x509.Certificate{
+		SerialNumber: big.NewInt(2024),
+		Subject: pkix.Name{
+			Organization: []string{"Kubernetes Gateway API"},
+			Country:      []string{"US"},
+			CommonName:   "gatewayapi",
+		},
+		Issuer: pkix.Name{
+			Organization: []string{"Kubernetes Gateway API"},
+			Country:      []string{"US"},
+			CommonName:   "kubernetes",
+		},
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().AddDate(1, 0, 0),
+		IsCA:                  true, // Indicates this is a CA Certificate.
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+
+	// Ensure only valid hosts make it into the CA cert.
+	for _, h := range hosts {
+		if ip := net.ParseIP(h); ip != nil {
+			ca.IPAddresses = append(ca.IPAddresses, ip)
+		} else if err := validateHost(h); err == nil {
+			ca.DNSNames = append(ca.DNSNames, h)
+		}
+	}
+
+	// Generate the private key to sign certificates.
+	caPrivKey, err := rsa.GenerateKey(rand.Reader, rsaBits)
+	if err != nil {
+		return nil, caBytes, caPrivKey, fmt.Errorf("error generating key for CA: %v", err)
+	}
+
+	// Create the self-signed certificate using the CA certificate.
+	caBytes, err = x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
+	if err != nil {
+		return nil, caBytes, caPrivKey, fmt.Errorf("error creating CA: %v", err)
+	}
+
+	return ca, caBytes, caPrivKey, nil
+}
+
+// validateHost ensures that the host name length is no more than 253 characters.
+// The only characters allowed in host name are alphanumeric characters, '-' or '.',
+// and it must start and end with an alphanumeric character. A trailing dot is NOT allowed.
+// The host name must in addition consist of one or more labels, with each label no more
+// than 63 characters from the character set described above, and each label must start and
+// end with an alphanumeric character.  Wildcards are handled specially.
+// DO NOT USE for general validation purposes, this is just for the hostnames set up for
+// conformance testing.
+func validateHost(host string) error {
+	// Remove wildcard if present.
+	host, _ = strings.CutPrefix(host, "*.")
+
+	errs := kvalidation.IsDNS1123Subdomain(host)
+	if len(errs) != 0 {
+		return fmt.Errorf("host %s must conform to DNS naming conventions: %v", host, errs)
+	}
+
+	labels := strings.Split(host, ".")
+	for _, l := range labels {
+		errs := kvalidation.IsDNS1123Label(l)
+		if len(errs) != 0 {
+			return fmt.Errorf("label %s in host %s must conform to DNS naming conventions: %v", l, host, errs)
+		}
+	}
+	return nil
+}
diff --git a/conformance/utils/kubernetes/helpers.go b/conformance/utils/kubernetes/helpers.go
index 2b43828c..9e385b4c 100644
--- a/conformance/utils/kubernetes/helpers.go
+++ b/conformance/utils/kubernetes/helpers.go
@@ -331,14 +331,22 @@ func MeshNamespacesMustBeReady(t *testing.T, c client.Client, timeoutConfig conf
 //     - ListenerConditionProgrammed
 //
 // The test will fail if these conditions are not met before the timeouts.
-func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeType any, routeNNs ...types.NamespacedName) string {
+// Note that this also returns a Gateway address to use, but it takes the port
+// from the first listener it finds.  Set parameter `usePort` to false if there
+// are multiple listeners, and true if there is only one listener.
+func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeType any, usePort bool, routeNNs ...types.NamespacedName) string {
 	t.Helper()
 
 	RouteTypeMustHaveParentsField(t, routeType)
 	gwAddr, err := WaitForGatewayAddress(t, c, timeoutConfig, gw)
-
 	require.NoErrorf(t, err, "timed out waiting for Gateway address to be assigned")
 
+	// If the Gateway has multiple listeners, get a portless gwAddr.
+	// Otherwise, you get the first listener's port, which might not be the one you want.
+	if !usePort {
+		gwAddr, _, _ = strings.Cut(gwAddr, ":")
+	}
+
 	ns := gatewayv1.Namespace(gw.Namespace)
 	kind := gatewayv1.Kind("Gateway")
 
@@ -401,7 +409,7 @@ func GatewayAndRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig
 //
 // The test will fail if these conditions are not met before the timeouts.
 func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string {
-	return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &gatewayv1.HTTPRoute{}, routeNNs...)
+	return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &gatewayv1.HTTPRoute{}, true, routeNNs...)
 }
 
 // GatewayAndUDPRoutesMustBeAccepted waits until the specified Gateway has an IP
@@ -409,7 +417,7 @@ func GatewayAndHTTPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutCo
 // Gateway. The test will fail if these conditions are not met before the
 // timeouts.
 func GatewayAndUDPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string {
-	return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &v1alpha2.UDPRoute{}, routeNNs...)
+	return GatewayAndRoutesMustBeAccepted(t, c, timeoutConfig, controllerName, gw, &v1alpha2.UDPRoute{}, true, routeNNs...)
 }
 
 // WaitForGatewayAddress waits until at least one IP Address has been set in the
@@ -419,16 +427,10 @@ func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig con
 
 	var ipAddr, port string
 	waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.GatewayMustHaveAddress, true, func(ctx context.Context) (bool, error) {
-		gw := &gatewayv1.Gateway{}
-		err := client.Get(ctx, gwRef.NamespacedName, gw)
-		if err != nil {
-			tlog.Logf(t, "error fetching Gateway: %v", err)
-			return false, fmt.Errorf("error fetching Gateway: %w", err)
-		}
-
-		if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil {
-			tlog.Log(t, "Gateway", err)
-			return false, nil
+		gw, err := getGatewayStatus(ctx, t, client, gwRef)
+		if gw == nil {
+			// The returned error is nil if the Gateway conditions don't have the latest observed generation.
+			return false, err
 		}
 
 		listener := gw.Spec.Listeners[0]
@@ -442,20 +444,34 @@ func WaitForGatewayAddress(t *testing.T, client client.Client, timeoutConfig con
 			}
 		}
 		port = strconv.FormatInt(int64(listener.Port), 10)
-
 		for _, address := range gw.Status.Addresses {
 			if address.Type != nil && (*address.Type == gatewayv1.IPAddressType || *address.Type == v1alpha2.HostnameAddressType) {
 				ipAddr = address.Value
 				return true, nil
 			}
 		}
-
 		return false, nil
 	})
 	require.NoErrorf(t, waitErr, "error waiting for Gateway to have at least one IP address in status")
 	return net.JoinHostPort(ipAddr, port), waitErr
 }
 
+func getGatewayStatus(ctx context.Context, t *testing.T, client client.Client, gwRef GatewayRef) (*gatewayv1.Gateway, error) {
+	gw := &gatewayv1.Gateway{}
+	err := client.Get(ctx, gwRef.NamespacedName, gw)
+	if err != nil {
+		tlog.Logf(t, "error fetching Gateway: %v", err)
+		return nil, fmt.Errorf("error fetching Gateway: %w", err)
+	}
+
+	if err := ConditionsHaveLatestObservedGeneration(gw, gw.Status.Conditions); err != nil {
+		tlog.Log(t, "Gateway", err)
+		return nil, nil
+	}
+
+	return gw, nil
+}
+
 // GatewayListenersMustHaveConditions checks if every listener of the specified gateway has all
 // the specified conditions.
 func GatewayListenersMustHaveConditions(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, gwName types.NamespacedName, conditions []metav1.Condition) {
diff --git a/conformance/utils/roundtripper/roundtripper.go b/conformance/utils/roundtripper/roundtripper.go
index f7950878..9490b648 100644
--- a/conformance/utils/roundtripper/roundtripper.go
+++ b/conformance/utils/roundtripper/roundtripper.go
@@ -88,6 +88,14 @@ type CapturedRequest struct {
 
 	Namespace string `json:"namespace"`
 	Pod       string `json:"pod"`
+	TLS       TLS    `json:"tls"`
+}
+
+type TLS struct {
+	Version            string `json:"version"`
+	ServerName         string `json:"serverName"`
+	NegotiatedProtocol string `json:"negotiatedProtocol"`
+	CipherSuite        string `json:"cipherSuite"`
 }
 
 // RedirectRequest contains a follow up request metadata captured from a redirect
@@ -228,6 +236,18 @@ func (d *DefaultRoundTripper) defaultRoundTrip(request Request, transport http.R
 
 	resp, err := client.Do(req)
 	if err != nil {
+		if d.Debug {
+			var dump []byte
+			if resp != nil {
+				dump, err = httputil.DumpResponse(resp, true)
+				if err != nil {
+					return nil, nil, err
+				}
+				tlog.Logf(request.T, "Error sending request:\n%s\n\n", formatDump(dump, "< "))
+			} else {
+				tlog.Logf(request.T, "Error sending request: %v (no response)\n", err)
+			}
+		}
 		return nil, nil, err
 	}
 	defer resp.Body.Close()
diff --git a/conformance/utils/suite/conformance.go b/conformance/utils/suite/conformance.go
index 12db14d6..436b3ed2 100644
--- a/conformance/utils/suite/conformance.go
+++ b/conformance/utils/suite/conformance.go
@@ -68,7 +68,7 @@ func (test *ConformanceTest) Run(t *testing.T, suite *ConformanceTestSuite) {
 
 	for _, manifestLocation := range test.Manifests {
 		tlog.Logf(t, "Applying %s", manifestLocation)
-		suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, true)
+		suite.Applier.MustApplyWithCleanup(t, suite.Client, suite.TimeoutConfig, manifestLocation, suite.Cleanup)
 	}
 
 	if featuresInfo != "" {
diff --git a/conformance/utils/suite/suite.go b/conformance/utils/suite/suite.go
index f1164fa2..63a6b93a 100644
--- a/conformance/utils/suite/suite.go
+++ b/conformance/utils/suite/suite.go
@@ -84,7 +84,7 @@ type ConformanceTestSuite struct {
 	// If SupportedFeatures are automatically determined from GWC Status.
 	// This will be required to report in future iterations as the passing
 	// will be determined based on this.
-	isInferredSupportedFeatures bool
+	supportedFeaturesSource supportedFeaturesSource
 
 	// mode is the operating mode of the implementation.
 	// The default value for it is "default".
@@ -188,24 +188,37 @@ const (
 	undefinedKeyword = "UNDEFINED"
 )
 
+// SupportedFeaturesSource represents the source from which supported features are derived.
+// It is used to distinguish between them being inferred from GWC Status or manually
+// supplied for the conformance report.
+type supportedFeaturesSource string
+
+const (
+	supportedFeaturesSourceManual   supportedFeaturesSource = "Manual"
+	supportedFeaturesSourceInferred supportedFeaturesSource = "Inferred"
+)
+
 // NewConformanceTestSuite is a helper to use for creating a new ConformanceTestSuite.
 func NewConformanceTestSuite(options ConformanceOptions) (*ConformanceTestSuite, error) {
 	supportedFeatures := options.SupportedFeatures.Difference(options.ExemptFeatures)
-	isInferred := false
-	switch {
-	case options.EnableAllSupportedFeatures:
+	source := supportedFeaturesSourceManual
+	if options.EnableAllSupportedFeatures {
 		supportedFeatures = features.SetsToNamesSet(features.AllFeatures)
-	case shouldInferSupportedFeatures(&options):
+	} else if shouldInferSupportedFeatures(&options) {
 		var err error
 		supportedFeatures, err = fetchSupportedFeatures(options.Client, options.GatewayClassName)
 		if err != nil {
-			return nil, fmt.Errorf("Cannot infer supported features: %w", err)
+			return nil, fmt.Errorf("cannot infer supported features: %w", err)
+		}
+
+		if hasMeshFeatures(supportedFeatures) {
+			return nil, fmt.Errorf("mesh features should not be populated in GatewayClass")
 		}
-		isInferred = true
+		source = supportedFeaturesSourceInferred
 	}
 
 	// If features were not inferred from Status, it's a GWC issue.
-	if isInferred && supportedFeatures.Len() == 0 {
+	if source == supportedFeaturesSourceInferred && supportedFeatures.Len() == 0 {
 		return nil, fmt.Errorf("no supported features were determined for test suite")
 	}
 
@@ -273,7 +286,7 @@ func NewConformanceTestSuite(options ConformanceOptions) (*ConformanceTestSuite,
 		mode:                        mode,
 		apiVersion:                  apiVersion,
 		apiChannel:                  apiChannel,
-		isInferredSupportedFeatures: isInferred,
+		supportedFeaturesSource:     source,
 		Hook:                        options.Hook,
 	}
 
@@ -370,6 +383,10 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest)
 		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
 		secret = kubernetes.MustCreateSelfSignedCertSecret(t, "gateway-conformance-app-backend", "tls-passthrough-checks-certificate", []string{"abc.example.com"})
 		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
+		caConfigMap, ca, caPrivKey := kubernetes.MustCreateCACertConfigMap(t, "gateway-conformance-infra", "backend-tls-checks-certificate", []string{"abc.example.com"})
+		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{caConfigMap}, suite.Cleanup)
+		secret = kubernetes.MustCreateCASignedCertSecret(t, "gateway-conformance-infra", "tls-checks-certificate", []string{"abc.example.com"}, ca, caPrivKey)
+		suite.Applier.MustApplyObjectsWithCleanup(t, suite.Client, suite.TimeoutConfig, []client.Object{secret}, suite.Cleanup)
 
 		tlog.Logf(t, "Test Setup: Ensuring Gateways and Pods from base manifests are ready")
 		namespaces := []string{
@@ -394,10 +411,6 @@ func (suite *ConformanceTestSuite) Setup(t *testing.T, tests []ConformanceTest)
 	}
 }
 
-func (suite *ConformanceTestSuite) IsInferredSupportedFeatures() bool {
-	return suite.isInferredSupportedFeatures
-}
-
 func (suite *ConformanceTestSuite) setClientsetForTest(test ConformanceTest) error {
 	featureNames := []string{}
 	for _, v := range test.Features {
@@ -552,7 +565,6 @@ func (suite *ConformanceTestSuite) Report() (*confv1.ConformanceReport, error) {
 		GatewayAPIChannel:         suite.apiChannel,
 		ProfileReports:            profileReports.list(),
 		SucceededProvisionalTests: succeededProvisionalTests,
-		InferredSupportedFeatures: suite.IsInferredSupportedFeatures(),
 	}, nil
 }
 
@@ -610,8 +622,6 @@ func shouldInferSupportedFeatures(opts *ConformanceOptions) bool {
 	return !opts.EnableAllSupportedFeatures &&
 		opts.SupportedFeatures.Len() == 0 &&
 		opts.ExemptFeatures.Len() == 0 &&
-		opts.ConformanceProfiles.Len() == 0 &&
-		len(opts.SkipTests) == 0 &&
 		opts.RunTest == ""
 }
 
@@ -645,3 +655,7 @@ func getAPIVersionAndChannel(crds []apiextensionsv1.CustomResourceDefinition) (v
 
 	return version, channel, nil
 }
+
+func hasMeshFeatures(f FeaturesSet) bool {
+	return f.HasAny(features.SetsToNamesSet(features.MeshCoreFeatures, features.MeshExtendedFeatures).UnsortedList()...)
+}
diff --git a/conformance/utils/tls/tls.go b/conformance/utils/tls/tls.go
index e27fab83..809a4938 100644
--- a/conformance/utils/tls/tls.go
+++ b/conformance/utils/tls/tls.go
@@ -54,8 +54,9 @@ func WaitForConsistentTLSResponse(t *testing.T, r roundtripper.RoundTripper, req
 			return false
 		}
 
-		if err := http.CompareRequest(t, &req, cReq, cRes, expected); err != nil {
+		if err := http.CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil {
 			tlog.Logf(t, "Response expectation failed for request: %+v  not ready yet: %v (after %v)", req, err, elapsed)
+			tlog.Logf(t, "Full response: %+v", cReq)
 			return false
 		}
 
diff --git a/docker/Dockerfile.echo-basic b/docker/Dockerfile.echo-basic
index ed6081fb..55d9b470 100644
--- a/docker/Dockerfile.echo-basic
+++ b/docker/Dockerfile.echo-basic
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 # Build
-FROM golang:1.22.2 as builder
+FROM golang:1.22.2 AS builder
 
 ENV CGO_ENABLED=0
 
@@ -23,9 +23,10 @@ COPY ./conformance/echo-basic ./
 
 # If left as go.mod and go.sum in the external repo, these files would
 # interfere with the ability to use reuse the protobuf/gRPC generated code
-# for the test client in the conformance tests.
-RUN mv .go.mod go.mod
-RUN mv .go.sum go.sum
+# for the test client in the conformance tests.  Add -f in case previous run
+# is aborted and not cleaned up.
+RUN mv -f .go.mod go.mod
+RUN mv -f .go.sum go.sum
 
 RUN go build -trimpath -ldflags="-buildid= -s -w" -o echo-basic .
 
diff --git a/geps/gep-3792/index.md b/geps/gep-3792/index.md
index b0c5d80b..802d729c 100644
--- a/geps/gep-3792/index.md
+++ b/geps/gep-3792/index.md
@@ -19,8 +19,8 @@ cluster, for various reasons which are out of the scope of this GEP. Chihiro
 and Ian want to be able to use these out-of-cluster proxies effectively and
 safely, though they recognize that this may require additional configuration.
 
-[Chihiro]: https://https//gateway-api.sigs.k8s.io/concepts/roles-and-personas/#chihiro
-[Ian]: https://https//gateway-api.sigs.k8s.io/concepts/roles-and-personas/#ian
+[Chihiro]: https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#chihiro
+[Ian]: https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#ian
 
 ### Nomenclature and Background
 
diff --git a/geps/gep-3793/index.md b/geps/gep-3793/index.md
index 0c8ba3e6..9f15ead8 100644
--- a/geps/gep-3793/index.md
+++ b/geps/gep-3793/index.md
@@ -19,7 +19,7 @@ really doesn't care what the Gateway is called.
 Therefore, Ana would like a way to be able to rely on a default Gateway that
 she doesn't have to explicitly name, and can simply trust to exist.
 
-[Ana]: https://https//gateway-api.sigs.k8s.io/concepts/roles-and-personas/#ana
+[Ana]: https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#ana
 
 ## Goals
 
@@ -156,8 +156,8 @@ Ana has easy access to this information, and that it's clear enough for her to
 understand, is clearly important for many more reasons than just default
 Gateways.
 
-[Chihiro]: https://https//gateway-api.sigs.k8s.io/concepts/roles-and-personas/#chihiro
-[Ian]: https://https//gateway-api.sigs.k8s.io/concepts/roles-and-personas/#ian
+[Chihiro]: https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#chihiro
+[Ian]: https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#ian
 
 ## API
 
diff --git a/go.mod b/go.mod
index a2eca0cf..82f05190 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/miekg/dns v1.1.66
 	github.com/stretchr/testify v1.10.0
 	golang.org/x/net v0.41.0
-	golang.org/x/sync v0.15.0
+	golang.org/x/sync v0.16.0
 	google.golang.org/grpc v1.73.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1
 	google.golang.org/protobuf v1.36.6
diff --git a/go.sum b/go.sum
index e08b99f8..85b14c3a 100644
--- a/go.sum
+++ b/go.sum
@@ -180,8 +180,8 @@ golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
-golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/pkg/features/backendtlspolicy.go b/pkg/features/backendtlspolicy.go
new file mode 100644
index 00000000..8763b32d
--- /dev/null
+++ b/pkg/features/backendtlspolicy.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package features
+
+import "k8s.io/apimachinery/pkg/util/sets"
+
+// -----------------------------------------------------------------------------
+// Features - BackendTLSPolicy Conformance (Core)
+// -----------------------------------------------------------------------------
+
+const (
+	// This option indicates support for BackendTLSPolicy.
+	SupportBackendTLSPolicy FeatureName = "BackendTLSPolicy"
+)
+
+// TLSRouteFeature contains metadata for the TLSRoute feature.
+var BackendTLSPolicyFeature = Feature{
+	Name:    SupportBackendTLSPolicy,
+	Channel: FeatureChannelExperimental,
+}
+
+// BackendTLSPolicyCoreFeatures includes all the supported features for the
+// BackendTLSPolicy API at a Core level of support.
+var BackendTLSPolicyCoreFeatures = sets.New(
+	BackendTLSPolicyFeature,
+)
diff --git a/pkg/features/features.go b/pkg/features/features.go
index 8406c4b8..2a67d006 100644
--- a/pkg/features/features.go
+++ b/pkg/features/features.go
@@ -60,7 +60,8 @@ var (
 			Insert(TLSRouteCoreFeatures.UnsortedList()...).
 			Insert(MeshCoreFeatures.UnsortedList()...).
 			Insert(MeshExtendedFeatures.UnsortedList()...).
-			Insert(GRPCRouteCoreFeatures.UnsortedList()...)
+			Insert(GRPCRouteCoreFeatures.UnsortedList()...).
+			Insert(BackendTLSPolicyCoreFeatures.UnsortedList()...)
 
 	featureMap = map[FeatureName]Feature{}
 )
diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go
index 7dcbaa33..cc919c61 100644
--- a/pkg/generated/openapi/zz_generated.openapi.go
+++ b/pkg/generated/openapi/zz_generated.openapi.go
@@ -7106,7 +7106,7 @@ func schema_sigsk8sio_gateway_api_apis_v1alpha3_BackendTLSPolicyValidation(ref c
 					},
 					"hostname": {
 						SchemaProps: spec.SchemaProps{
-							Description: "Hostname is used for two purposes in the connection between Gateways and backends:\n\n1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). 2. Hostname MUST be used for authentication and MUST match the certificate served by the matching backend, unless SubjectAltNames is specified.\n   authentication and MUST match the certificate served by the matching\n   backend.\n\nSupport: Core",
+							Description: "Hostname is used for two purposes in the connection between Gateways and backends:\n\n1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). 2. Hostname MUST be used for authentication and MUST match the certificate served by the matching backend, unless SubjectAltNames is specified.\n\nSupport: Core",
 							Default:     "",
 							Type:        []string{"string"},
 							Format:      "",
@@ -7293,7 +7293,7 @@ func schema_sigsk8sio_gateway_api_apis_v1alpha3_TLSRouteSpec(ref common.Referenc
 					},
 					"hostnames": {
 						SchemaProps: spec.SchemaProps{
-							Description: "Hostnames defines a set of SNI hostnames that should match against the SNI attribute of TLS ClientHello message in TLS handshake. This matches the RFC 1123 definition of a hostname with 2 notable exceptions:\n\n1. IPs are not allowed in SNI hostnames per RFC 6066. 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard\n   label must appear by itself as the first label.\n\nIf a hostname is specified by both the Listener and TLSRoute, there must be at least one intersecting hostname for the TLSRoute to be attached to the Listener. For example:\n\n* A Listener with `test.example.com` as the hostname matches TLSRoutes\n  that have either not specified any hostnames, or have specified at\n  least one of `test.example.com` or `*.example.com`.\n* A Listener with `*.example.com` as the hostname matches TLSRoutes\n  that have either not specified any hostnames or have specified at least\n  one hostname that matches the Listener hostname. For example,\n  `test.example.com` and `*.example.com` would both match. On the other\n  hand, `example.com` and `test.example.net` would not match.\n\nIf both the Listener and TLSRoute have specified hostnames, any TLSRoute hostnames that do not match the Listener hostname MUST be ignored. For example, if a Listener specified `*.example.com`, and the TLSRoute specified `test.example.com` and `test.example.net`, `test.example.net` must not be considered for a match.\n\nIf both the Listener and TLSRoute have specified hostnames, and none match with the criteria above, then the TLSRoute is not accepted. The implementation must raise an 'Accepted' Condition with a status of `False` in the corresponding RouteParentStatus.\n\nSupport: Core",
+							Description: "Hostnames defines a set of SNI hostnames that should match against the SNI attribute of TLS ClientHello message in TLS handshake. This matches the RFC 1123 definition of a hostname with 2 notable exceptions:\n\n1. IPs are not allowed in SNI hostnames per RFC 6066. 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard\n   label must appear by itself as the first label.\n\nIf a hostname is specified by both the Listener and TLSRoute, there must be at least one intersecting hostname for the TLSRoute to be attached to the Listener. For example:\n\n* A Listener with `test.example.com` as the hostname matches TLSRoutes\n  that have specified at least one of `test.example.com` or\n  `*.example.com`.\n* A Listener with `*.example.com` as the hostname matches TLSRoutes\n  that have specified at least one hostname that matches the Listener\n  hostname. For example, `test.example.com` and `*.example.com` would both\n  match. On the other hand, `example.com` and `test.example.net` would not\n  match.\n\nIf both the Listener and TLSRoute have specified hostnames, any TLSRoute hostnames that do not match the Listener hostname MUST be ignored. For example, if a Listener specified `*.example.com`, and the TLSRoute specified `test.example.com` and `test.example.net`, `test.example.net` must not be considered for a match.\n\nIf both the Listener and TLSRoute have specified hostnames, and none match with the criteria above, then the TLSRoute is not accepted. The implementation must raise an 'Accepted' Condition with a status of `False` in the corresponding RouteParentStatus.\n\nSupport: Core",
 							Type:        []string{"array"},
 							Items: &spec.SchemaOrArray{
 								Schema: &spec.Schema{



Test patch:
diff --git a/conformance/tests/backendtlspolicy.go b/conformance/tests/backendtlspolicy.go
new file mode 100644
index 00000000..1d556278
--- /dev/null
+++ b/conformance/tests/backendtlspolicy.go
@@ -0,0 +1,90 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tests
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/types"
+
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+	h "sigs.k8s.io/gateway-api/conformance/utils/http"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+	"sigs.k8s.io/gateway-api/conformance/utils/tls"
+	"sigs.k8s.io/gateway-api/pkg/features"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, BackendTLSPolicy)
+}
+
+var BackendTLSPolicy = suite.ConformanceTest{
+	ShortName:   "BackendTLSPolicy",
+	Description: "A single service that is targeted by a BackendTLSPolicy must successfully complete TLS termination",
+	Features: []features.FeatureName{
+		features.SupportGateway,
+		features.SupportHTTPRoute,
+		features.SupportBackendTLSPolicy,
+	},
+	Manifests: []string{"tests/backendtlspolicy.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		ns := "gateway-conformance-infra"
+		routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns}
+		gwNN := types.NamespacedName{Name: "gateway-backendtlspolicy", Namespace: ns}
+
+		kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})
+		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &gatewayv1.HTTPRoute{}, false, routeNN)
+		kubernetes.HTTPRouteMustHaveResolvedRefsConditionsTrue(t, suite.Client, suite.TimeoutConfig, routeNN, gwNN)
+
+		serverStr := "abc.example.com"
+
+		// Verify that the response to a backend-tls-only call to /backendTLS will return the matching SNI.
+		t.Run("Simple HTTP request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) {
+			h.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr,
+				h.ExpectedResponse{
+					Namespace: ns,
+					Request: h.Request{
+						Host: serverStr,
+						Path: "/backendTLS",
+						SNI:  serverStr,
+					},
+					Response: h.Response{StatusCode: 200},
+				})
+		})
+
+		// For the re-encrypt case, we need to use the cert for the frontend tls listener.
+		certNN := types.NamespacedName{Name: "tls-checks-certificate", Namespace: ns}
+		cPem, keyPem, err := GetTLSSecret(suite.Client, certNN)
+		if err != nil {
+			t.Fatalf("unexpected error finding TLS secret: %v", err)
+		}
+		// Verify that the response to a re-encrypted call to /backendTLS will return the matching SNI.
+		t.Run("Re-encrypt HTTPS request targeting BackendTLSPolicy should reach infra-backend", func(t *testing.T) {
+			tls.MakeTLSRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, cPem, keyPem, serverStr,
+				h.ExpectedResponse{
+					Namespace: ns,
+					Request: h.Request{
+						Host: serverStr,
+						Path: "/backendTLS",
+						SNI:  serverStr,
+					},
+					Response: h.Response{StatusCode: 200},
+				})
+		})
+	},
+}
diff --git a/conformance/tests/backendtlspolicy.yaml b/conformance/tests/backendtlspolicy.yaml
new file mode 100644
index 00000000..91f6a590
--- /dev/null
+++ b/conformance/tests/backendtlspolicy.yaml
@@ -0,0 +1,153 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: gateway-backendtlspolicy
+  namespace: gateway-conformance-infra
+spec:
+  gatewayClassName: "{GATEWAY_CLASS_NAME}"
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+    hostname: "abc.example.com"
+    allowedRoutes:
+      namespaces:
+        from: Same
+      kinds:
+      - kind: HTTPRoute
+  - name: https
+    port: 443
+    protocol: HTTPS
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - group: ""
+        kind: Secret
+        name: tls-checks-certificate
+    hostname: "abc.example.com"
+    allowedRoutes:
+      namespaces:
+        from: Same
+      kinds:
+      - kind: HTTPRoute
+---
+apiVersion: gateway.networking.k8s.io/v1alpha3
+kind: BackendTLSPolicy
+metadata:
+  name: normative-test-backendtlspolicy
+  namespace: gateway-conformance-infra
+spec:
+  targetRefs:
+  - group: ""
+    kind: Service
+    name: "backendtlspolicy-test"
+    sectionName: "btls"
+  validation:
+    caCertificateRefs:
+    - group: ""
+      kind: ConfigMap
+      # This secret is generated dynamically by the test suite.
+      name: "backend-tls-checks-certificate"
+    hostname: "abc.example.com"
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: gateway-conformance-infra-test
+  namespace: gateway-conformance-infra
+spec:
+  parentRefs:
+  - name: gateway-backendtlspolicy
+    namespace: gateway-conformance-infra
+  hostnames:
+  - abc.example.com
+  rules:
+  - backendRefs:
+    - group: ""
+      kind: Service
+      name: backendtlspolicy-test
+      port: 443
+    matches:
+    - path:
+        type: Exact
+        value: /backendTLS
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: backendtlspolicy-test
+  namespace: gateway-conformance-infra
+spec:
+  selector:
+    app: backendtlspolicy-test
+  ports:
+  - name: "btls"
+    protocol: TCP
+    port: 443
+    targetPort: 8443
+---
+# Deployment must not be applied until after the secret is generated.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backendtlspolicy-test
+  namespace: gateway-conformance-infra
+  labels:
+    app: backendtlspolicy-test
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backendtlspolicy-test
+  template:
+    metadata:
+      labels:
+        app: backendtlspolicy-test
+    spec:
+      containers:
+      - name: backendtlspolicy-test
+        image: gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
+        volumeMounts:
+        - name: ca-volume
+          mountPath: /etc/ca-volume
+        - name: secret-volume
+          mountPath: /etc/secret-volume
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: CA_CERT
+          value: /etc/ca-volume/crt
+        - name: CA_CERT_KEY
+          value: /etc/ca-volume/key
+        - name: TLS_SERVER_CERT
+          value: /etc/secret-volume/crt
+        - name: TLS_SERVER_PRIVKEY
+          value: /etc/secret-volume/key
+        resources:
+          requests:
+            cpu: 10m
+      volumes:
+      - name: ca-volume
+        configMap:
+          # This configMap is generated dynamically by the test suite.
+          name: backend-tls-checks-certificate
+          items:
+          - key: ca.crt
+            path: crt
+          - key: key.crt
+            path: key
+      - name: secret-volume
+        secret:
+          # This secret is generated dynamically by the test suite.
+          secretName: tls-checks-certificate
+          items:
+          - key: tls.crt
+            path: crt
+          - key: tls.key
+            path: key
diff --git a/conformance/tests/grpcroute-exact-method-matching.go b/conformance/tests/grpcroute-exact-method-matching.go
index 384cf3ae..c70c034b 100644
--- a/conformance/tests/grpcroute-exact-method-matching.go
+++ b/conformance/tests/grpcroute-exact-method-matching.go
@@ -46,7 +46,7 @@ var GRPCExactMethodMatching = suite.ConformanceTest{
 		ns := "gateway-conformance-infra"
 		routeNN := types.NamespacedName{Name: "exact-matching", Namespace: ns}
 		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
-		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, routeNN)
+		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN)
 
 		testCases := []grpc.ExpectedResponse{
 			{
diff --git a/conformance/tests/grpcroute-header-matching.go b/conformance/tests/grpcroute-header-matching.go
index 48b6e0da..2db1819d 100644
--- a/conformance/tests/grpcroute-header-matching.go
+++ b/conformance/tests/grpcroute-header-matching.go
@@ -46,7 +46,7 @@ var GRPCRouteHeaderMatching = suite.ConformanceTest{
 		ns := "gateway-conformance-infra"
 		routeNN := types.NamespacedName{Name: "grpc-header-matching", Namespace: ns}
 		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
-		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, routeNN)
+		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN)
 
 		testCases := []grpc.ExpectedResponse{{
 			EchoRequest: &pb.EchoRequest{},
diff --git a/conformance/tests/grpcroute-listener-hostname-matching.go b/conformance/tests/grpcroute-listener-hostname-matching.go
index 0e0492e7..ba8ee642 100644
--- a/conformance/tests/grpcroute-listener-hostname-matching.go
+++ b/conformance/tests/grpcroute-listener-hostname-matching.go
@@ -52,15 +52,15 @@ var GRPCRouteListenerHostnameMatching = suite.ConformanceTest{
 		gwNN := types.NamespacedName{Name: "grpcroute-listener-hostname-matching", Namespace: ns}
 
 		_ = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName,
-			kubernetes.NewGatewayRef(gwNN, "listener-1"), &v1.GRPCRoute{},
+			kubernetes.NewGatewayRef(gwNN, "listener-1"), &v1.GRPCRoute{}, true,
 			types.NamespacedName{Namespace: ns, Name: "backend-v1"},
 		)
 		_ = kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName,
-			kubernetes.NewGatewayRef(gwNN, "listener-2"), &v1.GRPCRoute{},
+			kubernetes.NewGatewayRef(gwNN, "listener-2"), &v1.GRPCRoute{}, true,
 			types.NamespacedName{Namespace: ns, Name: "backend-v2"},
 		)
 		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName,
-			kubernetes.NewGatewayRef(gwNN, "listener-3", "listener-4"), &v1.GRPCRoute{},
+			kubernetes.NewGatewayRef(gwNN, "listener-3", "listener-4"), &v1.GRPCRoute{}, true,
 			types.NamespacedName{Namespace: ns, Name: "backend-v3"},
 		)
 
diff --git a/conformance/tests/grpcroute-named-rule.go b/conformance/tests/grpcroute-named-rule.go
index 223e22b5..2c738a56 100644
--- a/conformance/tests/grpcroute-named-rule.go
+++ b/conformance/tests/grpcroute-named-rule.go
@@ -47,7 +47,7 @@ var GRPCRouteNamedRule = suite.ConformanceTest{
 		ns := "gateway-conformance-infra"
 		routeNN := types.NamespacedName{Name: "grpc-named-rules", Namespace: ns}
 		gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
-		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, routeNN)
+		gwAddr := kubernetes.GatewayAndRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), &v1.GRPCRoute{}, true, routeNN)
 
 		testCases := []grpc.ExpectedResponse{
 			{
diff --git a/conformance/tests/httproute-weight.go b/conformance/tests/httproute-weight.go
index 2a3e21af..2c1a520d 100644
--- a/conformance/tests/httproute-weight.go
+++ b/conformance/tests/httproute-weight.go
@@ -105,7 +105,7 @@ func testDistribution(t *testing.T, suite *suite.ConformanceTestSuite, gwAddr st
 			if err != nil {
 				return fmt.Errorf("failed to roundtrip request: %w", err)
 			}
-			if err := http.CompareRequest(t, &req, cReq, cRes, expected); err != nil {
+			if err := http.CompareRoundTrip(t, &req, cReq, cRes, expected); err != nil {
 				return fmt.Errorf("response expectation failed for request: %w", err)
 			}
 
diff --git a/conformance/tests/tlsroute-simple-same-namespace.go b/conformance/tests/tlsroute-simple-same-namespace.go
index 51db62ae..b8704c4b 100644
--- a/conformance/tests/tlsroute-simple-same-namespace.go
+++ b/conformance/tests/tlsroute-simple-same-namespace.go
@@ -49,7 +49,7 @@ var TLSRouteSimpleSameNamespace = suite.ConformanceTest{
 		ns := "gateway-conformance-infra"
 		routeNN := types.NamespacedName{Name: "gateway-conformance-infra-test", Namespace: ns}
 		gwNN := types.NamespacedName{Name: "gateway-tlsroute", Namespace: ns}
-		certNN := types.NamespacedName{Name: "tls-passthrough-checks-certificate", Namespace: ns}
+		certNN := types.NamespacedName{Name: "tls-checks-certificate", Namespace: ns}
 
 		kubernetes.NamespacesMustBeReady(t, suite.Client, suite.TimeoutConfig, []string{ns})
 
diff --git a/conformance/utils/kubernetes/certificate_test.go b/conformance/utils/kubernetes/certificate_test.go
new file mode 100644
index 00000000..f6c78048
--- /dev/null
+++ b/conformance/utils/kubernetes/certificate_test.go
@@ -0,0 +1,101 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package kubernetes
+
+import (
+	"bytes"
+	"crypto/x509"
+	"encoding/pem"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_generateCACert(t *testing.T) {
+	tests := []struct {
+		name        string
+		hosts       []string
+		expectedErr []string
+	}{
+		{
+			name:  "one host generates cert with no host",
+			hosts: []string{},
+		},
+		{
+			name:  "one host generates cert for same host",
+			hosts: []string{"abc.example.com"},
+		},
+		{
+			name:  "wildcard generates cert for same host",
+			hosts: []string{"*.example.com"},
+		},
+		{
+			name:  "two hosts generates cert for both hosts",
+			hosts: []string{"abc.example.com", "def.example.com"},
+		},
+		{
+			name:        "bad host generates cert for no host",
+			hosts:       []string{"--abc.example.com"},
+			expectedErr: []string{"x509: certificate is not valid for any names, but wanted to match --abc.example.com"},
+		},
+		{
+			name:        "one good host and one bad host generates cert for only good host",
+			hosts:       []string{"---.example.com", "def.example.com"},
+			expectedErr: []string{"x509: certificate is valid xxx for def.example.com, not ---.example.com", ""},
+		},
+	}
+
+	var serverKey, serverCert bytes.Buffer
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			serverCert.Reset()
+			serverKey.Reset()
+			// Test the function generateCACert.  We can only test normative function
+			// and hostnames, everything else is hardcoded.
+			_, caBytes, caPrivKey, err := generateCACert(tc.hosts)
+			require.NoError(t, err, "unexpected error generating RSA certificate")
+
+			var certData bytes.Buffer
+			if err := pem.Encode(&certData, &pem.Block{Type: "CERTIFICATE", Bytes: caBytes}); err != nil {
+				require.NoError(t, err, "failed to create certificater")
+			}
+
+			var keyData bytes.Buffer
+			if err := pem.Encode(&keyData, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey)}); err != nil {
+				require.NoError(t, err, "failed to create key")
+			}
+
+			// Test that the CA certificate is decodable, parseable, and has the configured hostname/s.
+			block, _ := pem.Decode(certData.Bytes())
+			if block == nil {
+				require.FailNow(t, "failed to decode PEM block containing cert")
+			} else if block.Type == "CERTIFICATE" {
+				cert, err := x509.ParseCertificate(block.Bytes)
+				require.NoError(t, err, "failed to parse certificate")
+				for idx, h := range tc.hosts {
+					err = cert.VerifyHostname(h)
+					if err != nil && len(tc.expectedErr) > 0 && tc.expectedErr[idx] == "" {
+						require.EqualValues(t, tc.expectedErr[idx], err.Error(), "certificate verification failed")
+					} else if err == nil && len(tc.expectedErr) > 0 && tc.expectedErr[idx] != "" {
+						require.EqualValues(t, tc.expectedErr[idx], err, "expected an error but certification verification succeeded")
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/conformance/utils/suite/suite_test.go b/conformance/utils/suite/suite_test.go
index ccb259ad..311da616 100644
--- a/conformance/utils/suite/suite_test.go
+++ b/conformance/utils/suite/suite_test.go
@@ -280,7 +280,6 @@ func TestSuiteReport(t *testing.T) {
 					coreProvisionalTest.ShortName,
 					extendedProvisionalTest.ShortName,
 				},
-				InferredSupportedFeatures: true,
 			},
 		},
 		{
@@ -390,7 +389,6 @@ func TestSuiteReport(t *testing.T) {
 						},
 					},
 				},
-				InferredSupportedFeatures: true,
 			},
 		},
 	}
@@ -434,33 +432,37 @@ func TestInferSupportedFeatures(t *testing.T) {
 		exemptFeatures     FeaturesSet
 		ConformanceProfile sets.Set[ConformanceProfileName]
 		expectedFeatures   FeaturesSet
-		expectedIsInferred bool
+		expectedSource     supportedFeaturesSource
 	}{
 		{
-			name:               "properly infer supported features",
-			expectedFeatures:   namesToFeatureSet(statusFeatureNames),
-			expectedIsInferred: true,
+			name:             "properly infer supported features",
+			expectedFeatures: namesToFeatureSet(statusFeatureNames),
+			expectedSource:   supportedFeaturesSourceInferred,
 		},
 		{
 			name:              "no features",
 			supportedFeatures: sets.New[features.FeatureName]("Gateway"),
 			expectedFeatures:  sets.New[features.FeatureName]("Gateway"),
+			expectedSource:    supportedFeaturesSourceManual,
 		},
 		{
 			name:              "remove exempt features",
 			supportedFeatures: sets.New[features.FeatureName]("Gateway", "HTTPRoute"),
 			exemptFeatures:    sets.New[features.FeatureName]("HTTPRoute"),
 			expectedFeatures:  sets.New[features.FeatureName]("Gateway"),
+			expectedSource:    supportedFeaturesSourceManual,
 		},
 		{
 			name:             "allow all features",
 			allowAllFeatures: true,
 			expectedFeatures: features.SetsToNamesSet(features.AllFeatures),
+			expectedSource:   supportedFeaturesSourceManual,
 		},
 		{
 			name:               "supports conformance profile - core",
 			ConformanceProfile: sets.New(GatewayHTTPConformanceProfileName),
-			expectedFeatures:   namesToFeatureSet([]string{"Gateway", "HTTPRoute", "ReferenceGrant"}),
+			expectedFeatures:   namesToFeatureSet(statusFeatureNames),
+			expectedSource:     supportedFeaturesSourceInferred,
 		},
 	}
 
@@ -512,8 +514,8 @@ func TestInferSupportedFeatures(t *testing.T) {
 				t.Fatalf("error initializing conformance suite: %v", err)
 			}
 
-			if cSuite.IsInferredSupportedFeatures() != tc.expectedIsInferred {
-				t.Errorf("InferredSupportedFeatures mismatch: got %v, want %v", cSuite.IsInferredSupportedFeatures(), tc.expectedIsInferred)
+			if cSuite.supportedFeaturesSource != tc.expectedSource {
+				t.Errorf("InferredSupportedFeatures mismatch: got %v, want %v", cSuite.supportedFeaturesSource, tc.expectedSource)
 			}
 
 			if equal := cSuite.SupportedFeatures.Equal(tc.expectedFeatures); !equal {
@@ -523,6 +525,55 @@ func TestInferSupportedFeatures(t *testing.T) {
 	}
 }
 
+func TestGWCPublishedMeshFeatures(t *testing.T) {
+	gwcName := "ochopintre"
+	gwc := &gatewayv1.GatewayClass{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: gwcName,
+		},
+		Spec: gatewayv1.GatewayClassSpec{
+			ControllerName: "example.com/gateway-controller",
+		},
+		Status: gatewayv1.GatewayClassStatus{
+			Conditions: []metav1.Condition{
+				{
+					Type:    string(gatewayv1.GatewayConditionAccepted),
+					Status:  metav1.ConditionTrue,
+					Reason:  "Accepted",
+					Message: "GatewayClass is accepted and ready for use",
+				},
+			},
+			SupportedFeatures: featureNamesToSet([]string{
+				string(features.SupportGateway),
+				string(features.SupportGatewayStaticAddresses),
+				string(features.SupportMeshClusterIPMatching),
+				string(features.SupportMeshConsumerRoute),
+			}),
+		},
+	}
+	scheme := runtime.NewScheme()
+	scheme.AddKnownTypes(gatewayv1.SchemeGroupVersion, &gatewayv1.GatewayClass{})
+	fakeClient := fake.NewClientBuilder().
+		WithScheme(scheme).
+		WithObjects(gwc).
+		WithLists(&apiextensionsv1.CustomResourceDefinitionList{}).
+		Build()
+
+	gatewayv1.Install(fakeClient.Scheme())
+	apiextensionsv1.AddToScheme(fakeClient.Scheme())
+
+	options := ConformanceOptions{
+		AllowCRDsMismatch: true,
+		GatewayClassName:  gwcName,
+		Client:            fakeClient,
+	}
+
+	_, err := NewConformanceTestSuite(options)
+	if err == nil {
+		t.Fatalf("expected an error but got nil")
+	}
+}
+
 func featureNamesToSet(set []string) []gatewayv1.SupportedFeature {
 	var features []gatewayv1.SupportedFeature
 	for _, feature := range set {