Repository: envoyproxy/gateway
Pull request #5613: feat(extension manager): dynamically reload CA certificates
Author: login: sapirpol
Created: 2025-03-25T20:36:46Z | Merged: 2025-04-16T19:24:57Z

**What type of PR is this?**
Dynamically reload CA certificates for GRPC connections on extension-manager.
<!--
Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary
and summary followed by a colon. format `chore/docs/api/feat/fix/refactor/style/test: summary`.
Examples:
* "docs: fix grammar error"
* "feat(translator): add new feature"
* "fix: fix xx bug"
* "chore: change ci & build tools etc"
* "api: add xxx fields in ClientTrafficPolicy"

Before raising a PR, please go through this section of the developer guide, https://gateway.envoyproxy.io/contributions/develop/#raising-a-pr
-->

<!--
NOTE: If your PR contains any API changes (changes under `/api`), we recommend you to separate these API changes into
a new PR, and we will review the API part first. It will save you lots of implementation time if the API get accepted.
-->

**What this PR does / why we need it**:
This PR introduces dynamic reloading of CA certificates used by Envoy Gateway's extension manager when establishing GRPC connections. Previously, certificates were loaded once, making updates difficult without restarting the service. To resolve this, the PR uses grpc-go's advancedtls library, enabling dynamic certificate management via a custom callback that reloads the certificate.

**Comparison of TLS Settings: Before vs. After Update**
Setting | Before (Old Approach) | After (New Approach)
-- | -- | --
Min TLS Version | TLS 1.2 | TLS 1.2
Max TLS Version | Highest available | Highest available
Ciphers | Used safe default cipher list with explicitly forbidden RFC 7540 ciphers [grpc-go](https://github.com/grpc/grpc-go/blob/master/credentials/tls.go#L192). | Uses safe default cipher list [grpc-go](https://github.com/grpc/grpc-go/blob/master/security/advancedtls/advancedtls.go#L220-L222). RSA key exchange, 3DES, and CBC cipher suites removed in [Go 1.22](https://go.dev/doc/go1.22#crypto/tls) and [Go 1.23](https://go.dev/doc/go1.22#crypto/tls).
Dynamic Certificate Loading | No | Yes (Using GetRootCertificates)

This change was manually tested.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes https://github.com/envoyproxy/gateway/issues/5396

<!--
For any non-trivial changes, you need to provide a brief description of the changes in the release notes.
Please add the description to the release-notes/current.yaml file and include this file in the PR.
-->
Release Notes: Yes


Diff:
diff --git a/go.mod b/go.mod
index 50ed5ca95e..e0f61f9f28 100644
--- a/go.mod
+++ b/go.mod
@@ -63,6 +63,7 @@ require (
 	golang.org/x/net v0.39.0
 	google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a
 	google.golang.org/grpc v1.71.1
+	google.golang.org/grpc/security/advancedtls v1.0.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/yaml.v3 v3.0.1
 	helm.sh/helm/v3 v3.17.3
diff --git a/go.sum b/go.sum
index ef997dfb3e..663313107f 100644
--- a/go.sum
+++ b/go.sum
@@ -1693,6 +1693,10 @@ google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
 google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
+google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b h1:NuxyvVZoDfHZwYW9LD4GJiF5/nhiSyP4/InTrvw9Ibk=
+google.golang.org/grpc/examples v0.0.0-20201112215255-90f1b3ee835b/go.mod h1:IBqQ7wSUJ2Ep09a8rMWFsg4fmI2r38zwsq8a0GgxXpM=
+google.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA=
+google.golang.org/grpc/security/advancedtls v1.0.0/go.mod h1:o+s4go+e1PJ2AjuQMY5hU82W7lDlefjJA6FqEHRVHWk=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
diff --git a/internal/extension/registry/extension_manager.go b/internal/extension/registry/extension_manager.go
index 057d77712a..2350a3bb2d 100644
--- a/internal/extension/registry/extension_manager.go
+++ b/internal/extension/registry/extension_manager.go
@@ -17,6 +17,7 @@ import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/security/advancedtls"
 	"google.golang.org/grpc/test/bufconn"
 	corev1 "k8s.io/api/core/v1"
 	k8scli "sigs.k8s.io/controller-runtime/pkg/client"
@@ -245,18 +246,6 @@ func (m *Manager) CleanupHookConns() {
 	}
 }
 
-func parseCA(caSecret *corev1.Secret) (*x509.CertPool, error) {
-	caCertPEMBytes, ok := caSecret.Data[corev1.TLSCertKey]
-	if !ok {
-		return nil, errors.New("no cert found in CA secret")
-	}
-	cp := x509.NewCertPool()
-	if ok := cp.AppendCertsFromPEM(caCertPEMBytes); !ok {
-		return nil, errors.New("failed to append certificates")
-	}
-	return cp, nil
-}
-
 func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) ([]grpc.DialOption, error) {
 	// These two errors shouldn't happen since we check these conditions when loading the extension
 	if ext == nil {
@@ -267,20 +256,16 @@ func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.Extens
 	}
 
 	var opts []grpc.DialOption
-	var creds credentials.TransportCredentials
 	if ext.Service.TLS != nil {
-		certRef := ext.Service.TLS.CertificateRef
-		secret, secretNamespace, err := kubernetes.ValidateSecretObjectReference(ctx, client, &certRef, namespace)
+		// Sanity check to ensure that the extension manager has a valid certificate reference
+		_, err := getCertPoolFromSecret(ctx, client, ext, namespace)
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("failed to get root CA certificates: %w", err)
 		}
-
-		cp, err := parseCA(secret)
+		creds, err := getGRPCCredentials(client, ext, namespace)
 		if err != nil {
-			return nil, fmt.Errorf("error parsing cert in Secret %s in namespace %s", string(certRef.Name), secretNamespace)
+			return nil, fmt.Errorf("failed to get gRPC TLS credentials: %w", err)
 		}
-
-		creds = credentials.NewClientTLSFromCert(cp, "")
 		opts = append(opts, grpc.WithTransportCredentials(creds))
 	} else {
 		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
@@ -300,3 +285,42 @@ func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.Extens
 
 	return opts, nil
 }
+
+func getGRPCCredentials(client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (credentials.TransportCredentials, error) {
+	return advancedtls.NewClientCreds(&advancedtls.Options{
+		RootOptions: advancedtls.RootCertificateOptions{
+			// A callback function that dynamically loads root CA certificates from secret
+			GetRootCertificates: createGetRootCertificatesHandler(client, ext, namespace),
+		},
+	})
+}
+
+func createGetRootCertificatesHandler(client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) func(*advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+	return func(params *advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+		ctx := context.Background()
+		cp, err := getCertPoolFromSecret(ctx, client, ext, namespace)
+		if err != nil {
+			return nil, err
+		}
+
+		return &advancedtls.RootCertificates{TrustCerts: cp}, nil
+	}
+}
+
+func getCertPoolFromSecret(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (*x509.CertPool, error) {
+	certRef := ext.Service.TLS.CertificateRef
+	secret, _, err := kubernetes.ValidateSecretObjectReference(ctx, client, &certRef, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("failed to validate TLS certificate reference: %w", err)
+	}
+
+	caCertPEMBytes, ok := secret.Data[corev1.TLSCertKey]
+	if !ok {
+		return nil, fmt.Errorf("no CA certificate found in Kubernetes Secret %s in namespace %s", secret.GetName(), secret.GetNamespace())
+	}
+	cp := x509.NewCertPool()
+	if ok := cp.AppendCertsFromPEM(caCertPEMBytes); !ok {
+		return nil, errors.New("failed to append certificates from CA secret")
+	}
+	return cp, nil
+}
diff --git a/internal/extension/registry/extension_manager_test.go b/internal/extension/registry/extension_manager_test.go
index e5d59783ca..252a19928a 100644
--- a/internal/extension/registry/extension_manager_test.go
+++ b/internal/extension/registry/extension_manager_test.go
@@ -7,18 +7,28 @@ package registry
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"fmt"
 	"math"
+	"net"
+	"os"
 	"testing"
 
+	v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	"github.com/stretchr/testify/require"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/envoygateway"
+	"github.com/envoyproxy/gateway/proto/extension"
 )
 
 func TestGetExtensionServerAddress(t *testing.T) {
@@ -150,3 +160,96 @@ func Test_setupGRPCOpts(t *testing.T) {
 		})
 	}
 }
+
+type testServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func (s *testServer) PostRouteModify(ctx context.Context, req *extension.PostRouteModifyRequest) (*extension.PostRouteModifyResponse, error) {
+	return &extension.PostRouteModifyResponse{
+		Route: req.Route,
+	}, nil
+}
+
+func Test_TLS(t *testing.T) {
+	testDir := "testdata"
+	caFile := testDir + "/ca.pem"
+	certFile := testDir + "/cert.pem"
+	keyFile := testDir + "/key.pem"
+
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	require.NoError(t, err)
+
+	caCert, err := os.ReadFile(caFile)
+	require.NoError(t, err)
+	caPool := x509.NewCertPool()
+	ok := caPool.AppendCertsFromPEM(caCert)
+	require.True(t, ok)
+
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	defer lis.Close()
+
+	port := lis.Addr().(*net.TCPAddr).Port
+	server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{cert},
+		ClientAuth:   tls.NoClientCert,
+		MinVersion:   tls.VersionTLS12,
+	})))
+	extension.RegisterEnvoyGatewayExtensionServer(server, &testServer{})
+	go func() {
+		_ = server.Serve(lis)
+		defer server.GracefulStop()
+	}()
+
+	extManager := &egv1a1.ExtensionManager{
+		Service: &egv1a1.ExtensionService{
+			BackendEndpoint: egv1a1.BackendEndpoint{
+				IP: &egv1a1.IPEndpoint{
+					Address: "localhost",
+					Port:    int32(port),
+				},
+			},
+			TLS: &egv1a1.ExtensionTLS{
+				CertificateRef: gwapiv1.SecretObjectReference{
+					Name:      "cert",
+					Namespace: ptr.To(gwapiv1.Namespace("default")),
+				},
+			},
+		},
+	}
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cert",
+			Namespace: "default",
+		},
+		Type: corev1.SecretTypeTLS,
+		Data: map[string][]byte{
+			corev1.TLSCertKey: caCert,
+		},
+	}
+
+	fakeClient := fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(secret).Build()
+
+	opts, err := setupGRPCOpts(context.Background(), fakeClient, extManager, "test-ns")
+	require.NoError(t, err)
+	require.NotEmpty(t, opts)
+
+	conn, err := grpc.DialContext(context.Background(), fmt.Sprintf("localhost:%d", port),
+		opts...,
+	)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	client := extension.NewEnvoyGatewayExtensionClient(conn)
+	require.NotNil(t, client)
+
+	response, err := client.PostRouteModify(context.Background(), &extension.PostRouteModifyRequest{
+		Route: &v3.Route{
+			Name: "test-route",
+		},
+	})
+	require.NoError(t, err)
+	require.Equal(t, "test-route", response.Route.Name)
+}
diff --git a/internal/extension/registry/testdata/ca.pem b/internal/extension/registry/testdata/ca.pem
new file mode 100644
index 0000000000..e45d7a28b6
--- /dev/null
+++ b/internal/extension/registry/testdata/ca.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAgmgAwIBAgIUGkbKo/xIQFgfIzSWmWIr6IIt6hEwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI1MDQwNjEyMjEzOFoYDzIxMjUw
+MzEzMTIyMTM4WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDfkT1bhPFG90raDJLSU5nwVGWJK7mtiT+aMQzaBuHr
+hXQPWNWF+SflslO/tsVf67gX4eChUwXiUmoTA6Ahh9nlJ7fTWmK7QJkdcTjKv1N+
+DiEFWUvOpssviBfHavXkJjMsorBLBGCGS03q4xznDVYf9mfwUIiprkVBGQtYcH+M
+BBb4ws2SxhTaFK0rr9cwwavOf+omHiE0cvQBd73+G8nFEq8vKTnDsub3ozk+GSXf
+5Hu81reePUcU1lWzksik4U5ZcsL+/B4sU1TwEhL6zLhQ9kr3KVyyW2wVESQzKk4O
+XrYrPnQdULXWnAQBFPUOfiD2kHCGcylTZ1D4pcp4Q8EnAgMBAAGjaTBnMB0GA1Ud
+DgQWBBSkuvsTM4xhiNIzJBk0cCtfj4KhWzAfBgNVHSMEGDAWgBSkuvsTM4xhiNIz
+JBk0cCtfj4KhWzAPBgNVHRMBAf8EBTADAQH/MBQGA1UdEQQNMAuCCWxvY2FsaG9z
+dDANBgkqhkiG9w0BAQsFAAOCAQEAwk/AvTJcjmDQ5n0CQfqEUNnzHd24yPLob4rG
+GbrBgLO7YRS0l4ot6oTRau5PUAJ2eL0v3eGITaxZ+N7IWpa5Y0TFCcyFXBwtW9hq
+i2+v1CfMfz6+/HjbWq1VvgRwATMctRbcl1XGR1DNs6jKKakTBoYDped9wAkKeSRb
++4ZK989M4zCuwLwcej/sEhRdazuEykm+QgUSabb45CrSnJb4gOYd0CdOilXr98EQ
+Z2k4Hno434aD1CEIm4Fu7iwiKSR6TLFHHqC0bBd2aknB6DHLLMZMo1dXJchjQrkN
+EO9DtJxCTVRLTbEA0589h50fW8/L1GtwYgSo721fntphbgw3VA==
+-----END CERTIFICATE-----
diff --git a/internal/extension/registry/testdata/cert.pem b/internal/extension/registry/testdata/cert.pem
new file mode 100644
index 0000000000..e45d7a28b6
--- /dev/null
+++ b/internal/extension/registry/testdata/cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDITCCAgmgAwIBAgIUGkbKo/xIQFgfIzSWmWIr6IIt6hEwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI1MDQwNjEyMjEzOFoYDzIxMjUw
+MzEzMTIyMTM4WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQDfkT1bhPFG90raDJLSU5nwVGWJK7mtiT+aMQzaBuHr
+hXQPWNWF+SflslO/tsVf67gX4eChUwXiUmoTA6Ahh9nlJ7fTWmK7QJkdcTjKv1N+
+DiEFWUvOpssviBfHavXkJjMsorBLBGCGS03q4xznDVYf9mfwUIiprkVBGQtYcH+M
+BBb4ws2SxhTaFK0rr9cwwavOf+omHiE0cvQBd73+G8nFEq8vKTnDsub3ozk+GSXf
+5Hu81reePUcU1lWzksik4U5ZcsL+/B4sU1TwEhL6zLhQ9kr3KVyyW2wVESQzKk4O
+XrYrPnQdULXWnAQBFPUOfiD2kHCGcylTZ1D4pcp4Q8EnAgMBAAGjaTBnMB0GA1Ud
+DgQWBBSkuvsTM4xhiNIzJBk0cCtfj4KhWzAfBgNVHSMEGDAWgBSkuvsTM4xhiNIz
+JBk0cCtfj4KhWzAPBgNVHRMBAf8EBTADAQH/MBQGA1UdEQQNMAuCCWxvY2FsaG9z
+dDANBgkqhkiG9w0BAQsFAAOCAQEAwk/AvTJcjmDQ5n0CQfqEUNnzHd24yPLob4rG
+GbrBgLO7YRS0l4ot6oTRau5PUAJ2eL0v3eGITaxZ+N7IWpa5Y0TFCcyFXBwtW9hq
+i2+v1CfMfz6+/HjbWq1VvgRwATMctRbcl1XGR1DNs6jKKakTBoYDped9wAkKeSRb
++4ZK989M4zCuwLwcej/sEhRdazuEykm+QgUSabb45CrSnJb4gOYd0CdOilXr98EQ
+Z2k4Hno434aD1CEIm4Fu7iwiKSR6TLFHHqC0bBd2aknB6DHLLMZMo1dXJchjQrkN
+EO9DtJxCTVRLTbEA0589h50fW8/L1GtwYgSo721fntphbgw3VA==
+-----END CERTIFICATE-----
diff --git a/internal/extension/registry/testdata/key.pem b/internal/extension/registry/testdata/key.pem
new file mode 100644
index 0000000000..8e789d3949
--- /dev/null
+++ b/internal/extension/registry/testdata/key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDfkT1bhPFG90ra
+DJLSU5nwVGWJK7mtiT+aMQzaBuHrhXQPWNWF+SflslO/tsVf67gX4eChUwXiUmoT
+A6Ahh9nlJ7fTWmK7QJkdcTjKv1N+DiEFWUvOpssviBfHavXkJjMsorBLBGCGS03q
+4xznDVYf9mfwUIiprkVBGQtYcH+MBBb4ws2SxhTaFK0rr9cwwavOf+omHiE0cvQB
+d73+G8nFEq8vKTnDsub3ozk+GSXf5Hu81reePUcU1lWzksik4U5ZcsL+/B4sU1Tw
+EhL6zLhQ9kr3KVyyW2wVESQzKk4OXrYrPnQdULXWnAQBFPUOfiD2kHCGcylTZ1D4
+pcp4Q8EnAgMBAAECggEAad3jCkWH460SuvKdTA/eo5mzgyM7h9uoTKHRjiUYMmk9
+rk7IvepgZDrvwacRC/0ZbeW+c2ZXMPcJ1/sthvNH0WXffKweeMF8QB7vX8L+DIEw
+TdIdPbxqFxYA/KO/0MvuC8L2bG+kLbPXL9VgSGFeWUBbs8bPEFq/3LW5U6XiIDCk
++tVmyjL8YgqFouzrMvfdy1CHChmC3Kk9vPlJIJJ90ru7+p1l7NK8fYMWe49CKNcR
+ayfPLedI3zNQwbywkpvFebzAM04/YGDMqwhyfajc0hHOpvroWA3/UNIssvHLULk6
+K4INTcMVn+F1PI4+iDEneWJvrjU02p0MjgM7XeOEkQKBgQDzvgQNDGL2NUwmqfia
+/qvqusWDVyH9DdCuPilt0GsFcvjdNhD1Jvp3V/B5bqFclR8+iPa71beYVqzGjVxp
+b+B5nJ2BMQ0D6p9ItZ0czO+ZHj1nQI13m90lG1D/Yfu9EQNpnDCUP7jL3oE3Aw+U
+7vZOkGI4JuDI4vBpXEr+XRsOBQKBgQDqz3z+djzKOzR1TcZ+qsrUkWhSKhDngLIf
+sz6wUE535Ebvu4u3gK9RethZK7ogkNxI4aN7g3Dr8k+jEflPZCbbuGjhyRA5mQal
+EUj7V5e17tgZC4LwXVVAd/b4grrMKfJD5CptBjmRFhbU/mNjiN+C/VpPiABLWzc+
+5KJjK1pOOwKBgQCOmY65K5QCQ3BH9o0x8OkXrSm9C48hA6IhLtECJDtYtskOcoE9
+TA6hH9vaz3SsO7pJ1cu0XPbKs3rltvJn5UJJI+2qFc5tiiiiW06N+P/8bwqxi7y4
+S4H4IaEjqGmlVXzPnsd1FErDS2wBiVLmaV/E2wf7nhNItCy/F3XwlvwrGQKBgQC4
+wusnjwHmXw+3/arioKFZAdGEVXVXs/x01SXOtmIIFKd8m6Ykji1lf7Qc9jtOxK2Q
+63soBRUlk1T2i34Q6k1pNoHQp9UMfUytNhynKRVHItkHW/d8CvS59atvf+5cF+V5
+Zl+7ydoiP69XiZPLDjhRaelWCz2wDeY6pszgG4zDYQKBgHgagAqV3qlWScVJBf6F
+FyqRjt3aXVxM11f8LakXTI/3crFFdWp33HGhEw8Pv5EKu4ynJnBbYc4EJ8N1KfTB
+iZ6rSpNYhkB6JRsSBO/RZjcceHc4wcyJkBciD3G+ZhwI5Ydrgt4/+pJX/9fA0wks
+DF883l7zYtiBvGQXpKYwup6t
+-----END PRIVATE KEY-----


Reviews:
user: login: guydc
body: LGTM
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T17:24:33Z
user: login: nareddyt
body: Thanks for making this PR! A few minor comments on the production code. Though the test may need another look.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T03:08:48Z
user: login: sapirpol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T12:19:03Z
user: login: sapirpol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T12:24:43Z
user: login: sapirpol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T12:25:33Z
user: login: nareddyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T01:53:47Z
user: login: nareddyt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T01:59:09Z
user: login: sapirpol
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:14:42Z
user: login: guydc
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T05:56:23Z
user: login: nareddyt
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T07:46:10Z
user: login: arkodg
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T19:24:33Z

Review comments:
diff_hunk: @@ -300,3 +285,41 @@ func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.Extens
 
 	return opts, nil
 }
+
+func getGRPCCredentials(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (credentials.TransportCredentials, error) {
+	return advancedtls.NewClientCreds(&advancedtls.Options{
+		RootOptions: advancedtls.RootCertificateOptions{
+			// A callback function that dynamically loads root CA certificates from secret
+			GetRootCertificates: createGetRootCertificatesHandler(ctx, client, ext, namespace),
+		},
+	})
+}
+
+func createGetRootCertificatesHandler(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) func(*advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+	return func(params *advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+		cp, err := getCertPoolFromSecret(ctx, client, ext, namespace)
+		if err != nil {
+			return nil, err
+		}
+
+		return &advancedtls.RootCertificates{TrustCerts: cp}, nil
+	}
+}
+
+func getCertPoolFromSecret(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (*x509.CertPool, error) {
+	certRef := ext.Service.TLS.CertificateRef
+	secret, _, err := kubernetes.ValidateSecretObjectReference(ctx, client, &certRef, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("failed to validate TLS certificate reference: %w", err)
+	}
+
+	caCertPEMBytes, ok := secret.Data[corev1.TLSCertKey]
+	if !ok {
+		return nil, errors.New("no TLS certificate found in CA secret")
path: internal/extension/registry/extension_manager.go
user: login: nareddyt
body: ```suggestion
		return nil, fmt.Errorf("no CA certificate found in Kubernetes Secret %s in namespace %s", secret.GetName(), secret.GetNamespace())
```

1. This is parsing a CA certificate, not TLS certificate. The pre-existing naming of key `corev1.TLSCertKey` is incorrect unfortunately, but we need to support it for backwards compatibility.
2. Let's include secret name and namespace for debuggability.
created_at: 2025-04-03T02:47:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,3 +285,41 @@ func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.Extens
 
 	return opts, nil
 }
+
+func getGRPCCredentials(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (credentials.TransportCredentials, error) {
+	return advancedtls.NewClientCreds(&advancedtls.Options{
+		RootOptions: advancedtls.RootCertificateOptions{
+			// A callback function that dynamically loads root CA certificates from secret
+			GetRootCertificates: createGetRootCertificatesHandler(ctx, client, ext, namespace),
+		},
+	})
+}
+
+func createGetRootCertificatesHandler(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) func(*advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+	return func(params *advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+		cp, err := getCertPoolFromSecret(ctx, client, ext, namespace)
path: internal/extension/registry/extension_manager.go
user: login: nareddyt
body: This function will run in the background in a goroutine inside advancedtls, correct?

If so, I suggest you don't pass in the parent context to this function, as someone may change the parent context to cancel or timeout in the future.

Instead, create a new context in this function `ctx := context.Background()`
created_at: 2025-04-03T02:56:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,3 +160,103 @@ func Test_setupGRPCOpts(t *testing.T) {
 		})
 	}
 }
+
+type testServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func (s *testServer) PostRouteModify(ctx context.Context, req *extension.PostRouteModifyRequest) (*extension.PostRouteModifyResponse, error) {
+	return &extension.PostRouteModifyResponse{
+		Route: req.Route,
+	}, nil
+}
+
+func Test_TLS(t *testing.T) {
+	testDir := "testdata"
+	caFile := testDir + "/ca.pem"
+	certFile := testDir + "/cert.pem"
+	keyFile := testDir + "/key.pem"
+
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	require.NoError(t, err)
+
+	caCert, err := os.ReadFile(caFile)
+	require.NoError(t, err)
+	caPool := x509.NewCertPool()
+	ok := caPool.AppendCertsFromPEM(caCert)
+	require.True(t, ok)
+
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	defer lis.Close()
+
+	port := lis.Addr().(*net.TCPAddr).Port
+	server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{cert},
+		ClientAuth:   tls.NoClientCert,
+	})))
+	extension.RegisterEnvoyGatewayExtensionServer(server, &testServer{})
+	go func() {
+		_ = server.Serve(lis)
+		defer server.GracefulStop()
+	}()
+
+	extManager := &egv1a1.ExtensionManager{
+		Service: &egv1a1.ExtensionService{
+			BackendEndpoint: egv1a1.BackendEndpoint{
+				IP: &egv1a1.IPEndpoint{
+					Address: "localhost",
+					Port:    int32(port),
+				},
+			},
+			TLS: &egv1a1.ExtensionTLS{
+				CertificateRef: gwapiv1.SecretObjectReference{
+					Name:      "cert",
+					Namespace: ptr.To(gwapiv1.Namespace("default")),
+				},
+			},
+		},
+	}
+
+	certData, err := os.ReadFile(certFile)
+	require.NoError(t, err)
+	keyData, err := os.ReadFile(keyFile)
+	require.NoError(t, err)
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cert",
+			Namespace: "default",
+		},
+		Type: corev1.SecretTypeTLS,
+		Data: map[string][]byte{
+			corev1.TLSCertKey:       certData,
path: internal/extension/registry/extension_manager_test.go
user: login: nareddyt
body: Unfortunately this doesn't seem right. `extension_manager.go` line 316 is reading CA cert from `corev1.TLSCertKey` (yes it is incorrectly named, but that is a legacy behavior we need to support).

If so, I'm not entirely sure how this test is working (?) Because the CA cert is not in the correct place.
created_at: 2025-04-03T03:01:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,3 +160,103 @@ func Test_setupGRPCOpts(t *testing.T) {
 		})
 	}
 }
+
+type testServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func (s *testServer) PostRouteModify(ctx context.Context, req *extension.PostRouteModifyRequest) (*extension.PostRouteModifyResponse, error) {
+	return &extension.PostRouteModifyResponse{
+		Route: req.Route,
+	}, nil
+}
+
+func Test_TLS(t *testing.T) {
+	testDir := "testdata"
+	caFile := testDir + "/ca.pem"
+	certFile := testDir + "/cert.pem"
+	keyFile := testDir + "/key.pem"
+
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	require.NoError(t, err)
+
+	caCert, err := os.ReadFile(caFile)
+	require.NoError(t, err)
+	caPool := x509.NewCertPool()
+	ok := caPool.AppendCertsFromPEM(caCert)
+	require.True(t, ok)
+
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	defer lis.Close()
+
+	port := lis.Addr().(*net.TCPAddr).Port
+	server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{cert},
+		ClientAuth:   tls.NoClientCert,
+	})))
+	extension.RegisterEnvoyGatewayExtensionServer(server, &testServer{})
+	go func() {
+		_ = server.Serve(lis)
+		defer server.GracefulStop()
+	}()
+
+	extManager := &egv1a1.ExtensionManager{
+		Service: &egv1a1.ExtensionService{
+			BackendEndpoint: egv1a1.BackendEndpoint{
+				IP: &egv1a1.IPEndpoint{
+					Address: "localhost",
+					Port:    int32(port),
+				},
+			},
+			TLS: &egv1a1.ExtensionTLS{
+				CertificateRef: gwapiv1.SecretObjectReference{
+					Name:      "cert",
+					Namespace: ptr.To(gwapiv1.Namespace("default")),
+				},
+			},
+		},
+	}
+
+	certData, err := os.ReadFile(certFile)
+	require.NoError(t, err)
+	keyData, err := os.ReadFile(keyFile)
+	require.NoError(t, err)
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cert",
+			Namespace: "default",
+		},
+		Type: corev1.SecretTypeTLS,
+		Data: map[string][]byte{
+			corev1.TLSCertKey:       certData,
path: internal/extension/registry/extension_manager_test.go
user: login: sapirpol
body: I checked the test, and it worked because both the CA and the server certificate were accepted — see: https://stackoverflow.com/questions/77084841/server-certificate-used-as-ca-cert-why-does-it-work/77084918#77084918

I've now updated the test to use a self-signed certificate, so the certificate acts as both the server cert and the CA.
created_at: 2025-04-06T12:19:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,3 +285,41 @@ func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.Extens
 
 	return opts, nil
 }
+
+func getGRPCCredentials(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (credentials.TransportCredentials, error) {
+	return advancedtls.NewClientCreds(&advancedtls.Options{
+		RootOptions: advancedtls.RootCertificateOptions{
+			// A callback function that dynamically loads root CA certificates from secret
+			GetRootCertificates: createGetRootCertificatesHandler(ctx, client, ext, namespace),
+		},
+	})
+}
+
+func createGetRootCertificatesHandler(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) func(*advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+	return func(params *advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+		cp, err := getCertPoolFromSecret(ctx, client, ext, namespace)
path: internal/extension/registry/extension_manager.go
user: login: sapirpol
body: Done
created_at: 2025-04-06T12:24:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -300,3 +285,41 @@ func setupGRPCOpts(ctx context.Context, client k8scli.Client, ext *egv1a1.Extens
 
 	return opts, nil
 }
+
+func getGRPCCredentials(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (credentials.TransportCredentials, error) {
+	return advancedtls.NewClientCreds(&advancedtls.Options{
+		RootOptions: advancedtls.RootCertificateOptions{
+			// A callback function that dynamically loads root CA certificates from secret
+			GetRootCertificates: createGetRootCertificatesHandler(ctx, client, ext, namespace),
+		},
+	})
+}
+
+func createGetRootCertificatesHandler(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) func(*advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+	return func(params *advancedtls.ConnectionInfo) (*advancedtls.RootCertificates, error) {
+		cp, err := getCertPoolFromSecret(ctx, client, ext, namespace)
+		if err != nil {
+			return nil, err
+		}
+
+		return &advancedtls.RootCertificates{TrustCerts: cp}, nil
+	}
+}
+
+func getCertPoolFromSecret(ctx context.Context, client k8scli.Client, ext *egv1a1.ExtensionManager, namespace string) (*x509.CertPool, error) {
+	certRef := ext.Service.TLS.CertificateRef
+	secret, _, err := kubernetes.ValidateSecretObjectReference(ctx, client, &certRef, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("failed to validate TLS certificate reference: %w", err)
+	}
+
+	caCertPEMBytes, ok := secret.Data[corev1.TLSCertKey]
+	if !ok {
+		return nil, errors.New("no TLS certificate found in CA secret")
path: internal/extension/registry/extension_manager.go
user: login: sapirpol
body: I fixed the error message
created_at: 2025-04-06T12:25:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,3 +160,103 @@ func Test_setupGRPCOpts(t *testing.T) {
 		})
 	}
 }
+
+type testServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func (s *testServer) PostRouteModify(ctx context.Context, req *extension.PostRouteModifyRequest) (*extension.PostRouteModifyResponse, error) {
+	return &extension.PostRouteModifyResponse{
+		Route: req.Route,
+	}, nil
+}
+
+func Test_TLS(t *testing.T) {
+	testDir := "testdata"
+	caFile := testDir + "/ca.pem"
+	certFile := testDir + "/cert.pem"
+	keyFile := testDir + "/key.pem"
+
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	require.NoError(t, err)
+
+	caCert, err := os.ReadFile(caFile)
+	require.NoError(t, err)
+	caPool := x509.NewCertPool()
+	ok := caPool.AppendCertsFromPEM(caCert)
+	require.True(t, ok)
+
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	defer lis.Close()
+
+	port := lis.Addr().(*net.TCPAddr).Port
+	server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{cert},
+		ClientAuth:   tls.NoClientCert,
+	})))
+	extension.RegisterEnvoyGatewayExtensionServer(server, &testServer{})
+	go func() {
+		_ = server.Serve(lis)
+		defer server.GracefulStop()
+	}()
+
+	extManager := &egv1a1.ExtensionManager{
+		Service: &egv1a1.ExtensionService{
+			BackendEndpoint: egv1a1.BackendEndpoint{
+				IP: &egv1a1.IPEndpoint{
+					Address: "localhost",
+					Port:    int32(port),
+				},
+			},
+			TLS: &egv1a1.ExtensionTLS{
+				CertificateRef: gwapiv1.SecretObjectReference{
+					Name:      "cert",
+					Namespace: ptr.To(gwapiv1.Namespace("default")),
+				},
+			},
+		},
+	}
+
+	certData, err := os.ReadFile(certFile)
+	require.NoError(t, err)
+	keyData, err := os.ReadFile(keyFile)
+	require.NoError(t, err)
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cert",
+			Namespace: "default",
+		},
+		Type: corev1.SecretTypeTLS,
+		Data: map[string][]byte{
+			corev1.TLSCertKey:       certData,
path: internal/extension/registry/extension_manager_test.go
user: login: nareddyt
body: Wow, did not know that. Thanks for the explanation!
created_at: 2025-04-07T01:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,3 +160,103 @@ func Test_setupGRPCOpts(t *testing.T) {
 		})
 	}
 }
+
+type testServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func (s *testServer) PostRouteModify(ctx context.Context, req *extension.PostRouteModifyRequest) (*extension.PostRouteModifyResponse, error) {
+	return &extension.PostRouteModifyResponse{
+		Route: req.Route,
+	}, nil
+}
+
+func Test_TLS(t *testing.T) {
+	testDir := "testdata"
+	caFile := testDir + "/ca.pem"
+	certFile := testDir + "/cert.pem"
+	keyFile := testDir + "/key.pem"
+
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	require.NoError(t, err)
+
+	caCert, err := os.ReadFile(caFile)
+	require.NoError(t, err)
+	caPool := x509.NewCertPool()
+	ok := caPool.AppendCertsFromPEM(caCert)
+	require.True(t, ok)
+
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	defer lis.Close()
+
+	port := lis.Addr().(*net.TCPAddr).Port
+	server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{cert},
+		ClientAuth:   tls.NoClientCert,
+	})))
+	extension.RegisterEnvoyGatewayExtensionServer(server, &testServer{})
+	go func() {
+		_ = server.Serve(lis)
+		defer server.GracefulStop()
+	}()
+
+	extManager := &egv1a1.ExtensionManager{
+		Service: &egv1a1.ExtensionService{
+			BackendEndpoint: egv1a1.BackendEndpoint{
+				IP: &egv1a1.IPEndpoint{
+					Address: "localhost",
+					Port:    int32(port),
+				},
+			},
+			TLS: &egv1a1.ExtensionTLS{
+				CertificateRef: gwapiv1.SecretObjectReference{
+					Name:      "cert",
+					Namespace: ptr.To(gwapiv1.Namespace("default")),
+				},
+			},
+		},
+	}
+
+	certData, err := os.ReadFile(certFile)
+	require.NoError(t, err)
+	keyData, err := os.ReadFile(keyFile)
+	require.NoError(t, err)
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cert",
+			Namespace: "default",
+		},
+		Type: corev1.SecretTypeTLS,
+		Data: map[string][]byte{
+			corev1.TLSCertKey:       certData,
path: internal/extension/registry/extension_manager_test.go
user: login: nareddyt
body: I recommend modifying the test to only pass in the exact necessary data. This makes it clearer to readers that the current naming is incorrect.

Specifically, change the secret to just this, the test should still pass:

```
Data: map[string][]byte{
  corev1.TLSCertKey:       caCert,
},
```

Otherwise the PR looks good.
created_at: 2025-04-07T01:58:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -150,3 +160,103 @@ func Test_setupGRPCOpts(t *testing.T) {
 		})
 	}
 }
+
+type testServer struct {
+	extension.UnimplementedEnvoyGatewayExtensionServer
+}
+
+func (s *testServer) PostRouteModify(ctx context.Context, req *extension.PostRouteModifyRequest) (*extension.PostRouteModifyResponse, error) {
+	return &extension.PostRouteModifyResponse{
+		Route: req.Route,
+	}, nil
+}
+
+func Test_TLS(t *testing.T) {
+	testDir := "testdata"
+	caFile := testDir + "/ca.pem"
+	certFile := testDir + "/cert.pem"
+	keyFile := testDir + "/key.pem"
+
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	require.NoError(t, err)
+
+	caCert, err := os.ReadFile(caFile)
+	require.NoError(t, err)
+	caPool := x509.NewCertPool()
+	ok := caPool.AppendCertsFromPEM(caCert)
+	require.True(t, ok)
+
+	lis, err := net.Listen("tcp", "localhost:0")
+	require.NoError(t, err)
+	defer lis.Close()
+
+	port := lis.Addr().(*net.TCPAddr).Port
+	server := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{cert},
+		ClientAuth:   tls.NoClientCert,
+	})))
+	extension.RegisterEnvoyGatewayExtensionServer(server, &testServer{})
+	go func() {
+		_ = server.Serve(lis)
+		defer server.GracefulStop()
+	}()
+
+	extManager := &egv1a1.ExtensionManager{
+		Service: &egv1a1.ExtensionService{
+			BackendEndpoint: egv1a1.BackendEndpoint{
+				IP: &egv1a1.IPEndpoint{
+					Address: "localhost",
+					Port:    int32(port),
+				},
+			},
+			TLS: &egv1a1.ExtensionTLS{
+				CertificateRef: gwapiv1.SecretObjectReference{
+					Name:      "cert",
+					Namespace: ptr.To(gwapiv1.Namespace("default")),
+				},
+			},
+		},
+	}
+
+	certData, err := os.ReadFile(certFile)
+	require.NoError(t, err)
+	keyData, err := os.ReadFile(keyFile)
+	require.NoError(t, err)
+
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "cert",
+			Namespace: "default",
+		},
+		Type: corev1.SecretTypeTLS,
+		Data: map[string][]byte{
+			corev1.TLSCertKey:       certData,
path: internal/extension/registry/extension_manager_test.go
user: login: sapirpol
body: Thanks! I updated the secret to include only the necessary data
created_at: 2025-04-07T10:14:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: guydc
created_at: 2025-03-26T14:36:17Z
body: cc @nareddyt 
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-26T14:46:42Z
body: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/5613?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 `60.00000%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 65.23%. Comparing base [(`096cb8d`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/096cb8d83d1418ed06dfb268b67c41343011b058?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`f9a3f16`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/f9a3f164c8b2475a21c41f16e3587e80d6d6c76d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/envoyproxy/gateway/pull/5613?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/extension/registry/extension\_manager.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/5613?src=pr&el=tree&filepath=internal%2Fextension%2Fregistry%2Fextension_manager.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZXh0ZW5zaW9uL3JlZ2lzdHJ5L2V4dGVuc2lvbl9tYW5hZ2VyLmdv) | 60.00% | [10 Missing and 4 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/5613?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

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


```diff
@@            Coverage Diff             @@
##             main    #5613      +/-   ##
==========================================
+ Coverage   65.19%   65.23%   +0.03%     
==========================================
  Files         214      214              
  Lines       34321    34339      +18     
==========================================
+ Hits        22377    22401      +24     
+ Misses      10591    10578      -13     
- Partials     1353     1360       +7     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/5613?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>
author_association: NONE
user: login: sapirpol
created_at: 2025-04-02T08:59:58Z
body: @nareddyt Could you please review?
author_association: CONTRIBUTOR
user: login: sapirpol
created_at: 2025-04-06T12:27:51Z
body: > Thanks for making this PR! A few minor comments on the production code. Though the test may need another look.

Thanks! I’ve made the changes and replied back.
author_association: CONTRIBUTOR
user: login: sapirpol
created_at: 2025-04-10T07:13:59Z
body: @nareddyt Could you please re-review the PR and trigger the tests?
author_association: CONTRIBUTOR
user: login: arkodg
created_at: 2025-04-12T13:54:34Z
body: Hey @sapirpol can you rebase the go.mod file
author_association: CONTRIBUTOR
user: login: sapirpol
created_at: 2025-04-16T08:36:19Z
body: Hi @arkodg, I rebased the branch. Could you please review the PR and retrigger the tests?
author_association: CONTRIBUTOR