Просмотр исходного кода

feat(v2): harden grpc provider path and enforce opt-in runtime gate

Moritz Johner 3 месяцев назад
Родитель
Сommit
9c7c4d1e4e

+ 1 - 0
cmd/controller/root.go

@@ -141,6 +141,7 @@ var rootCmd = &cobra.Command{
 	Long:  `For more information visit https://external-secrets.io`,
 	Run: func(cmd *cobra.Command, _ []string) {
 		setupLogger()
+		clientmanager.SetV2ProvidersEnabled(enableV2Providers)
 
 		ctrlmetrics.SetUpLabelNames(enableExtendedMetricLabels)
 		esmetrics.SetUpMetrics()

+ 1 - 3
pkg/clientmanager/manager.go

@@ -370,7 +370,7 @@ func (m *Manager) Close(ctx context.Context) error {
 	// Close v1 provider clients (they don't use the pool)
 	for key, val := range m.clientMap {
 		// Only close v1 clients; v2 clients are managed by the pool
-		if key.providerType != "v2-provider" {
+		if key.providerType != "v2-provider" && key.providerType != "v2-cluster-provider" {
 			err := val.client.Close(ctx)
 			if err != nil {
 				errs = append(errs, err.Error())
@@ -438,5 +438,3 @@ func (m *Manager) shouldProcessSecret(store esv1.GenericStore, ns string) (bool,
 
 	return false, nil
 }
-
-

+ 39 - 3
providers/v2/adapter/store/client.go

@@ -17,7 +17,9 @@ package store
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"strconv"
 
 	corev1 "k8s.io/api/core/v1"
 
@@ -51,10 +53,27 @@ func (w *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemot
 	return w.v2Provider.GetSecret(ctx, ref, w.providerRef, w.sourceNamespace)
 }
 
-// GetSecretMap is not supported for v2 providers.
-// V2 providers don't have this method as it's being phased out in favor of GetAllSecrets.
 func (w *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	return nil, fmt.Errorf("GetSecretMap not supported for v2 providers")
+	secretData, err := w.v2Provider.GetSecret(ctx, ref, w.providerRef, w.sourceNamespace)
+	if err != nil {
+		return nil, err
+	}
+
+	values := make(map[string]any)
+	if err := json.Unmarshal(secretData, &values); err != nil {
+		return nil, fmt.Errorf("failed to decode secret as JSON object for extract: %w", err)
+	}
+
+	secretMap := make(map[string][]byte, len(values))
+	for key, value := range values {
+		byteValue, err := toByteValue(value)
+		if err != nil {
+			return nil, fmt.Errorf("failed to convert extracted value for key %q: %w", key, err)
+		}
+		secretMap[key] = byteValue
+	}
+
+	return secretMap, nil
 }
 
 // GetAllSecrets retrieves multiple secrets based on find criteria.
@@ -119,3 +138,20 @@ func (w *Client) Validate() (esv1.ValidationResult, error) {
 func (w *Client) Close(ctx context.Context) error {
 	return w.v2Provider.Close(ctx)
 }
+
+func toByteValue(value any) ([]byte, error) {
+	switch v := value.(type) {
+	case string:
+		return []byte(v), nil
+	case float64:
+		return []byte(strconv.FormatFloat(v, 'f', -1, 64)), nil
+	case bool:
+		return []byte(strconv.FormatBool(v)), nil
+	case nil:
+		return nil, nil
+	case map[string]any, []any:
+		return json.Marshal(v)
+	default:
+		return nil, fmt.Errorf("unsupported extracted value type: %T", v)
+	}
+}

+ 100 - 0
providers/v2/adapter/store/client_test.go

@@ -0,0 +1,100 @@
+/*
+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 store
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	pb "github.com/external-secrets/external-secrets/proto/provider"
+)
+
+type fakeV2Provider struct {
+	getSecretResponse []byte
+	getSecretErr      error
+}
+
+func (f *fakeV2Provider) GetSecret(context.Context, esv1.ExternalSecretDataRemoteRef, *pb.ProviderReference, string) ([]byte, error) {
+	return f.getSecretResponse, f.getSecretErr
+}
+
+func (f *fakeV2Provider) GetAllSecrets(context.Context, esv1.ExternalSecretFind, *pb.ProviderReference, string) (map[string][]byte, error) {
+	return nil, nil
+}
+
+func (f *fakeV2Provider) PushSecret(context.Context, map[string][]byte, *pb.PushSecretData, *pb.ProviderReference, string) error {
+	return nil
+}
+
+func (f *fakeV2Provider) DeleteSecret(context.Context, *pb.PushSecretRemoteRef, *pb.ProviderReference, string) error {
+	return nil
+}
+
+func (f *fakeV2Provider) SecretExists(context.Context, *pb.PushSecretRemoteRef, *pb.ProviderReference, string) (bool, error) {
+	return false, nil
+}
+
+func (f *fakeV2Provider) Validate(context.Context, *pb.ProviderReference, string) error {
+	return nil
+}
+
+func (f *fakeV2Provider) Capabilities(context.Context, *pb.ProviderReference, string) (pb.SecretStoreCapabilities, error) {
+	return pb.SecretStoreCapabilities_READ_WRITE, nil
+}
+
+func (f *fakeV2Provider) Close(context.Context) error {
+	return nil
+}
+
+func TestGetSecretMap(t *testing.T) {
+	t.Run("converts JSON object to byte map", func(t *testing.T) {
+		provider := &fakeV2Provider{
+			getSecretResponse: []byte(`{"foo":"bar","num":42,"obj":{"nested":"value"}}`),
+		}
+		client := NewClient(provider, &pb.ProviderReference{Name: "provider"}, "default")
+
+		secretMap, err := client.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "sample"})
+		if err != nil {
+			t.Fatalf("GetSecretMap() error = %v", err)
+		}
+
+		if string(secretMap["foo"]) != "bar" {
+			t.Fatalf("expected foo=bar, got %q", string(secretMap["foo"]))
+		}
+		if string(secretMap["num"]) != "42" {
+			t.Fatalf("expected num=42, got %q", string(secretMap["num"]))
+		}
+		if string(secretMap["obj"]) != `{"nested":"value"}` {
+			t.Fatalf("expected obj JSON value, got %q", string(secretMap["obj"]))
+		}
+	})
+
+	t.Run("returns error for non JSON object payload", func(t *testing.T) {
+		provider := &fakeV2Provider{
+			getSecretResponse: []byte(`"plain-string"`),
+		}
+		client := NewClient(provider, &pb.ProviderReference{Name: "provider"}, "default")
+
+		_, err := client.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{Key: "sample"})
+		if err == nil {
+			t.Fatal("expected error, got nil")
+		}
+		if !strings.Contains(err.Error(), "failed to decode secret as JSON object for extract") {
+			t.Fatalf("expected JSON decode error, got %v", err)
+		}
+	})
+}

+ 24 - 2
providers/v2/adapter/store/server.go

@@ -45,7 +45,7 @@ type ProviderMapping map[schema.GroupVersionKind]esv1.ProviderInterface
 
 // SpecMapper maps a provider reference to a SecretStoreSpec.
 // This is used to create a synthetic store for the v1 provider.
-type SpecMapper func(ref *pb.ProviderReference) (*esv1.SecretStoreSpec, error)
+type SpecMapper func(ref *pb.ProviderReference, sourceNamespace string) (*esv1.SecretStoreSpec, error)
 
 // NewServer creates a new AdapterServer that wraps v1 providers and generators.
 func NewServer(kubeClient client.Client, resourceMapping ProviderMapping, specMapping SpecMapper) *Server {
@@ -85,7 +85,7 @@ func (s *Server) getClient(ctx context.Context, ref *pb.ProviderReference, names
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
 
-	spec, err := s.specMapper(ref)
+	spec, err := s.specMapper(ref, namespace)
 	if err != nil {
 		return nil, fmt.Errorf("failed to map provider reference to spec: %w", err)
 	}
@@ -106,6 +106,9 @@ func (s *Server) GetSecret(ctx context.Context, req *pb.GetSecretRequest) (*pb.G
 	if req == nil || req.RemoteRef == nil {
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get client: %w", err)
@@ -140,6 +143,9 @@ func (s *Server) PushSecret(ctx context.Context, req *pb.PushSecretRequest) (*pb
 	if req == nil || req.PushSecretData == nil {
 		return nil, fmt.Errorf("request or push secret data is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -174,6 +180,9 @@ func (s *Server) DeleteSecret(ctx context.Context, req *pb.DeleteSecretRequest)
 	if req == nil || req.RemoteRef == nil {
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -200,6 +209,9 @@ func (s *Server) SecretExists(ctx context.Context, req *pb.SecretExistsRequest)
 	if req == nil || req.RemoteRef == nil {
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -229,6 +241,9 @@ func (s *Server) GetAllSecrets(ctx context.Context, req *pb.GetAllSecretsRequest
 	if req == nil || req.Find == nil {
 		return nil, fmt.Errorf("request or find criteria is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -332,6 +347,13 @@ func (s *Server) Capabilities(_ context.Context, req *pb.CapabilitiesRequest) (*
 	}, nil
 }
 
+func validateSourceNamespace(sourceNamespace string) error {
+	if sourceNamespace == "" {
+		return fmt.Errorf("source namespace is required")
+	}
+	return nil
+}
+
 // pushSecretData implements esv1.PushSecretData.
 type pushSecretData struct {
 	property  string

+ 24 - 2
providers/v2/adapter/v1_to_v2.go

@@ -59,7 +59,7 @@ type GeneratorMapping map[schema.GroupVersionKind]genv1alpha1.Generator
 
 // SpecMapper maps a provider reference to a SecretStoreSpec.
 // This is used to create a synthetic store for the v1 provider.
-type SpecMapper func(ref *pb.ProviderReference) (*esv1.SecretStoreSpec, error)
+type SpecMapper func(ref *pb.ProviderReference, sourceNamespace string) (*esv1.SecretStoreSpec, error)
 
 // NewAdapterServer creates a new V1AdapterServer that wraps v1 providers and generators.
 func NewAdapterServer(kubeClient client.Client, scheme *runtime.Scheme, resourceMapping ProviderMapping, specMapping SpecMapper, generatorMapping GeneratorMapping) *V1AdapterServer {
@@ -101,7 +101,7 @@ func (s *V1AdapterServer) getClient(ctx context.Context, ref *pb.ProviderReferen
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
 
-	spec, err := s.specMapper(ref)
+	spec, err := s.specMapper(ref, namespace)
 	if err != nil {
 		return nil, fmt.Errorf("failed to map provider reference to spec: %w", err)
 	}
@@ -122,6 +122,9 @@ func (s *V1AdapterServer) GetSecret(ctx context.Context, req *pb.GetSecretReques
 	if req == nil || req.RemoteRef == nil {
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
 		return nil, fmt.Errorf("failed to get client: %w", err)
@@ -156,6 +159,9 @@ func (s *V1AdapterServer) PushSecret(ctx context.Context, req *pb.PushSecretRequ
 	if req == nil || req.PushSecretData == nil {
 		return nil, fmt.Errorf("request or push secret data is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -190,6 +196,9 @@ func (s *V1AdapterServer) DeleteSecret(ctx context.Context, req *pb.DeleteSecret
 	if req == nil || req.RemoteRef == nil {
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -216,6 +225,9 @@ func (s *V1AdapterServer) SecretExists(ctx context.Context, req *pb.SecretExists
 	if req == nil || req.RemoteRef == nil {
 		return nil, fmt.Errorf("request or remote ref is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -245,6 +257,9 @@ func (s *V1AdapterServer) GetAllSecrets(ctx context.Context, req *pb.GetAllSecre
 	if req == nil || req.Find == nil {
 		return nil, fmt.Errorf("request or find criteria is nil")
 	}
+	if err := validateSourceNamespace(req.SourceNamespace); err != nil {
+		return nil, err
+	}
 
 	client, err := s.getClient(ctx, req.ProviderRef, req.SourceNamespace)
 	if err != nil {
@@ -348,6 +363,13 @@ func (s *V1AdapterServer) Capabilities(_ context.Context, req *pb.CapabilitiesRe
 	}, nil
 }
 
+func validateSourceNamespace(sourceNamespace string) error {
+	if sourceNamespace == "" {
+		return fmt.Errorf("source namespace is required")
+	}
+	return nil
+}
+
 // pushSecretData implements esv1.PushSecretData.
 type pushSecretData struct {
 	property  string

+ 7 - 4
providers/v2/aws/config.go

@@ -28,14 +28,18 @@ import (
 
 // GetSpecMapper returns the spec mapper function for the AWS provider.
 // This function converts v2 ProviderReference to v1 SecretStoreSpec.
-func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.SecretStoreSpec, error) {
-	return func(ref *pb.ProviderReference) (*v1.SecretStoreSpec, error) {
+func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference, string) (*v1.SecretStoreSpec, error) {
+	return func(ref *pb.ProviderReference, sourceNamespace string) (*v1.SecretStoreSpec, error) {
 		if ref.Kind != awsv2alpha1.SecretsManagerKind {
 			return nil, fmt.Errorf("unsupported provider kind: %s", ref.Kind)
 		}
+		namespace := ref.Namespace
+		if namespace == "" {
+			namespace = sourceNamespace
+		}
 		var awsProvider awsv2alpha1.SecretsManager
 		err := kubeClient.Get(context.Background(), client.ObjectKey{
-			Namespace: ref.Namespace,
+			Namespace: namespace,
 			Name:      ref.Name,
 		}, &awsProvider)
 		if err != nil {
@@ -59,4 +63,3 @@ func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.Se
 		}, nil
 	}
 }
-

+ 15 - 6
providers/v2/common/grpc/pool.go

@@ -16,6 +16,8 @@ package grpc
 
 import (
 	"context"
+	"crypto/sha256"
+	"encoding/hex"
 	"fmt"
 	"sync"
 	"time"
@@ -312,7 +314,8 @@ func (p *ConnectionPool) isConnectionValid(pooled *pooledConnection) bool {
 // connectionKey generates a unique key for caching connections.
 func (p *ConnectionPool) connectionKey(address string, tlsConfig *TLSConfig) string {
 	if tlsConfig != nil {
-		return fmt.Sprintf("%s-tls", address)
+		sum := sha256.Sum256(append(append(append([]byte{}, tlsConfig.CACert...), tlsConfig.ClientCert...), tlsConfig.ClientKey...))
+		return fmt.Sprintf("%s|%s-tls", address, hex.EncodeToString(sum[:8]))
 	}
 	return fmt.Sprintf("%s-insecure", address)
 }
@@ -320,7 +323,13 @@ func (p *ConnectionPool) connectionKey(address string, tlsConfig *TLSConfig) str
 // parseConnectionKey extracts address and TLS status from a connection key.
 func (p *ConnectionPool) parseConnectionKey(key string) (address string, tlsEnabled bool) {
 	if len(key) > 4 && key[len(key)-4:] == "-tls" {
-		return key[:len(key)-4], true
+		trimmed := key[:len(key)-4]
+		for i := 0; i < len(trimmed); i++ {
+			if trimmed[i] == '|' {
+				return trimmed[:i], true
+			}
+		}
+		return trimmed, true
 	}
 	if len(key) > 9 && key[len(key)-9:] == "-insecure" {
 		return key[:len(key)-9], false
@@ -345,17 +354,17 @@ func (p *ConnectionPool) updatePoolMetrics() {
 	for key, pooled := range p.connections {
 		pooled.mu.Lock()
 		address, tlsEnabled := p.parseConnectionKey(key)
-		
+
 		statKey := key
 		s := stats[statKey]
 		s.total++
-		
+
 		if pooled.references > 0 {
 			s.active++
 		} else {
 			s.idle++
 		}
-		
+
 		stats[statKey] = s
 
 		// Record connection age and idle time
@@ -363,7 +372,7 @@ func (p *ConnectionPool) updatePoolMetrics() {
 		if pooled.references == 0 {
 			poolMetrics.RecordConnectionIdle(address, tlsEnabled, now.Sub(pooled.lastUsed))
 		}
-		
+
 		pooled.mu.Unlock()
 	}
 

+ 7 - 4
providers/v2/fake/config.go

@@ -26,11 +26,15 @@ import (
 
 // GetSpecMapper returns the spec mapper function for the Fake provider.
 // This function converts v2 ProviderReference to v1 SecretStoreSpec.
-func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.SecretStoreSpec, error) {
-	return func(ref *pb.ProviderReference) (*v1.SecretStoreSpec, error) {
+func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference, string) (*v1.SecretStoreSpec, error) {
+	return func(ref *pb.ProviderReference, sourceNamespace string) (*v1.SecretStoreSpec, error) {
+		namespace := ref.Namespace
+		if namespace == "" {
+			namespace = sourceNamespace
+		}
 		var fakeProvider fakev2alpha1.Fake
 		err := kubeClient.Get(context.Background(), client.ObjectKey{
-			Namespace: ref.Namespace,
+			Namespace: namespace,
 			Name:      ref.Name,
 		}, &fakeProvider)
 		if err != nil {
@@ -43,4 +47,3 @@ func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.Se
 		}, nil
 	}
 }
-

+ 7 - 4
providers/v2/kubernetes/config.go

@@ -27,11 +27,15 @@ import (
 
 // GetSpecMapper returns the spec mapper function for the Kubernetes provider.
 // This function converts v2 ProviderReference to v1 SecretStoreSpec.
-func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.SecretStoreSpec, error) {
-	return func(ref *pb.ProviderReference) (*v1.SecretStoreSpec, error) {
+func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference, string) (*v1.SecretStoreSpec, error) {
+	return func(ref *pb.ProviderReference, sourceNamespace string) (*v1.SecretStoreSpec, error) {
+		namespace := ref.Namespace
+		if namespace == "" {
+			namespace = sourceNamespace
+		}
 		var kubernetesProvider k8sv2alpha1.Kubernetes
 		err := kubeClient.Get(context.Background(), client.ObjectKey{
-			Namespace: ref.Namespace,
+			Namespace: namespace,
 			Name:      ref.Name,
 		}, &kubernetesProvider)
 		if err != nil {
@@ -44,4 +48,3 @@ func GetSpecMapper(kubeClient client.Client) func(*pb.ProviderReference) (*v1.Se
 		}, nil
 	}
 }
-

+ 25 - 4
runtime/clientmanager/manager.go

@@ -24,6 +24,7 @@ import (
 	"regexp"
 	"strings"
 	"sync"
+	"sync/atomic"
 
 	"github.com/go-logr/logr"
 	v1 "k8s.io/api/core/v1"
@@ -45,6 +46,7 @@ const (
 	errSecretStoreNotReady   = "%s %q is not ready"
 	errClusterStoreMismatch  = "using cluster store %q is not allowed from namespace %q: denied by spec.condition"
 	errClusterProviderDenied = "using ClusterProvider %q is not allowed from namespace %q: denied by spec.conditions"
+	errV2ProvidersDisabled   = "v2 provider support is disabled, refusing %s %q (enable with --enable-v2-providers)"
 )
 
 var (
@@ -54,8 +56,19 @@ var (
 	globalV2ConnectionPool     *grpc.ConnectionPool
 	globalV2ConnectionPoolOnce sync.Once
 	globalV2ConnectionPoolLog  logr.Logger
+	v2ProvidersEnabled         atomic.Bool
 )
 
+// SetV2ProvidersEnabled toggles support for experimental v2 Provider and ClusterProvider references.
+func SetV2ProvidersEnabled(enabled bool) {
+	v2ProvidersEnabled.Store(enabled)
+}
+
+// V2ProvidersEnabled reports whether v2 Provider and ClusterProvider references are allowed.
+func V2ProvidersEnabled() bool {
+	return v2ProvidersEnabled.Load()
+}
+
 // initGlobalV2ConnectionPool initializes the global connection pool for v2 providers.
 // This is called once on first use via sync.Once.
 func initGlobalV2ConnectionPool() {
@@ -169,15 +182,23 @@ func (m *Manager) GetFromStore(ctx context.Context, store esv1.GenericStore, nam
 // Do not close the client returned from this func, instead close
 // the manager once you're done with recinciling the external secret.
 func (m *Manager) Get(ctx context.Context, storeRef esv1.SecretStoreRef, namespace string, sourceRef *esv1.StoreGeneratorSourceRef) (esv1.SecretsClient, error) {
+	if sourceRef != nil && sourceRef.SecretStoreRef != nil {
+		storeRef = *sourceRef.SecretStoreRef
+	}
+
 	if storeRef.Kind == esv1.ProviderKindStr {
+		if !V2ProvidersEnabled() {
+			return nil, fmt.Errorf(errV2ProvidersDisabled, storeRef.Kind, storeRef.Name)
+		}
 		return m.getV2ProviderClient(ctx, storeRef.Name, namespace)
 	}
 	if storeRef.Kind == esv1.ClusterProviderKindStr {
+		if !V2ProvidersEnabled() {
+			return nil, fmt.Errorf(errV2ProvidersDisabled, storeRef.Kind, storeRef.Name)
+		}
 		return m.getV2ClusterProviderClient(ctx, storeRef.Name, namespace)
 	}
-	if sourceRef != nil && sourceRef.SecretStoreRef != nil {
-		storeRef = *sourceRef.SecretStoreRef
-	}
+
 	store, err := m.getStore(ctx, &storeRef, namespace)
 	if err != nil {
 		return nil, err
@@ -500,7 +521,7 @@ func (m *Manager) Close(ctx context.Context) error {
 	// Close v1 provider clients (they don't use the pool)
 	for key, val := range m.clientMap {
 		// Only close v1 clients; v2 clients are managed by the pool
-		if key.providerType != "v2-provider" {
+		if key.providerType != "v2-provider" && key.providerType != "v2-cluster-provider" {
 			err := val.client.Close(ctx)
 			if err != nil {
 				errs = append(errs, err.Error())

+ 39 - 0
runtime/clientmanager/manager_test.go

@@ -411,6 +411,45 @@ func TestShouldProcessSecret(t *testing.T) {
 	}
 }
 
+func TestGetV2ProviderFeatureGate(t *testing.T) {
+	previous := V2ProvidersEnabled()
+	SetV2ProvidersEnabled(false)
+	t.Cleanup(func() {
+		SetV2ProvidersEnabled(previous)
+	})
+
+	mgr := NewManager(nil, "default", false)
+
+	_, err := mgr.Get(context.Background(), esv1.SecretStoreRef{
+		Name: "example-provider",
+		Kind: esv1.ProviderKindStr,
+	}, "default", nil)
+	require.Error(t, err)
+	assert.ErrorContains(t, err, "v2 provider support is disabled")
+}
+
+func TestGetV2ProviderFeatureGateFromSourceRef(t *testing.T) {
+	previous := V2ProvidersEnabled()
+	SetV2ProvidersEnabled(false)
+	t.Cleanup(func() {
+		SetV2ProvidersEnabled(previous)
+	})
+
+	mgr := NewManager(nil, "default", false)
+
+	_, err := mgr.Get(context.Background(), esv1.SecretStoreRef{
+		Name: "example-store",
+		Kind: esv1.SecretStoreKind,
+	}, "default", &esv1.StoreGeneratorSourceRef{
+		SecretStoreRef: &esv1.SecretStoreRef{
+			Name: "example-provider",
+			Kind: esv1.ProviderKindStr,
+		},
+	})
+	require.Error(t, err)
+	assert.ErrorContains(t, err, "v2 provider support is disabled")
+}
+
 type WrapProvider struct {
 	newClientFunc func(
 		context.Context,