Browse Source

feat(release): Support reading values from kubernetes CRD (#6211)

Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Signed-off-by: Alexander Chernov <alexander@chernov.it>
Alexander Chernov 1 week ago
parent
commit
eb58489f99

+ 124 - 0
apis/externalsecrets/v1/secretstore_crd_types.go

@@ -0,0 +1,124 @@
+/*
+Copyright © The ESO 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
+
+    https://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 v1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// CRDProviderResource identifies a Kubernetes resource (CRD or core) by its
+// full API coordinates: group, version and kind.
+type CRDProviderResource struct {
+	// Group is the API group of the resource. Use "" (empty string) for core
+	// Kubernetes resources such as ConfigMap; use e.g. "config.example.io"
+	// for a CRD. The field is required to be present in the manifest — write
+	// `group: ""` explicitly for core resources so typos fail at admission
+	// time rather than later at discovery.
+	// +kubebuilder:validation:Required
+	Group string `json:"group"`
+
+	// Version is the API version of the resource (e.g. "v1alpha1").
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinLength=1
+	Version string `json:"version"`
+
+	// Kind is the Kubernetes resource kind (e.g. "MyCustomResource").
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinLength=1
+	Kind string `json:"kind"`
+}
+
+// CRDProviderWhitelistRule defines a single allow rule for CRD reads.
+type CRDProviderWhitelistRule struct {
+	// Name is an optional regular expression matched against the bare object name.
+	// For both SecretStore and ClusterSecretStore this is always the object name
+	// without any namespace prefix (e.g. "my-db-spec", not "prod/my-db-spec").
+	// +optional
+	Name string `json:"name,omitempty"`
+
+	// Namespace is an optional regular expression matched against the namespace of
+	// the object. Applies only when a ClusterSecretStore is used; it is ignored
+	// for SecretStore (where the namespace is fixed to the store namespace).
+	// +optional
+	Namespace string `json:"namespace,omitempty"`
+
+	// Properties is an optional list of regular expressions matched against
+	// requested property keys (for example: "spec.secretValue").
+	// +optional
+	Properties []string `json:"properties,omitempty"`
+}
+
+// CRDProviderWhitelist configures allow-list rules for CRD reads.
+// If any rules are present, a request must satisfy ALL non-empty filters of at
+// least one rule; requests that match no rule are denied.
+type CRDProviderWhitelist struct {
+	// Rules is a list of allow rules. If rules are set, at least one rule must
+	// match for a request to be allowed.
+	// +optional
+	Rules []CRDProviderWhitelistRule `json:"rules,omitempty"`
+}
+
+// CRDProvider configures a store to fetch data from arbitrary Kubernetes
+// resources, including both custom resources (CRDs) and core API resources
+// (e.g. ConfigMap, addressed by setting resource.group to ""). Kubernetes
+// Secrets are intentionally blocked; use the Kubernetes provider for those.
+//
+// # Authentication modes
+//
+// In-cluster: set auth.serviceAccount and omit server. The server URL defaults
+// to the in-cluster API (kubernetes.default) and the controller mints a
+// short-lived token for the referenced ServiceAccount to read CRDs locally.
+//
+// Remote cluster: set server plus auth (serviceAccount, token, or cert) or
+// authRef (a kubeconfig Secret), exactly like the Kubernetes provider.
+//
+// # Remote reference keys
+//
+//   - SecretStore: the key is the object name only; '/' is not allowed. The API
+//     namespace is always the store namespace, never part of the key.
+//   - ClusterSecretStore: use "namespace/objectName" to read a namespaced CR;
+//     a key without '/' addresses a cluster-scoped CR by object name. For
+//     dataFrom Find with a namespaced kind, listing spans all namespaces and
+//     result keys are "namespace/objectName".
+//
+// +kubebuilder:validation:AtMostOneOf=auth;authRef
+// +kubebuilder:validation:XValidation:rule="has(self.auth) || has(self.authRef)",message="one of auth or authRef is required"
+type CRDProvider struct {
+	// Server configures the Kubernetes API address and TLS trust, same as the
+	// Kubernetes provider. When omitted, the URL defaults to the in-cluster API.
+	// +optional
+	Server KubernetesServer `json:"server,omitempty"`
+
+	// Auth configures authentication to the Kubernetes API, same as the
+	// Kubernetes provider. Required when Server.URL is set (unless using AuthRef).
+	// +optional
+	Auth *KubernetesAuth `json:"auth,omitempty"`
+
+	// AuthRef references a Secret containing a kubeconfig. Same semantics as the
+	// Kubernetes provider.
+	// +optional
+	AuthRef *esmeta.SecretKeySelector `json:"authRef,omitempty"`
+
+	// Resource identifies the CRD by its API group, version and kind.
+	// +kubebuilder:validation:Required
+	Resource CRDProviderResource `json:"resource"`
+
+	// Whitelist optionally restricts which object names and requested properties
+	// are allowed to be read.
+	// +optional
+	Whitelist *CRDProviderWhitelist `json:"whitelist,omitempty"`
+}

+ 7 - 4
apis/externalsecrets/v1/secretstore_kubernetes_types.go

@@ -31,7 +31,7 @@ type KubernetesServer struct {
 	// +optional
 	CABundle []byte `json:"caBundle,omitempty"`
 
-	// see: https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider
+	// see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider
 	// +optional
 	CAProvider *CAProvider `json:"caProvider,omitempty"`
 }
@@ -78,11 +78,14 @@ type KubernetesAuth struct {
 
 // CertAuth defines certificate-based authentication configuration for Kubernetes.
 type CertAuth struct {
-	ClientCert esmeta.SecretKeySelector `json:"clientCert,omitempty"`
-	ClientKey  esmeta.SecretKeySelector `json:"clientKey,omitempty"`
+	// +kubebuilder:validation:Required
+	ClientCert esmeta.SecretKeySelector `json:"clientCert"`
+	// +kubebuilder:validation:Required
+	ClientKey esmeta.SecretKeySelector `json:"clientKey"`
 }
 
 // TokenAuth defines token-based authentication configuration for Kubernetes.
 type TokenAuth struct {
-	BearerToken esmeta.SecretKeySelector `json:"bearerToken,omitempty"`
+	// +kubebuilder:validation:Required
+	BearerToken esmeta.SecretKeySelector `json:"bearerToken"`
 }

+ 8 - 0
apis/externalsecrets/v1/secretstore_types.go

@@ -136,6 +136,14 @@ type SecretStoreProvider struct {
 	// +optional
 	Kubernetes *KubernetesProvider `json:"kubernetes,omitempty"`
 
+	// CRD configures this store to sync secrets from arbitrary Kubernetes resources,
+	// including both custom resources (CRDs) and core API resources. Resources are
+	// selected by API group, version and kind, where group can be "" (empty string)
+	// for core resources such as ConfigMap. Reading the core v1 Secret is
+	// intentionally blocked — use the Kubernetes provider for that.
+	// +optional
+	CRD *CRDProvider `json:"crd,omitempty"`
+
 	// Fake configures a store with static key/value pairs
 	// +optional
 	Fake *FakeProvider `json:"fake,omitempty"`

+ 94 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -802,6 +802,95 @@ func (in *CAProvider) DeepCopy() *CAProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CRDProvider) DeepCopyInto(out *CRDProvider) {
+	*out = *in
+	in.Server.DeepCopyInto(&out.Server)
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(KubernetesAuth)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.AuthRef != nil {
+		in, out := &in.AuthRef, &out.AuthRef
+		*out = new(apismetav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+	out.Resource = in.Resource
+	if in.Whitelist != nil {
+		in, out := &in.Whitelist, &out.Whitelist
+		*out = new(CRDProviderWhitelist)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDProvider.
+func (in *CRDProvider) DeepCopy() *CRDProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(CRDProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CRDProviderResource) DeepCopyInto(out *CRDProviderResource) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDProviderResource.
+func (in *CRDProviderResource) DeepCopy() *CRDProviderResource {
+	if in == nil {
+		return nil
+	}
+	out := new(CRDProviderResource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CRDProviderWhitelist) DeepCopyInto(out *CRDProviderWhitelist) {
+	*out = *in
+	if in.Rules != nil {
+		in, out := &in.Rules, &out.Rules
+		*out = make([]CRDProviderWhitelistRule, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDProviderWhitelist.
+func (in *CRDProviderWhitelist) DeepCopy() *CRDProviderWhitelist {
+	if in == nil {
+		return nil
+	}
+	out := new(CRDProviderWhitelist)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CRDProviderWhitelistRule) DeepCopyInto(out *CRDProviderWhitelistRule) {
+	*out = *in
+	if in.Properties != nil {
+		in, out := &in.Properties, &out.Properties
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRDProviderWhitelistRule.
+func (in *CRDProviderWhitelistRule) DeepCopy() *CRDProviderWhitelistRule {
+	if in == nil {
+		return nil
+	}
+	out := new(CRDProviderWhitelistRule)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CSMAuth) DeepCopyInto(out *CSMAuth) {
 	*out = *in
@@ -3884,6 +3973,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(KubernetesProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.CRD != nil {
+		in, out := &in.CRD, &out.CRD
+		*out = new(CRDProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Fake != nil {
 		in, out := &in.Fake, &out.Fake
 		*out = new(FakeProvider)

+ 303 - 1
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -1871,6 +1871,303 @@ spec:
                     - auth
                     - url
                     type: object
+                  crd:
+                    description: |-
+                      CRD configures this store to sync secrets from arbitrary Kubernetes resources,
+                      including both custom resources (CRDs) and core API resources. Resources are
+                      selected by API group, version and kind, where group can be "" (empty string)
+                      for core resources such as ConfigMap. Reading the core v1 Secret is
+                      intentionally blocked — use the Kubernetes provider for that.
+                    properties:
+                      auth:
+                        description: |-
+                          Auth configures authentication to the Kubernetes API, same as the
+                          Kubernetes provider. Required when Server.URL is set (unless using AuthRef).
+                        maxProperties: 1
+                        minProperties: 1
+                        properties:
+                          cert:
+                            description: has both clientCert and clientKey as secretKeySelector
+                            properties:
+                              clientCert:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              clientKey:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - clientCert
+                            - clientKey
+                            type: object
+                          serviceAccount:
+                            description: points to a service account that should be
+                              used for authentication
+                            properties:
+                              audiences:
+                                description: |-
+                                  Audience specifies the `aud` claim for the service account token
+                                  If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                  then this audiences will be appended to the list
+                                items:
+                                  type: string
+                                type: array
+                              name:
+                                description: The name of the ServiceAccount resource
+                                  being referred to.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to.
+                                  Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            required:
+                            - name
+                            type: object
+                          token:
+                            description: use static token to authenticate with
+                            properties:
+                              bearerToken:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - bearerToken
+                            type: object
+                        type: object
+                      authRef:
+                        description: |-
+                          AuthRef references a Secret containing a kubeconfig. Same semantics as the
+                          Kubernetes provider.
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace of the Secret resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        type: object
+                      resource:
+                        description: Resource identifies the CRD by its API group,
+                          version and kind.
+                        properties:
+                          group:
+                            description: |-
+                              Group is the API group of the resource. Use "" (empty string) for core
+                              Kubernetes resources such as ConfigMap; use e.g. "config.example.io"
+                              for a CRD. The field is required to be present in the manifest — write
+                              `group: ""` explicitly for core resources so typos fail at admission
+                              time rather than later at discovery.
+                            type: string
+                          kind:
+                            description: Kind is the Kubernetes resource kind (e.g.
+                              "MyCustomResource").
+                            minLength: 1
+                            type: string
+                          version:
+                            description: Version is the API version of the resource
+                              (e.g. "v1alpha1").
+                            minLength: 1
+                            type: string
+                        required:
+                        - group
+                        - kind
+                        - version
+                        type: object
+                      server:
+                        description: |-
+                          Server configures the Kubernetes API address and TLS trust, same as the
+                          Kubernetes provider. When omitted, the URL defaults to the in-cluster API.
+                        properties:
+                          caBundle:
+                            description: CABundle is a base64-encoded CA certificate
+                            format: byte
+                            type: string
+                          caProvider:
+                            description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
+                            properties:
+                              key:
+                                description: The key where the CA certificate can
+                                  be found in the Secret or ConfigMap.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[-._a-zA-Z0-9]+$
+                                type: string
+                              name:
+                                description: The name of the object located at the
+                                  provider type.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  The namespace the Provider type is in.
+                                  Can only be defined when used in a ClusterSecretStore.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                              type:
+                                description: The type of provider to use such as "Secret",
+                                  or "ConfigMap".
+                                enum:
+                                - Secret
+                                - ConfigMap
+                                type: string
+                            required:
+                            - name
+                            - type
+                            type: object
+                          url:
+                            default: kubernetes.default
+                            description: configures the Kubernetes server Address.
+                            type: string
+                        type: object
+                      whitelist:
+                        description: |-
+                          Whitelist optionally restricts which object names and requested properties
+                          are allowed to be read.
+                        properties:
+                          rules:
+                            description: |-
+                              Rules is a list of allow rules. If rules are set, at least one rule must
+                              match for a request to be allowed.
+                            items:
+                              description: CRDProviderWhitelistRule defines a single
+                                allow rule for CRD reads.
+                              properties:
+                                name:
+                                  description: |-
+                                    Name is an optional regular expression matched against the bare object name.
+                                    For both SecretStore and ClusterSecretStore this is always the object name
+                                    without any namespace prefix (e.g. "my-db-spec", not "prod/my-db-spec").
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace is an optional regular expression matched against the namespace of
+                                    the object. Applies only when a ClusterSecretStore is used; it is ignored
+                                    for SecretStore (where the namespace is fixed to the store namespace).
+                                  type: string
+                                properties:
+                                  description: |-
+                                    Properties is an optional list of regular expressions matched against
+                                    requested property keys (for example: "spec.secretValue").
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            type: array
+                        type: object
+                    required:
+                    - resource
+                    type: object
+                    x-kubernetes-validations:
+                    - message: one of auth or authRef is required
+                      rule: has(self.auth) || has(self.authRef)
+                    - message: at most one of the fields in [auth authRef] may be
+                        set
+                      rule: '[has(self.auth),has(self.authRef)].filter(x,x==true).size()
+                        <= 1'
                   delinea:
                     description: |-
                       Delinea DevOps Secrets Vault
@@ -3727,6 +4024,9 @@ spec:
                                     pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                     type: string
                                 type: object
+                            required:
+                            - clientCert
+                            - clientKey
                             type: object
                           serviceAccount:
                             description: points to a service account that should be
@@ -3790,6 +4090,8 @@ spec:
                                     pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                     type: string
                                 type: object
+                            required:
+                            - bearerToken
                             type: object
                         type: object
                       authRef:
@@ -3835,7 +4137,7 @@ spec:
                             format: byte
                             type: string
                           caProvider:
-                            description: 'see: https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider'
+                            description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
                             properties:
                               key:
                                 description: The key where the CA certificate can

+ 303 - 1
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -1871,6 +1871,303 @@ spec:
                     - auth
                     - url
                     type: object
+                  crd:
+                    description: |-
+                      CRD configures this store to sync secrets from arbitrary Kubernetes resources,
+                      including both custom resources (CRDs) and core API resources. Resources are
+                      selected by API group, version and kind, where group can be "" (empty string)
+                      for core resources such as ConfigMap. Reading the core v1 Secret is
+                      intentionally blocked — use the Kubernetes provider for that.
+                    properties:
+                      auth:
+                        description: |-
+                          Auth configures authentication to the Kubernetes API, same as the
+                          Kubernetes provider. Required when Server.URL is set (unless using AuthRef).
+                        maxProperties: 1
+                        minProperties: 1
+                        properties:
+                          cert:
+                            description: has both clientCert and clientKey as secretKeySelector
+                            properties:
+                              clientCert:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              clientKey:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - clientCert
+                            - clientKey
+                            type: object
+                          serviceAccount:
+                            description: points to a service account that should be
+                              used for authentication
+                            properties:
+                              audiences:
+                                description: |-
+                                  Audience specifies the `aud` claim for the service account token
+                                  If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                  then this audiences will be appended to the list
+                                items:
+                                  type: string
+                                type: array
+                              name:
+                                description: The name of the ServiceAccount resource
+                                  being referred to.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to.
+                                  Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            required:
+                            - name
+                            type: object
+                          token:
+                            description: use static token to authenticate with
+                            properties:
+                              bearerToken:
+                                description: |-
+                                  SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - bearerToken
+                            type: object
+                        type: object
+                      authRef:
+                        description: |-
+                          AuthRef references a Secret containing a kubeconfig. Same semantics as the
+                          Kubernetes provider.
+                        properties:
+                          key:
+                            description: |-
+                              A key in the referenced Secret.
+                              Some instances of this field may be defaulted, in others it may be required.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[-._a-zA-Z0-9]+$
+                            type: string
+                          name:
+                            description: The name of the Secret resource being referred
+                              to.
+                            maxLength: 253
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          namespace:
+                            description: |-
+                              The namespace of the Secret resource being referred to.
+                              Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                        type: object
+                      resource:
+                        description: Resource identifies the CRD by its API group,
+                          version and kind.
+                        properties:
+                          group:
+                            description: |-
+                              Group is the API group of the resource. Use "" (empty string) for core
+                              Kubernetes resources such as ConfigMap; use e.g. "config.example.io"
+                              for a CRD. The field is required to be present in the manifest — write
+                              `group: ""` explicitly for core resources so typos fail at admission
+                              time rather than later at discovery.
+                            type: string
+                          kind:
+                            description: Kind is the Kubernetes resource kind (e.g.
+                              "MyCustomResource").
+                            minLength: 1
+                            type: string
+                          version:
+                            description: Version is the API version of the resource
+                              (e.g. "v1alpha1").
+                            minLength: 1
+                            type: string
+                        required:
+                        - group
+                        - kind
+                        - version
+                        type: object
+                      server:
+                        description: |-
+                          Server configures the Kubernetes API address and TLS trust, same as the
+                          Kubernetes provider. When omitted, the URL defaults to the in-cluster API.
+                        properties:
+                          caBundle:
+                            description: CABundle is a base64-encoded CA certificate
+                            format: byte
+                            type: string
+                          caProvider:
+                            description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
+                            properties:
+                              key:
+                                description: The key where the CA certificate can
+                                  be found in the Secret or ConfigMap.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[-._a-zA-Z0-9]+$
+                                type: string
+                              name:
+                                description: The name of the object located at the
+                                  provider type.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  The namespace the Provider type is in.
+                                  Can only be defined when used in a ClusterSecretStore.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                              type:
+                                description: The type of provider to use such as "Secret",
+                                  or "ConfigMap".
+                                enum:
+                                - Secret
+                                - ConfigMap
+                                type: string
+                            required:
+                            - name
+                            - type
+                            type: object
+                          url:
+                            default: kubernetes.default
+                            description: configures the Kubernetes server Address.
+                            type: string
+                        type: object
+                      whitelist:
+                        description: |-
+                          Whitelist optionally restricts which object names and requested properties
+                          are allowed to be read.
+                        properties:
+                          rules:
+                            description: |-
+                              Rules is a list of allow rules. If rules are set, at least one rule must
+                              match for a request to be allowed.
+                            items:
+                              description: CRDProviderWhitelistRule defines a single
+                                allow rule for CRD reads.
+                              properties:
+                                name:
+                                  description: |-
+                                    Name is an optional regular expression matched against the bare object name.
+                                    For both SecretStore and ClusterSecretStore this is always the object name
+                                    without any namespace prefix (e.g. "my-db-spec", not "prod/my-db-spec").
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace is an optional regular expression matched against the namespace of
+                                    the object. Applies only when a ClusterSecretStore is used; it is ignored
+                                    for SecretStore (where the namespace is fixed to the store namespace).
+                                  type: string
+                                properties:
+                                  description: |-
+                                    Properties is an optional list of regular expressions matched against
+                                    requested property keys (for example: "spec.secretValue").
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            type: array
+                        type: object
+                    required:
+                    - resource
+                    type: object
+                    x-kubernetes-validations:
+                    - message: one of auth or authRef is required
+                      rule: has(self.auth) || has(self.authRef)
+                    - message: at most one of the fields in [auth authRef] may be
+                        set
+                      rule: '[has(self.auth),has(self.authRef)].filter(x,x==true).size()
+                        <= 1'
                   delinea:
                     description: |-
                       Delinea DevOps Secrets Vault
@@ -3727,6 +4024,9 @@ spec:
                                     pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                     type: string
                                 type: object
+                            required:
+                            - clientCert
+                            - clientKey
                             type: object
                           serviceAccount:
                             description: points to a service account that should be
@@ -3790,6 +4090,8 @@ spec:
                                     pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                     type: string
                                 type: object
+                            required:
+                            - bearerToken
                             type: object
                         type: object
                       authRef:
@@ -3835,7 +4137,7 @@ spec:
                             format: byte
                             type: string
                           caProvider:
-                            description: 'see: https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider'
+                            description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
                             properties:
                               key:
                                 description: The key where the CA certificate can

+ 576 - 2
deploy/crds/bundle.yaml

@@ -4071,6 +4071,288 @@ spec:
                         - auth
                         - url
                       type: object
+                    crd:
+                      description: |-
+                        CRD configures this store to sync secrets from arbitrary Kubernetes resources,
+                        including both custom resources (CRDs) and core API resources. Resources are
+                        selected by API group, version and kind, where group can be "" (empty string)
+                        for core resources such as ConfigMap. Reading the core v1 Secret is
+                        intentionally blocked — use the Kubernetes provider for that.
+                      properties:
+                        auth:
+                          description: |-
+                            Auth configures authentication to the Kubernetes API, same as the
+                            Kubernetes provider. Required when Server.URL is set (unless using AuthRef).
+                          maxProperties: 1
+                          minProperties: 1
+                          properties:
+                            cert:
+                              description: has both clientCert and clientKey as secretKeySelector
+                              properties:
+                                clientCert:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                clientKey:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - clientCert
+                                - clientKey
+                              type: object
+                            serviceAccount:
+                              description: points to a service account that should be used for authentication
+                              properties:
+                                audiences:
+                                  description: |-
+                                    Audience specifies the `aud` claim for the service account token
+                                    If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                    then this audiences will be appended to the list
+                                  items:
+                                    type: string
+                                  type: array
+                                name:
+                                  description: The name of the ServiceAccount resource being referred to.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to.
+                                    Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                              required:
+                                - name
+                              type: object
+                            token:
+                              description: use static token to authenticate with
+                              properties:
+                                bearerToken:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - bearerToken
+                              type: object
+                          type: object
+                        authRef:
+                          description: |-
+                            AuthRef references a Secret containing a kubeconfig. Same semantics as the
+                            Kubernetes provider.
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace of the Secret resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          type: object
+                        resource:
+                          description: Resource identifies the CRD by its API group, version and kind.
+                          properties:
+                            group:
+                              description: |-
+                                Group is the API group of the resource. Use "" (empty string) for core
+                                Kubernetes resources such as ConfigMap; use e.g. "config.example.io"
+                                for a CRD. The field is required to be present in the manifest — write
+                                `group: ""` explicitly for core resources so typos fail at admission
+                                time rather than later at discovery.
+                              type: string
+                            kind:
+                              description: Kind is the Kubernetes resource kind (e.g. "MyCustomResource").
+                              minLength: 1
+                              type: string
+                            version:
+                              description: Version is the API version of the resource (e.g. "v1alpha1").
+                              minLength: 1
+                              type: string
+                          required:
+                            - group
+                            - kind
+                            - version
+                          type: object
+                        server:
+                          description: |-
+                            Server configures the Kubernetes API address and TLS trust, same as the
+                            Kubernetes provider. When omitted, the URL defaults to the in-cluster API.
+                          properties:
+                            caBundle:
+                              description: CABundle is a base64-encoded CA certificate
+                              format: byte
+                              type: string
+                            caProvider:
+                              description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
+                              properties:
+                                key:
+                                  description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[-._a-zA-Z0-9]+$
+                                  type: string
+                                name:
+                                  description: The name of the object located at the provider type.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    The namespace the Provider type is in.
+                                    Can only be defined when used in a ClusterSecretStore.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                                type:
+                                  description: The type of provider to use such as "Secret", or "ConfigMap".
+                                  enum:
+                                    - Secret
+                                    - ConfigMap
+                                  type: string
+                              required:
+                                - name
+                                - type
+                              type: object
+                            url:
+                              default: kubernetes.default
+                              description: configures the Kubernetes server Address.
+                              type: string
+                          type: object
+                        whitelist:
+                          description: |-
+                            Whitelist optionally restricts which object names and requested properties
+                            are allowed to be read.
+                          properties:
+                            rules:
+                              description: |-
+                                Rules is a list of allow rules. If rules are set, at least one rule must
+                                match for a request to be allowed.
+                              items:
+                                description: CRDProviderWhitelistRule defines a single allow rule for CRD reads.
+                                properties:
+                                  name:
+                                    description: |-
+                                      Name is an optional regular expression matched against the bare object name.
+                                      For both SecretStore and ClusterSecretStore this is always the object name
+                                      without any namespace prefix (e.g. "my-db-spec", not "prod/my-db-spec").
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      Namespace is an optional regular expression matched against the namespace of
+                                      the object. Applies only when a ClusterSecretStore is used; it is ignored
+                                      for SecretStore (where the namespace is fixed to the store namespace).
+                                    type: string
+                                  properties:
+                                    description: |-
+                                      Properties is an optional list of regular expressions matched against
+                                      requested property keys (for example: "spec.secretValue").
+                                    items:
+                                      type: string
+                                    type: array
+                                type: object
+                              type: array
+                          type: object
+                      required:
+                        - resource
+                      type: object
+                      x-kubernetes-validations:
+                        - message: one of auth or authRef is required
+                          rule: has(self.auth) || has(self.authRef)
+                        - message: at most one of the fields in [auth authRef] may be set
+                          rule: '[has(self.auth),has(self.authRef)].filter(x,x==true).size() <= 1'
                     delinea:
                       description: |-
                         Delinea DevOps Secrets Vault
@@ -5798,6 +6080,9 @@ spec:
                                       pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                       type: string
                                   type: object
+                              required:
+                                - clientCert
+                                - clientKey
                               type: object
                             serviceAccount:
                               description: points to a service account that should be used for authentication
@@ -5858,6 +6143,8 @@ spec:
                                       pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                       type: string
                                   type: object
+                              required:
+                                - bearerToken
                               type: object
                           type: object
                         authRef:
@@ -5901,7 +6188,7 @@ spec:
                               format: byte
                               type: string
                             caProvider:
-                              description: 'see: https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider'
+                              description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
                               properties:
                                 key:
                                   description: The key where the CA certificate can be found in the Secret or ConfigMap.
@@ -16977,6 +17264,288 @@ spec:
                         - auth
                         - url
                       type: object
+                    crd:
+                      description: |-
+                        CRD configures this store to sync secrets from arbitrary Kubernetes resources,
+                        including both custom resources (CRDs) and core API resources. Resources are
+                        selected by API group, version and kind, where group can be "" (empty string)
+                        for core resources such as ConfigMap. Reading the core v1 Secret is
+                        intentionally blocked — use the Kubernetes provider for that.
+                      properties:
+                        auth:
+                          description: |-
+                            Auth configures authentication to the Kubernetes API, same as the
+                            Kubernetes provider. Required when Server.URL is set (unless using AuthRef).
+                          maxProperties: 1
+                          minProperties: 1
+                          properties:
+                            cert:
+                              description: has both clientCert and clientKey as secretKeySelector
+                              properties:
+                                clientCert:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                clientKey:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - clientCert
+                                - clientKey
+                              type: object
+                            serviceAccount:
+                              description: points to a service account that should be used for authentication
+                              properties:
+                                audiences:
+                                  description: |-
+                                    Audience specifies the `aud` claim for the service account token
+                                    If the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity
+                                    then this audiences will be appended to the list
+                                  items:
+                                    type: string
+                                  type: array
+                                name:
+                                  description: The name of the ServiceAccount resource being referred to.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to.
+                                    Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                              required:
+                                - name
+                              type: object
+                            token:
+                              description: use static token to authenticate with
+                              properties:
+                                bearerToken:
+                                  description: |-
+                                    SecretKeySelector is a reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - bearerToken
+                              type: object
+                          type: object
+                        authRef:
+                          description: |-
+                            AuthRef references a Secret containing a kubeconfig. Same semantics as the
+                            Kubernetes provider.
+                          properties:
+                            key:
+                              description: |-
+                                A key in the referenced Secret.
+                                Some instances of this field may be defaulted, in others it may be required.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[-._a-zA-Z0-9]+$
+                              type: string
+                            name:
+                              description: The name of the Secret resource being referred to.
+                              maxLength: 253
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                              type: string
+                            namespace:
+                              description: |-
+                                The namespace of the Secret resource being referred to.
+                                Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                              maxLength: 63
+                              minLength: 1
+                              pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                              type: string
+                          type: object
+                        resource:
+                          description: Resource identifies the CRD by its API group, version and kind.
+                          properties:
+                            group:
+                              description: |-
+                                Group is the API group of the resource. Use "" (empty string) for core
+                                Kubernetes resources such as ConfigMap; use e.g. "config.example.io"
+                                for a CRD. The field is required to be present in the manifest — write
+                                `group: ""` explicitly for core resources so typos fail at admission
+                                time rather than later at discovery.
+                              type: string
+                            kind:
+                              description: Kind is the Kubernetes resource kind (e.g. "MyCustomResource").
+                              minLength: 1
+                              type: string
+                            version:
+                              description: Version is the API version of the resource (e.g. "v1alpha1").
+                              minLength: 1
+                              type: string
+                          required:
+                            - group
+                            - kind
+                            - version
+                          type: object
+                        server:
+                          description: |-
+                            Server configures the Kubernetes API address and TLS trust, same as the
+                            Kubernetes provider. When omitted, the URL defaults to the in-cluster API.
+                          properties:
+                            caBundle:
+                              description: CABundle is a base64-encoded CA certificate
+                              format: byte
+                              type: string
+                            caProvider:
+                              description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
+                              properties:
+                                key:
+                                  description: The key where the CA certificate can be found in the Secret or ConfigMap.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[-._a-zA-Z0-9]+$
+                                  type: string
+                                name:
+                                  description: The name of the object located at the provider type.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    The namespace the Provider type is in.
+                                    Can only be defined when used in a ClusterSecretStore.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                                type:
+                                  description: The type of provider to use such as "Secret", or "ConfigMap".
+                                  enum:
+                                    - Secret
+                                    - ConfigMap
+                                  type: string
+                              required:
+                                - name
+                                - type
+                              type: object
+                            url:
+                              default: kubernetes.default
+                              description: configures the Kubernetes server Address.
+                              type: string
+                          type: object
+                        whitelist:
+                          description: |-
+                            Whitelist optionally restricts which object names and requested properties
+                            are allowed to be read.
+                          properties:
+                            rules:
+                              description: |-
+                                Rules is a list of allow rules. If rules are set, at least one rule must
+                                match for a request to be allowed.
+                              items:
+                                description: CRDProviderWhitelistRule defines a single allow rule for CRD reads.
+                                properties:
+                                  name:
+                                    description: |-
+                                      Name is an optional regular expression matched against the bare object name.
+                                      For both SecretStore and ClusterSecretStore this is always the object name
+                                      without any namespace prefix (e.g. "my-db-spec", not "prod/my-db-spec").
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      Namespace is an optional regular expression matched against the namespace of
+                                      the object. Applies only when a ClusterSecretStore is used; it is ignored
+                                      for SecretStore (where the namespace is fixed to the store namespace).
+                                    type: string
+                                  properties:
+                                    description: |-
+                                      Properties is an optional list of regular expressions matched against
+                                      requested property keys (for example: "spec.secretValue").
+                                    items:
+                                      type: string
+                                    type: array
+                                type: object
+                              type: array
+                          type: object
+                      required:
+                        - resource
+                      type: object
+                      x-kubernetes-validations:
+                        - message: one of auth or authRef is required
+                          rule: has(self.auth) || has(self.authRef)
+                        - message: at most one of the fields in [auth authRef] may be set
+                          rule: '[has(self.auth),has(self.authRef)].filter(x,x==true).size() <= 1'
                     delinea:
                       description: |-
                         Delinea DevOps Secrets Vault
@@ -18704,6 +19273,9 @@ spec:
                                       pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                       type: string
                                   type: object
+                              required:
+                                - clientCert
+                                - clientKey
                               type: object
                             serviceAccount:
                               description: points to a service account that should be used for authentication
@@ -18764,6 +19336,8 @@ spec:
                                       pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
                                       type: string
                                   type: object
+                              required:
+                                - bearerToken
                               type: object
                           type: object
                         authRef:
@@ -18807,7 +19381,7 @@ spec:
                               format: byte
                               type: string
                             caProvider:
-                              description: 'see: https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider'
+                              description: 'see: https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider'
                               properties:
                                 key:
                                   description: The key where the CA certificate can be found in the Secret or ConfigMap.

+ 284 - 1
docs/api/spec.md

@@ -2153,6 +2153,269 @@ Can only be defined when used in a ClusterSecretStore.</p>
 </td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1.CRDProvider">CRDProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>CRDProvider configures a store to fetch data from arbitrary Kubernetes
+resources, including both custom resources (CRDs) and core API resources
+(e.g. ConfigMap, addressed by setting resource.group to &ldquo;&rdquo;). Kubernetes
+Secrets are intentionally blocked; use the Kubernetes provider for those.</p>
+<h1>Authentication modes</h1>
+<p>In-cluster: set auth.serviceAccount and omit server. The server URL defaults
+to the in-cluster API (kubernetes.default) and the controller mints a
+short-lived token for the referenced ServiceAccount to read CRDs locally.</p>
+<p>Remote cluster: set server plus auth (serviceAccount, token, or cert) or
+authRef (a kubeconfig Secret), exactly like the Kubernetes provider.</p>
+<h1>Remote reference keys</h1>
+<ul>
+<li>SecretStore: the key is the object name only; &lsquo;/&rsquo; is not allowed. The API
+namespace is always the store namespace, never part of the key.</li>
+<li>ClusterSecretStore: use &ldquo;namespace/objectName&rdquo; to read a namespaced CR;
+a key without &lsquo;/&rsquo; addresses a cluster-scoped CR by object name. For
+dataFrom Find with a namespaced kind, listing spans all namespaces and
+result keys are &ldquo;namespace/objectName&rdquo;.</li>
+</ul>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>server</code></br>
+<em>
+<a href="#external-secrets.io/v1.KubernetesServer">
+KubernetesServer
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Server configures the Kubernetes API address and TLS trust, same as the
+Kubernetes provider. When omitted, the URL defaults to the in-cluster API.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1.KubernetesAuth">
+KubernetesAuth
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Auth configures authentication to the Kubernetes API, same as the
+Kubernetes provider. Required when Server.URL is set (unless using AuthRef).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>authRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>AuthRef references a Secret containing a kubeconfig. Same semantics as the
+Kubernetes provider.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>resource</code></br>
+<em>
+<a href="#external-secrets.io/v1.CRDProviderResource">
+CRDProviderResource
+</a>
+</em>
+</td>
+<td>
+<p>Resource identifies the CRD by its API group, version and kind.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>whitelist</code></br>
+<em>
+<a href="#external-secrets.io/v1.CRDProviderWhitelist">
+CRDProviderWhitelist
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Whitelist optionally restricts which object names and requested properties
+are allowed to be read.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.CRDProviderResource">CRDProviderResource
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.CRDProvider">CRDProvider</a>)
+</p>
+<p>
+<p>CRDProviderResource identifies a Kubernetes resource (CRD or core) by its
+full API coordinates: group, version and kind.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>group</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Group is the API group of the resource. Use &ldquo;&rdquo; (empty string) for core
+Kubernetes resources such as ConfigMap; use e.g. &ldquo;config.example.io&rdquo;
+for a CRD. The field is required to be present in the manifest — write
+<code>group: &quot;&quot;</code> explicitly for core resources so typos fail at admission
+time rather than later at discovery.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>version</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Version is the API version of the resource (e.g. &ldquo;v1alpha1&rdquo;).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>kind</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Kind is the Kubernetes resource kind (e.g. &ldquo;MyCustomResource&rdquo;).</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.CRDProviderWhitelist">CRDProviderWhitelist
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.CRDProvider">CRDProvider</a>)
+</p>
+<p>
+<p>CRDProviderWhitelist configures allow-list rules for CRD reads.
+If any rules are present, a request must satisfy ALL non-empty filters of at
+least one rule; requests that match no rule are denied.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>rules</code></br>
+<em>
+<a href="#external-secrets.io/v1.CRDProviderWhitelistRule">
+[]CRDProviderWhitelistRule
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Rules is a list of allow rules. If rules are set, at least one rule must
+match for a request to be allowed.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1.CRDProviderWhitelistRule">CRDProviderWhitelistRule
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.CRDProviderWhitelist">CRDProviderWhitelist</a>)
+</p>
+<p>
+<p>CRDProviderWhitelistRule defines a single allow rule for CRD reads.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>name</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Name is an optional regular expression matched against the bare object name.
+For both SecretStore and ClusterSecretStore this is always the object name
+without any namespace prefix (e.g. &ldquo;my-db-spec&rdquo;, not &ldquo;prod/my-db-spec&rdquo;).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>namespace</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Namespace is an optional regular expression matched against the namespace of
+the object. Applies only when a ClusterSecretStore is used; it is ignored
+for SecretStore (where the namespace is fixed to the store namespace).</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>properties</code></br>
+<em>
+[]string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Properties is an optional list of regular expressions matched against
+requested property keys (for example: &ldquo;spec.secretValue&rdquo;).</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.CSMAuth">CSMAuth
 </h3>
 <p>
@@ -7363,6 +7626,7 @@ bool
 </h3>
 <p>
 (<em>Appears on:</em>
+<a href="#external-secrets.io/v1.CRDProvider">CRDProvider</a>, 
 <a href="#external-secrets.io/v1.KubernetesProvider">KubernetesProvider</a>)
 </p>
 <p>
@@ -7541,6 +7805,7 @@ string
 </h3>
 <p>
 (<em>Appears on:</em>
+<a href="#external-secrets.io/v1.CRDProvider">CRDProvider</a>, 
 <a href="#external-secrets.io/v1.KubernetesProvider">KubernetesProvider</a>)
 </p>
 <p>
@@ -7589,7 +7854,7 @@ CAProvider
 </td>
 <td>
 <em>(Optional)</em>
-<p>see: <a href="https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider">https://external-secrets.io/v0.4.1/spec/#external-secrets.io/v1alpha1.CAProvider</a></p>
+<p>see: <a href="https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider">https://external-secrets.io/latest/spec/#external-secrets.io/v1alpha1.CAProvider</a></p>
 </td>
 </tr>
 </tbody>
@@ -10766,6 +11031,24 @@ KubernetesProvider
 </tr>
 <tr>
 <td>
+<code>crd</code></br>
+<em>
+<a href="#external-secrets.io/v1.CRDProvider">
+CRDProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>CRD configures this store to sync secrets from arbitrary Kubernetes resources,
+including both custom resources (CRDs) and core API resources. Resources are
+selected by API group, version and kind, where group can be &ldquo;&rdquo; (empty string)
+for core resources such as ConfigMap. Reading the core v1 Secret is
+intentionally blocked — use the Kubernetes provider for that.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>fake</code></br>
 <em>
 <a href="#external-secrets.io/v1.FakeProvider">

+ 349 - 0
docs/provider/crd.md

@@ -0,0 +1,349 @@
+## Kubernetes CRD Provider
+
+External Secrets Operator can read data from arbitrary Kubernetes Custom Resources (CRDs) in the local cluster or in a remote cluster. It uses the same connection model as the [Kubernetes provider](kubernetes.md); remote-cluster access is covered in the [Remote cluster connection](#remote-cluster-connection) section below.
+
+This provider is useful when secrets or secret-like values are already managed inside CRDs (i.e. by an operator) and you want to project them into regular Kubernetes Secrets.
+
+The CRD provider is read-only.
+
+### How it works
+
+1. The provider connects to a Kubernetes API using the same connection model as the Kubernetes provider. To read the local cluster, set `auth.serviceAccount` and point `server.caProvider` at the in-cluster CA (published in every namespace as the `kube-root-ca.crt` ConfigMap); `server.url` can be omitted, as it defaults to the in-cluster API. The CA is required even in-cluster: the API server certificate is not signed by the system roots, so without it the TLS handshake fails. To read a remote cluster, set `server` plus `auth` (`serviceAccount`, `token`, or `cert`) or `authRef` (a kubeconfig Secret).
+2. It resolves the configured resource (`group`/`version`/`kind`) via Kubernetes discovery.
+3. It reads objects using the dynamic Kubernetes client.
+4. It applies optional whitelist rules before returning values.
+
+### Remote cluster connection
+
+Set `server` plus `auth` (or `authRef`) to connect to a remote Kubernetes API directly, exactly like the Kubernetes provider. The identity in `auth`/`authRef` is the identity the provider reads CRDs as; there is no separate impersonation step.
+
+Example (`ClusterSecretStore`) connecting to a remote cluster:
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ClusterSecretStore
+metadata:
+  name: crd-remote
+spec:
+  provider:
+    crd:
+      server:
+        url: https://remote-api.example.com
+        caProvider:
+          type: ConfigMap
+          name: remote-cluster-ca
+          namespace: external-secrets
+          key: ca.crt
+      auth:
+        serviceAccount:
+          name: eso-remote-connector
+          namespace: external-secrets
+      resource:
+        group: example.io
+        version: v1alpha1
+        kind: Widget
+```
+
+Notes:
+
+- `auth` accepts `serviceAccount`, `token`, or `cert`, the same options as the Kubernetes provider.
+- `authRef` can be used instead of `auth` when the connection details (a kubeconfig) come from a Secret. In that case `server` is optional because the kubeconfig already carries the API address.
+
+### SecretStore / ClusterSecretStore configuration
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: SecretStore
+metadata:
+  name: crd-store
+spec:
+  provider:
+    crd:
+      # In-cluster read: reference the cluster CA so TLS to the API server
+      # verifies. kube-root-ca.crt exists in every namespace. For a SecretStore
+      # caProvider.namespace must be omitted; server.url defaults in-cluster.
+      server:
+        caProvider:
+          type: ConfigMap
+          name: kube-root-ca.crt
+          key: ca.crt
+      auth:
+        serviceAccount:
+          name: crd-reader
+      resource:
+        group: example.io
+        version: v1alpha1
+        kind: Widget
+      # optional. If not empty, at least one rule must match the request
+      whitelist:
+        rules:
+          # Name-only rule: allows objects whose name matches this regexp
+          - name: "^app-.*$"
+
+          # Name + properties rule: both must match
+          - name: "^team-.*$"
+            properties:
+              - "^spec\\.password$"
+              - "^spec\\.username$"
+
+          # ClusterSecretStore-only rule: allows objects from matching namespaces
+          - namespace: "^(prod|staging)$"
+
+          # Properties-only rule: allows a request when the requested property matches
+          - properties:
+              - "^spec\\.password$"
+```
+
+#### Resource fields
+
+- `auth.serviceAccount`: ServiceAccount used for API access. For an in-cluster read, pair it with `server.caProvider` pointing at the `kube-root-ca.crt` ConfigMap so TLS to the API server verifies; `server.url` can be omitted (it defaults to the in-cluster API).
+- `server` + `auth`/`authRef`: Kubernetes API connection and authentication. A remote cluster needs the full `server` block; the local cluster needs only `server.caProvider` (the URL defaults in-cluster).
+- Setting `server.url` requires `auth` or `authRef`; a store that sets `server.url` without credentials is rejected at admission.
+- `resource.group`: API group of the resource (empty for core API resources).
+- `resource.version`: API version of the resource.
+- `resource.kind`: Kind of the resource.
+
+### Whitelist rules
+
+`whitelist.rules` is an array of allow rules.
+
+If `whitelist.rules` is empty or omitted, requests are allowed.
+If `whitelist.rules` is not empty, at least one rule must match.
+
+Each rule can define:
+
+- `name` (regexp, optional): matched against requested object name.
+- `namespace` (regexp, optional): matched against requested object namespace for `ClusterSecretStore`.
+- `properties` (array of regexps, optional): matched against requested property keys.
+
+Rule behavior:
+
+1. `name` only: request is allowed when object name matches.
+2. `namespace` only: request is allowed when object namespace matches (`ClusterSecretStore` only).
+3. `properties` only: request is allowed when requested property keys match.
+4. any combination of `name`, `namespace`, and `properties`: all configured fields in the same rule must match.
+
+A rule with none of `name`, `namespace`, or `properties` is invalid.
+
+#### Notes about `properties` matching
+
+- For `data[].remoteRef.property`, the requested key is that property path (for example `spec.password`).
+- For requests without a specific property (for example `GetAllSecrets`), only rules that do not require `properties` can match.
+- `namespace` matching is only enforced for `ClusterSecretStore`; `SecretStore` ignores this field.
+
+### Property Expressions (GJSON)
+
+`remoteRef.property` is evaluated using [GJSON path syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md), the same syntax used by the Kubernetes provider and the rest of ESO, so the property dialect is consistent across providers.
+
+Examples:
+
+- `spec.user`
+- `spec.password`
+- `status.0.key`
+- `status.#(key=="rotationDate").val`
+
+If the expression resolves to no value, the provider returns `property not found`.
+
+### Return values
+
+The value returned for a reference depends on how it is requested:
+
+- `remoteRef.property` set: the value at that GJSON path. A string leaf is returned unquoted (`spec.password` -> `hunter2`); objects, arrays, numbers, and booleans are returned as their raw JSON (`spec.replicas` -> `3`).
+- `remoteRef.property` omitted: the entire object is returned as JSON.
+- Map extraction (`dataFrom`, or a `remoteRef` consumed as a map): the object's top-level keys become secret keys. String values are unwrapped; non-string values keep their raw JSON.
+- `dataFrom.find`: each matching object is returned as JSON, keyed by object name (`SecretStore`) or `namespace/objectName` (`ClusterSecretStore`). Use `conversionStrategy` on the `find` block to sanitize keys that are not valid Secret data keys.
+
+A `property` that resolves to no value returns `property not found`; a missing object is reported as "secret not found".
+
+### ExternalSecret examples
+
+#### Fetch a single property
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: app-credentials
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: crd-store
+    kind: SecretStore
+  target:
+    name: app-credentials
+  data:
+    - secretKey: password
+      remoteRef:
+        key: app-backend
+        property: spec.password
+    - secretKey: username
+      remoteRef:
+        key: app-backend
+        property: spec.username
+```
+
+#### ClusterSecretStore with namespace whitelist
+
+Use a namespace-qualified key (`namespace/objectName`) for namespaced resources when referencing a `ClusterSecretStore`.
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ClusterSecretStore
+metadata:
+  name: crd-cluster-store
+spec:
+  provider:
+    crd:
+      # ClusterSecretStore reading the local cluster: caProvider.namespace is
+      # required here (unlike SecretStore). kube-root-ca.crt exists in every
+      # namespace, so any namespace the controller can read works.
+      server:
+        caProvider:
+          type: ConfigMap
+          name: kube-root-ca.crt
+          namespace: external-secrets
+          key: ca.crt
+      auth:
+        serviceAccount:
+          name: crd-reader
+          namespace: external-secrets
+      resource:
+        group: example.io
+        version: v1alpha1
+        kind: Widget
+      whitelist:
+        rules:
+          - namespace: "^(team-a|team-b)$"
+            name: "^app-.*$"
+            properties:
+              - "^spec\\.password$"
+---
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: team-a-widget
+  namespace: app
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: crd-cluster-store
+    kind: ClusterSecretStore
+  target:
+    name: team-a-widget
+  data:
+    - secretKey: password
+      remoteRef:
+        key: team-a/app-backend
+        property: spec.password
+```
+
+#### Fetch all matching CRD objects
+
+```yaml
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: app-widgets
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: crd-store
+    kind: SecretStore
+  target:
+    name: app-widgets
+  dataFrom:
+    - find:
+        name:
+          regexp: "^app-.*$"
+```
+
+`find.name.regexp` filters the listed objects by name.
+Whitelist rules are applied in addition to this filter.
+
+### RBAC
+
+The configured ServiceAccount must be allowed to read the target resource.
+At minimum, grant `get` on the selected resource. The `list` verb is only required when an `ExternalSecret` uses `dataFrom.find` (which calls `GetAllSecrets()` internally); store bootstrap only checks `get`.
+
+The right scope depends on which kind of store you are using:
+
+- **`SecretStore` (namespaced)**: a namespace-scoped `Role` + `RoleBinding` is enough; the controller only reads from the store's own namespace.
+- **`ClusterSecretStore`**: the controller may read across namespaces. `dataFrom.find` lists the target resource across all namespaces, and `remoteRef.key` uses the `namespace/objectName` form, so a `ClusterRole` + `ClusterRoleBinding` is required.
+
+#### SecretStore (namespace-scoped) example
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: crd-reader
+  namespace: default
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: crd-reader
+  namespace: default
+rules:
+  - apiGroups: ["example.io"]
+    resources: ["widgets"]
+    verbs: ["get", "list"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: crd-reader
+  namespace: default
+subjects:
+  - kind: ServiceAccount
+    name: crd-reader
+    namespace: default
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: crd-reader
+```
+
+#### ClusterSecretStore example
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: crd-reader
+  namespace: external-secrets
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: crd-reader
+rules:
+  - apiGroups: ["example.io"]
+    resources: ["widgets"]
+    verbs: ["get", "list"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: crd-reader
+subjects:
+  - kind: ServiceAccount
+    name: crd-reader
+    namespace: external-secrets
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: crd-reader
+```
+
+### ClusterSecretStore note
+
+When using `ClusterSecretStore`:
+
+- for namespaced resources, `remoteRef.key` must be `namespace/objectName`.
+- `whitelist.rules[].namespace` can be used to constrain which namespaces are readable.
+- with `auth.serviceAccount`, the `namespace` field is optional. When omitted, the ServiceAccount is resolved in the consuming `ExternalSecret`'s own namespace (referent authentication), so a single store can serve many namespaces, each authenticating as its local ServiceAccount. Set `auth.serviceAccount.namespace` to pin one fixed namespace instead.
+
+#### Referent authentication and RBAC
+
+With referent authentication (no `auth.serviceAccount.namespace`), the named ServiceAccount must exist in **every** namespace that consumes the store, and each must be granted `get` (plus `list` for `dataFrom.find`) on the target resource. Grant this with a `ClusterRole` plus a `RoleBinding` per consuming namespace, or a `ClusterRoleBinding` if the same access is acceptable cluster-wide. When you instead pin `auth.serviceAccount.namespace`, only that one ServiceAccount needs the binding.

+ 2 - 0
go.mod

@@ -31,6 +31,7 @@ replace (
 	github.com/external-secrets/external-secrets/providers/v1/chef => ./providers/v1/chef
 	github.com/external-secrets/external-secrets/providers/v1/cloudru => ./providers/v1/cloudru
 	github.com/external-secrets/external-secrets/providers/v1/conjur => ./providers/v1/conjur
+	github.com/external-secrets/external-secrets/providers/v1/crd => ./providers/v1/crd
 	github.com/external-secrets/external-secrets/providers/v1/delinea => ./providers/v1/delinea
 	github.com/external-secrets/external-secrets/providers/v1/doppler => ./providers/v1/doppler
 	github.com/external-secrets/external-secrets/providers/v1/dvls => ./providers/v1/dvls
@@ -148,6 +149,7 @@ require (
 	github.com/external-secrets/external-secrets/providers/v1/chef v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/cloudru v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/conjur v0.0.0-00010101000000-000000000000
+	github.com/external-secrets/external-secrets/providers/v1/crd v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/delinea v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/doppler v0.0.0-00010101000000-000000000000
 	github.com/external-secrets/external-secrets/providers/v1/dvls v0.0.0-00010101000000-000000000000

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -136,6 +136,7 @@ nav:
       - Google Cloud Secret Manager: provider/google-secrets-manager.md
       - HashiCorp Vault: provider/hashicorp-vault.md
       - Kubernetes: provider/kubernetes.md
+      - Kubernetes CRD: provider/crd.md
       - IBM Secrets Manager: provider/ibm-secrets-manager.md
       - Akeyless: provider/akeyless.md
       - Yandex Certificate Manager: provider/yandex-certificate-manager.md

+ 28 - 0
pkg/register/crd.go

@@ -0,0 +1,28 @@
+//go:build crd || all_providers
+
+/*
+Copyright © The ESO 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
+
+    https://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 register
+
+import (
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	crd "github.com/external-secrets/external-secrets/providers/v1/crd"
+)
+
+func init() {
+	esv1.Register(crd.NewProvider(), crd.ProviderSpec(), crd.MaintenanceStatus())
+}

+ 424 - 0
providers/v1/crd/crd.go

@@ -0,0 +1,424 @@
+/*
+Copyright © The ESO 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
+
+    https://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 crd implements an External Secrets provider that reads data from
+// arbitrary Kubernetes Custom Resources (CRDs) using ServiceAccount token auth.
+package crd
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+
+	"github.com/tidwall/gjson"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+)
+
+// ensureConnected rejects reads on the referent stub client, which has no kube
+// client (see newClient): the stub only answers Validate() while the store's SA
+// namespace is unknown, and the operational client is rebuilt per-ExternalSecret
+// at reconcile. Without this guard a read would nil-panic on c.kube.
+func (c *Client) ensureConnected() error {
+	if c.kube == nil {
+		return errClientNotReady
+	}
+	return nil
+}
+
+// GetSecret retrieves a single value from a CRD object.
+// ref.Key is interpreted per store kind (see parseRemoteRefKey); ref.Property is an optional GJSON path expression.
+func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	obj, err := c.fetchObject(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	return extractValue(obj, ref.Property, nil)
+}
+
+// GetSecretMap returns a map of key/value pairs extracted from a CRD object.
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	obj, err := c.fetchObject(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	raw, err := extractValue(obj, ref.Property, nil)
+	if err != nil {
+		return nil, err
+	}
+	return jsonBytesToMap(raw)
+}
+
+// fetchObject validates ref.Key, enforces the whitelist, and retrieves the named CRD object.
+func (c *Client) fetchObject(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (*unstructured.Unstructured, error) {
+	return c.resolveWhitelistedObject(ctx, ref.Key, ref.Property)
+}
+
+// resolveWhitelistedObject validates the key, enforces the whitelist, and
+// retrieves the named CRD object. Shared by GetSecret and SecretExists so both
+// read paths apply the same whitelist gate; without it SecretExists could be
+// used to probe the existence of objects the whitelist does not permit.
+func (c *Client) resolveWhitelistedObject(ctx context.Context, key, property string) (*unstructured.Unstructured, error) {
+	if err := c.ensureConnected(); err != nil {
+		return nil, err
+	}
+	if key == "" {
+		return nil, errors.New("crd: ref.key must not be empty")
+	}
+	objectName, keyNamespace, err := parseRemoteRefKey(c.storeKind, key)
+	if err != nil {
+		return nil, err
+	}
+	ns := ""
+	if keyNamespace != nil {
+		ns = *keyNamespace
+	}
+	var requestedKeys []string
+	if property != "" {
+		requestedKeys = []string{property}
+	}
+	if !c.matchesWhitelistRule(objectName, ns, requestedKeys) {
+		return nil, fmt.Errorf("crd: request for %q denied by whitelist rules", key)
+	}
+	return c.getObject(ctx, objectName, keyNamespace)
+}
+
+// GetAllSecrets lists CRD objects whose logical keys match the store Name pattern
+// (regex) and returns a map of logicalKey to serialized value.
+// For SecretStore (namespaced kind), listing is limited to the store namespace and keys are object names.
+// For ClusterSecretStore with a namespaced kind, listing spans all namespaces and keys are
+// namespace/name. Cluster-scoped kinds use object names only.
+func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	if err := c.ensureConnected(); err != nil {
+		return nil, err
+	}
+	// Verify the caller actually has "list" permission. The preflight at store
+	// bootstrap only checks "get" — moving "list" here means a SA that only
+	// ever uses GetSecret does not need list rights, but anything that calls
+	// dataFrom.find must.
+	if c.listAccessCheck != nil {
+		if err := c.listAccessCheck(ctx); err != nil {
+			return nil, err
+		}
+	}
+
+	list := &unstructured.UnstructuredList{}
+	gvk := c.buildGVK()
+	list.SetGroupVersionKind(gvk.GroupVersion().WithKind(gvk.Kind + "List"))
+
+	var opts []kclient.ListOption
+	if c.namespaced && c.storeKind != esv1.ClusterSecretStoreKind {
+		// SecretStore over a namespaced kind lists within its own namespace.
+		// Cluster-scoped kinds, and a ClusterSecretStore over a namespaced kind,
+		// list across all namespaces (no namespace option).
+		if c.namespace == "" {
+			return nil, fmt.Errorf("crd: namespace is required for namespaced resource kind %q", c.store.Resource.Kind)
+		}
+		opts = append(opts, kclient.InNamespace(c.namespace))
+	}
+	if err := c.kube.List(ctx, list, opts...); err != nil {
+		return nil, fmt.Errorf("crd: failed to list %s: %w", c.store.Resource.Kind, err)
+	}
+
+	var re *regexp.Regexp
+	if ref.Name != nil && ref.Name.RegExp != "" {
+		compiled, err := regexp.Compile(ref.Name.RegExp)
+		if err != nil {
+			return nil, fmt.Errorf("crd: invalid name pattern %q: %w", ref.Name.RegExp, err)
+		}
+		re = compiled
+	}
+
+	result := make(map[string][]byte, len(list.Items))
+	for i := range list.Items {
+		item := &list.Items[i]
+		objName := item.GetName()
+		objNS := item.GetNamespace()
+		logicalKey := objName
+		if c.namespaced && c.storeKind == esv1.ClusterSecretStoreKind {
+			logicalKey = objNS + "/" + objName
+		}
+		if re != nil && !re.MatchString(logicalKey) {
+			continue
+		}
+		if !c.matchesWhitelistRule(objName, objNS, nil) {
+			continue
+		}
+
+		b, err := extractValue(item, "", nil)
+		if err != nil {
+			return nil, fmt.Errorf("crd: failed to extract value from %s/%s: %w", c.store.Resource.Kind, logicalKey, err)
+		}
+		result[logicalKey] = b
+	}
+	return esutils.ConvertKeys(ref.ConversionStrategy, result)
+}
+
+// SecretExists returns true when the named CRD object exists and is permitted
+// by the whitelist. The whitelist is enforced here (not just in GetSecret) so
+// this cannot be used to probe for objects outside the allowed set.
+func (c *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
+	_, err := c.resolveWhitelistedObject(ctx, ref.GetRemoteKey(), ref.GetProperty())
+	if err != nil {
+		if errors.Is(err, esv1.NoSecretError{}) {
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// Validate checks that the provider is correctly configured.
+func (c *Client) Validate() (esv1.ValidationResult, error) {
+	// A referent ClusterSecretStore cannot be validated at store-creation time:
+	// the ServiceAccount namespace is only known once an ExternalSecret consumes
+	// the store. Report "unknown" rather than a false "ready".
+	if c.referent {
+		return esv1.ValidationResultUnknown, nil
+	}
+	return esv1.ValidationResultReady, nil
+}
+
+// Close is a no-op for the CRD provider.
+func (c *Client) Close(_ context.Context) error {
+	return nil
+}
+
+// buildGVK returns the GroupVersionKind of the configured target resource. The
+// controller-runtime client's RESTMapper resolves this to the correct resource
+// and scope at request time.
+func (c *Client) buildGVK() schema.GroupVersionKind {
+	return schema.GroupVersionKind{
+		Group:   c.store.Resource.Group,
+		Version: c.store.Resource.Version,
+		Kind:    c.store.Resource.Kind,
+	}
+}
+
+// getObject fetches a CRD object from the already-parsed remoteRef.key
+// components (see parseRemoteRefKey). Callers parse the key once and pass the
+// object name and optional namespace in, so the key is not re-parsed here.
+func (c *Client) getObject(ctx context.Context, objName string, keyNS *string) (*unstructured.Unstructured, error) {
+	obj := &unstructured.Unstructured{}
+	obj.SetGroupVersionKind(c.buildGVK())
+
+	if c.namespaced {
+		var requestNS string
+		switch {
+		case keyNS != nil:
+			requestNS = *keyNS
+		case c.storeKind == esv1.SecretStoreKind:
+			requestNS = c.namespace
+		default:
+			return nil, fmt.Errorf("crd: namespaced resource kind %q requires remoteRef.key in the form namespace/objectName when using ClusterSecretStore", c.store.Resource.Kind)
+		}
+		if requestNS == "" {
+			return nil, fmt.Errorf("crd: namespace is required for namespaced resource kind %q", c.store.Resource.Kind)
+		}
+		if err := c.kube.Get(ctx, kclient.ObjectKey{Namespace: requestNS, Name: objName}, obj); err != nil {
+			if apierrors.IsNotFound(err) {
+				return nil, esv1.NoSecretError{}
+			}
+			return nil, fmt.Errorf("crd: failed to get %s %s/%s: %w", c.store.Resource.Kind, requestNS, objName, err)
+		}
+		return obj, nil
+	}
+
+	if keyNS != nil {
+		return nil, fmt.Errorf("crd: cluster-scoped resource kind %q does not allow '/' in remoteRef.key (use object name only)", c.store.Resource.Kind)
+	}
+	if err := c.kube.Get(ctx, kclient.ObjectKey{Name: objName}, obj); err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, esv1.NoSecretError{}
+		}
+		return nil, fmt.Errorf("crd: failed to get %s/%s: %w", c.store.Resource.Kind, objName, err)
+	}
+	return obj, nil
+}
+
+// extractValue serializes an unstructured object (or a sub-field) to bytes.
+// property is a GJSON path expression taking precedence over fields; it uses the
+// same syntax as the Kubernetes provider (see
+// https://github.com/tidwall/gjson/blob/master/SYNTAX.md) so the property
+// dialect is consistent across ESO providers.
+// fields is the store-level Properties list restricting which fields are included.
+func extractValue(obj *unstructured.Unstructured, property string, fields []string) ([]byte, error) {
+	raw, err := json.Marshal(obj.Object)
+	if err != nil {
+		return nil, fmt.Errorf("crd: failed to marshal object: %w", err)
+	}
+
+	if property != "" {
+		res := gjson.GetBytes(raw, property)
+		if !res.Exists() {
+			return nil, fmt.Errorf("crd: property %q not found in object %q", property, obj.GetName())
+		}
+		// String leaves are returned unwrapped; everything else (objects,
+		// arrays, numbers, booleans) is returned as its raw JSON.
+		if res.Type == gjson.String {
+			return []byte(res.Str), nil
+		}
+		return []byte(res.Raw), nil
+	}
+
+	if len(fields) > 0 {
+		subset := make(map[string]any, len(fields))
+		for _, f := range fields {
+			res := gjson.GetBytes(raw, f)
+			if res.Exists() {
+				subset[f] = res.Value()
+			}
+		}
+		return esutils.JSONMarshal(subset)
+	}
+
+	return raw, nil
+}
+
+// jsonBytesToMap converts a JSON byte slice to map[string][]byte.
+// String values are unwrapped (JSON quotes removed); non-string values
+// (objects, arrays, numbers, booleans) are kept as raw JSON bytes.
+//
+// When the input is valid JSON but not an object (e.g. a bare string
+// `"hello"` or an array `[1,2]`), it cannot be mapped to key/value
+// pairs. In that case the raw payload is returned under a single
+// "value" key. This is intentional: the input always originates from
+// extractValue which already validated it via json.Marshal, so a
+// non-object result is expected for non-map properties.
+func jsonBytesToMap(raw []byte) (map[string][]byte, error) {
+	var kv map[string]json.RawMessage
+	if err := json.Unmarshal(raw, &kv); err != nil {
+		return map[string][]byte{"value": raw}, nil
+	}
+	out := make(map[string][]byte, len(kv))
+	for k, v := range kv {
+		var s string
+		if err := json.Unmarshal(v, &s); err == nil {
+			out[k] = []byte(s)
+		} else {
+			out[k] = v
+		}
+	}
+	return out, nil
+}
+
+// compiledWhitelistRule is a pre-validated, pre-compiled form of
+// CRDProviderWhitelistRule. Patterns are compiled once at Client construction
+// and reused on every read instead of recompiling on the hot path.
+type compiledWhitelistRule struct {
+	name       *regexp.Regexp   // nil when the rule does not constrain the object name
+	namespace  *regexp.Regexp   // nil when the rule does not constrain the namespace
+	properties []*regexp.Regexp // empty when the rule does not constrain properties
+}
+
+// compileWhitelistRules validates and compiles every regex in the whitelist.
+// Returns nil with no error when the whitelist is unset or has no rules.
+// Empty rules (no name, no namespace, no properties) are rejected because they
+// would match anything and silently widen access.
+func compileWhitelistRules(wl *esv1.CRDProviderWhitelist) ([]compiledWhitelistRule, error) {
+	if wl == nil || len(wl.Rules) == 0 {
+		return nil, nil
+	}
+	rules := make([]compiledWhitelistRule, 0, len(wl.Rules))
+	for i, r := range wl.Rules {
+		if r.Name == "" && r.Namespace == "" && len(r.Properties) == 0 {
+			return nil, fmt.Errorf("crd: whitelist.rules[%d]: %w", i, errEmptyWhitelistRule)
+		}
+		var cr compiledWhitelistRule
+		if r.Name != "" {
+			re, err := regexp.Compile(r.Name)
+			if err != nil {
+				return nil, fmt.Errorf("crd: invalid whitelist.rules[%d].name regex %q: %w", i, r.Name, err)
+			}
+			cr.name = re
+		}
+		if r.Namespace != "" {
+			re, err := regexp.Compile(r.Namespace)
+			if err != nil {
+				return nil, fmt.Errorf("crd: invalid whitelist.rules[%d].namespace regex %q: %w", i, r.Namespace, err)
+			}
+			cr.namespace = re
+		}
+		if len(r.Properties) > 0 {
+			cr.properties = make([]*regexp.Regexp, 0, len(r.Properties))
+			for j, p := range r.Properties {
+				re, err := regexp.Compile(p)
+				if err != nil {
+					return nil, fmt.Errorf("crd: invalid whitelist.rules[%d].properties[%d] regex %q: %w", i, j, p, err)
+				}
+				cr.properties = append(cr.properties, re)
+			}
+		}
+		rules = append(rules, cr)
+	}
+	return rules, nil
+}
+
+// matchesWhitelistRule checks whether the given object (identified by its bare
+// name and namespace) is permitted by the store's whitelist rules.
+// objectName is always the bare name without any namespace prefix.
+// namespace is the object's namespace; it is only considered when the store is
+// a ClusterSecretStore and rule.namespace is set – for SecretStore the field
+// is ignored because the namespace is implicitly fixed to the store namespace.
+func (c *Client) matchesWhitelistRule(objectName, namespace string, requestedKeys []string) bool {
+	if len(c.whitelistRules) == 0 {
+		return true
+	}
+	for _, rule := range c.whitelistRules {
+		if rule.name != nil && !rule.name.MatchString(objectName) {
+			continue
+		}
+		// Namespace check: only evaluated for ClusterSecretStore. Cluster-scoped
+		// objects (namespace=="") never match a namespace rule — the rule
+		// explicitly targets namespaced objects.
+		if rule.namespace != nil && c.storeKind == esv1.ClusterSecretStoreKind {
+			if namespace == "" || !rule.namespace.MatchString(namespace) {
+				continue
+			}
+		}
+		if len(rule.properties) == 0 {
+			return true
+		}
+		if len(requestedKeys) == 0 {
+			continue
+		}
+		allMatched := true
+		for _, key := range requestedKeys {
+			matched := false
+			for _, re := range rule.properties {
+				if re.MatchString(key) {
+					matched = true
+					break
+				}
+			}
+			if !matched {
+				allMatched = false
+				break
+			}
+		}
+		if allMatched {
+			return true
+		}
+	}
+	return false
+}

+ 710 - 0
providers/v1/crd/crd_test.go

@@ -0,0 +1,710 @@
+/*
+Copyright © The ESO 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
+
+    https://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 crd
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"strings"
+	"testing"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	crfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// ── test doubles ─────────────────────────────────────────────────────────────
+
+type testPushSecretData struct{}
+
+func (testPushSecretData) GetMetadata() *apiextensionsv1.JSON { return nil }
+func (testPushSecretData) GetSecretKey() string               { return "" }
+func (testPushSecretData) GetRemoteKey() string               { return "" }
+func (testPushSecretData) GetProperty() string                { return "" }
+
+type testPushSecretRemoteRef struct {
+	remoteKey string
+	property  string
+}
+
+func (r testPushSecretRemoteRef) GetRemoteKey() string { return r.remoteKey }
+func (r testPushSecretRemoteRef) GetProperty() string  { return r.property }
+
+// ── helpers ──────────────────────────────────────────────────────────────────
+
+const testStringHello = "hello"
+
+var testResource = esv1.CRDProviderResource{
+	Group: "example.io", Version: "v1alpha1", Kind: "Widget",
+}
+
+func makeStore(rules ...esv1.CRDProviderWhitelistRule) *esv1.CRDProvider {
+	s := &esv1.CRDProvider{
+		Auth: &esv1.KubernetesAuth{
+			ServiceAccount: &esmeta.ServiceAccountSelector{Name: "reader"},
+		},
+		Resource: testResource,
+	}
+	if len(rules) > 0 {
+		s.Whitelist = &esv1.CRDProviderWhitelist{Rules: rules}
+	}
+	return s
+}
+
+func wlRule(name string, props ...string) esv1.CRDProviderWhitelistRule {
+	return esv1.CRDProviderWhitelistRule{Name: name, Properties: props}
+}
+
+func wlRuleNS(ns, name string, props ...string) esv1.CRDProviderWhitelistRule {
+	return esv1.CRDProviderWhitelistRule{Namespace: ns, Name: name, Properties: props}
+}
+
+func widget(name, namespace string, spec map[string]any) *unstructured.Unstructured {
+	metaData := map[string]any{"name": name}
+	if namespace != "" {
+		metaData["namespace"] = namespace
+	}
+	return &unstructured.Unstructured{Object: map[string]any{
+		"apiVersion": "example.io/v1alpha1", "kind": "Widget",
+		"metadata": metaData, "spec": spec,
+	}}
+}
+
+// testWidgetGVK is the GroupVersionKind of the Widget test resource.
+var testWidgetGVK = schema.GroupVersionKind{Group: "example.io", Version: "v1alpha1", Kind: "Widget"}
+
+// fakeCRDClient builds a controller-runtime fake client that serves the Widget
+// test resource as unstructured objects, with a RESTMapper carrying the given
+// scope so List/Get behave like the production controller-runtime client.
+func fakeCRDClient(namespaced bool, objs ...kclient.Object) kclient.Client {
+	scheme := runtime.NewScheme()
+	scheme.AddKnownTypeWithName(testWidgetGVK, &unstructured.Unstructured{})
+	scheme.AddKnownTypeWithName(testWidgetGVK.GroupVersion().WithKind(testWidgetGVK.Kind+"List"), &unstructured.UnstructuredList{})
+
+	scope := meta.RESTScopeNamespace
+	if !namespaced {
+		scope = meta.RESTScopeRoot
+	}
+	mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{testWidgetGVK.GroupVersion()})
+	mapper.Add(testWidgetGVK, scope)
+
+	return crfake.NewClientBuilder().WithScheme(scheme).WithRESTMapper(mapper).WithObjects(objs...).Build()
+}
+
+// newTestClient builds a Client for use in unit tests.
+// storeKind must be esv1.SecretStoreKind or esv1.ClusterSecretStoreKind.
+// Whitelist regexes must be valid: invalid patterns are caught at admission
+// (ValidateStore) and are not reachable via the Client constructor in production.
+func newTestClient(store *esv1.CRDProvider, storeKind, namespace string, namespaced bool, objs ...kclient.Object) *Client {
+	rules, err := compileWhitelistRules(store.Whitelist)
+	if err != nil {
+		panic("newTestClient: invalid whitelist in test fixture: " + err.Error())
+	}
+	return &Client{
+		store:          store,
+		namespace:      namespace,
+		namespaced:     namespaced,
+		storeKind:      storeKind,
+		kube:           fakeCRDClient(namespaced, objs...),
+		whitelistRules: rules,
+	}
+}
+
+// Shorthands for the two most common configurations.
+func ssClient(store *esv1.CRDProvider, ns string, objs ...kclient.Object) *Client {
+	return newTestClient(store, esv1.SecretStoreKind, ns, true, objs...)
+}
+
+func cssClient(store *esv1.CRDProvider, objs ...kclient.Object) *Client {
+	return newTestClient(store, esv1.ClusterSecretStoreKind, "", true, objs...)
+}
+
+func ref(key, prop string) esv1.ExternalSecretDataRemoteRef {
+	return esv1.ExternalSecretDataRemoteRef{Key: key, Property: prop}
+}
+
+// assertJSON unmarshals b and calls check on the result.
+func assertJSON[T any](t *testing.T, b []byte, check func(*testing.T, T)) {
+	t.Helper()
+	var v T
+	if err := json.Unmarshal(b, &v); err != nil {
+		t.Fatalf("unmarshal: %v\nraw: %s", err, b)
+	}
+	check(t, v)
+}
+
+// ── tests ────────────────────────────────────────────────────────────────────
+
+func TestClientBuildGVK(t *testing.T) {
+	c := newTestClient(makeStore(), esv1.SecretStoreKind, "", true)
+	gvk := c.buildGVK()
+	if gvk.Group != "example.io" || gvk.Version != "v1alpha1" || gvk.Kind != "Widget" {
+		t.Fatalf("unexpected GVK: %+v", gvk)
+	}
+}
+
+func TestClientGetSecret(t *testing.T) {
+	richSpec := map[string]any{
+		"password": "pw1",
+		"foo":      map[string]any{"bar": int64(42), "baz": testStringHello},
+		"nested":   []any{map[string]any{"key": "ep", "val": "db:5432"}, map[string]any{"key": "fqdn", "val": "u:p@db"}},
+	}
+
+	tests := []struct {
+		name       string
+		client     func() *Client
+		ref        esv1.ExternalSecretDataRemoteRef
+		wantStr    string
+		wantErrIs  error
+		wantErrMsg string
+		checkFn    func(*testing.T, []byte)
+	}{
+		// ── SecretStore ──
+		{
+			name: "empty key", client: func() *Client { return ssClient(makeStore(), "ns1", widget("x", "ns1", richSpec)) },
+			ref: ref("", ""), wantErrMsg: "must not be empty",
+		},
+		{
+			name: "slash rejected", client: func() *Client { return ssClient(makeStore(), "ns1", widget("x", "ns1", richSpec)) },
+			ref: ref("a/b", ""), wantErrMsg: "must not contain '/'",
+		},
+		{
+			name: "missing object", client: func() *Client { return ssClient(makeStore(), "ns1", widget("x", "ns1", richSpec)) },
+			ref: ref("does-not-exist", ""), wantErrIs: esv1.NoSecretError{},
+		},
+		{
+			name: "scalar property", client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
+			ref: ref("item-a", "spec.password"), wantStr: "pw1",
+		},
+		{
+			name: "nested scalar via dot path", client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
+			ref: ref("item-a", "spec.foo.bar"), wantStr: "42",
+		},
+		{
+			name: "gjson query on array", client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
+			ref: ref("item-a", `spec.nested.#(key=="fqdn").val`), wantStr: "u:p@db",
+		},
+		{
+			name:   "nested object returns JSON",
+			client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
+			ref:    ref("item-a", "spec.foo"),
+			checkFn: func(t *testing.T, b []byte) {
+				assertJSON(t, b, func(t *testing.T, m map[string]any) {
+					if m["bar"] != float64(42) || m["baz"] != testStringHello {
+						t.Fatalf("spec.foo = %v", m)
+					}
+				})
+			},
+		},
+		{
+			name:   "array property returns JSON array",
+			client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
+			ref:    ref("item-a", "spec.nested"),
+			checkFn: func(t *testing.T, b []byte) {
+				assertJSON(t, b, func(t *testing.T, arr []map[string]any) {
+					if len(arr) != 2 || arr[0]["key"] != "ep" {
+						t.Fatalf("spec.nested = %v", arr)
+					}
+				})
+			},
+		},
+
+		// ── ClusterSecretStore: namespaced kind ──
+		{
+			name: "CSS: namespace/name resolves", client: func() *Client { return cssClient(makeStore(), widget("item-a", "ns1", richSpec)) },
+			ref: ref("ns1/item-a", "spec.password"), wantStr: "pw1",
+		},
+		{
+			name: "CSS: bare name rejected for namespaced kind", client: func() *Client { return cssClient(makeStore(), widget("item-a", "ns1", richSpec)) },
+			ref: ref("item-a", ""), wantErrMsg: "namespace/objectName",
+		},
+
+		// ── ClusterSecretStore: cluster-scoped kind ──
+		{
+			name: "CSS cluster-scoped: bare name resolves",
+			client: func() *Client {
+				return newTestClient(makeStore(), esv1.ClusterSecretStoreKind, "default", false,
+					widget("global", "", map[string]any{"password": "x"}))
+			},
+			ref: ref("global", "spec.password"), wantStr: "x",
+		},
+		{
+			name: "CSS cluster-scoped: slash rejected",
+			client: func() *Client {
+				return newTestClient(makeStore(), esv1.ClusterSecretStoreKind, "default", false,
+					widget("global", "", map[string]any{"password": "x"}))
+			},
+			ref: ref("ns/global", "spec.password"), wantErrMsg: "does not allow '/'",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.client().GetSecret(context.Background(), tt.ref)
+			switch {
+			case tt.wantErrMsg != "":
+				if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
+					t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
+				}
+			case tt.wantErrIs != nil:
+				if !errors.Is(err, tt.wantErrIs) {
+					t.Fatalf("error = %v, want %T", err, tt.wantErrIs)
+				}
+			default:
+				if err != nil {
+					t.Fatalf("unexpected error: %v", err)
+				}
+				if tt.wantStr != "" && string(got) != tt.wantStr {
+					t.Fatalf("= %q, want %q", string(got), tt.wantStr)
+				}
+				if tt.checkFn != nil {
+					tt.checkFn(t, got)
+				}
+			}
+		})
+	}
+}
+
+func TestExtractValue(t *testing.T) {
+	obj := widget("sample", "default", map[string]any{
+		"password": "s3cr3t",
+		"meta":     map[string]any{"a": "b"},
+		"targets":  []any{map[string]any{"name": "app", "value": "v1"}, map[string]any{"name": "db", "value": "v2"}},
+	})
+
+	tests := []struct {
+		name       string
+		property   string
+		fields     []string
+		wantStr    string
+		wantErrMsg string
+		checkFn    func(*testing.T, []byte)
+	}{
+		{name: "by property", property: "spec.password", wantStr: "s3cr3t"},
+		{name: "missing property", property: "spec.missing", wantErrMsg: "not found"},
+		{name: "query with no match is not found", property: `spec.targets.#(name=="nope").value`, wantErrMsg: "not found"},
+		{name: "gjson array query", property: `spec.targets.#(name=="db").value`, wantStr: "v2"},
+		{
+			name: "selected fields", fields: []string{"spec.password", "spec.meta.a"},
+			checkFn: func(t *testing.T, b []byte) {
+				assertJSON(t, b, func(t *testing.T, m map[string]any) {
+					if m["spec.password"] != "s3cr3t" || m["spec.meta.a"] != "b" {
+						t.Fatalf("subset = %v", m)
+					}
+				})
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := extractValue(obj, tt.property, tt.fields)
+			if tt.wantErrMsg != "" {
+				if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
+					t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if tt.wantStr != "" && string(got) != tt.wantStr {
+				t.Fatalf("= %q, want %q", string(got), tt.wantStr)
+			}
+			if tt.checkFn != nil {
+				tt.checkFn(t, got)
+			}
+		})
+	}
+}
+
+func TestJSONBytesToMap(t *testing.T) {
+	tests := []struct {
+		name    string
+		raw     string
+		checkFn func(*testing.T, map[string][]byte)
+	}{
+		{
+			name: "mixed value types",
+			raw:  `{"a":"x","b":1}`,
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				if string(got["a"]) != "x" || string(got["b"]) != "1" {
+					t.Fatalf("got %v", got)
+				}
+			},
+		},
+		{
+			name: "non-object falls back to value key",
+			raw:  `"hello"`,
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				if string(got["value"]) != `"hello"` {
+					t.Fatalf(`["value"] = %q`, string(got["value"]))
+				}
+			},
+		},
+		{
+			name: "nested object preserved as JSON",
+			raw:  `{"user":"admin","foo":{"bar":42,"baz":"hello"}}`,
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				if string(got["user"]) != "admin" {
+					t.Fatalf(`["user"] = %q`, string(got["user"]))
+				}
+				assertJSON(t, got["foo"], func(t *testing.T, m map[string]any) {
+					if m["bar"] != float64(42) || m["baz"] != testStringHello {
+						t.Fatalf("foo = %v", m)
+					}
+				})
+			},
+		},
+		{
+			name: "array preserved as JSON",
+			raw:  `{"items":[{"key":"a","val":"1"},{"key":"b","val":"2"}]}`,
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				assertJSON(t, got["items"], func(t *testing.T, items []map[string]any) {
+					if len(items) != 2 || items[0]["key"] != "a" {
+						t.Fatalf("items = %v", items)
+					}
+				})
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := jsonBytesToMap([]byte(tt.raw))
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			tt.checkFn(t, got)
+		})
+	}
+}
+
+func TestClientGetSecretMap(t *testing.T) {
+	obj := widget("item-a", "ns1", map[string]any{
+		"map":    map[string]any{"a": "x", "b": int64(1)},
+		"foo":    map[string]any{"bar": int64(42), "baz": testStringHello},
+		"nested": []any{map[string]any{"key": "ep", "val": "db:5432"}, map[string]any{"key": "fqdn", "val": "u:p@db"}},
+	})
+	c := ssClient(makeStore(), "ns1", obj)
+
+	tests := []struct {
+		name    string
+		ref     esv1.ExternalSecretDataRemoteRef
+		checkFn func(*testing.T, map[string][]byte)
+	}{
+		{
+			name: "flat sub-object",
+			ref:  ref("item-a", "spec.map"),
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				if string(got["a"]) != "x" || string(got["b"]) != "1" {
+					t.Fatalf("got %v", got)
+				}
+			},
+		},
+		{
+			name: "spec returns nested objects as JSON",
+			ref:  ref("item-a", "spec"),
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				assertJSON(t, got["foo"], func(t *testing.T, m map[string]any) {
+					if m["bar"] != float64(42) || m["baz"] != testStringHello {
+						t.Fatalf("foo = %v", m)
+					}
+				})
+				assertJSON(t, got["nested"], func(t *testing.T, arr []map[string]any) {
+					if len(arr) != 2 || arr[0]["key"] != "ep" {
+						t.Fatalf("nested = %v", arr)
+					}
+				})
+			},
+		},
+		{
+			name: "spec.foo returns flat map",
+			ref:  ref("item-a", "spec.foo"),
+			checkFn: func(t *testing.T, got map[string][]byte) {
+				if string(got["bar"]) != "42" || string(got["baz"]) != testStringHello {
+					t.Fatalf("got %v", got)
+				}
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := c.GetSecretMap(context.Background(), tt.ref)
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			tt.checkFn(t, got)
+		})
+	}
+}
+
+func TestClientGetAllSecrets(t *testing.T) {
+	objA := widget("app-a", "ns1", map[string]any{"password": "a"})
+	objB := widget("sys-b", "ns1", map[string]any{"password": "b"})
+
+	tests := []struct {
+		name       string
+		client     func() *Client
+		find       esv1.ExternalSecretFind
+		wantKeys   []string
+		wantErrMsg string
+	}{
+		{
+			name: "no filter returns all", wantKeys: []string{"app-a", "sys-b"},
+			client: func() *Client { return ssClient(makeStore(), "ns1", objA, objB) },
+		},
+		{
+			name: "regexp filters list", wantKeys: []string{"sys-b"},
+			client: func() *Client { return ssClient(makeStore(), "ns1", objA, objB) },
+			find:   esv1.ExternalSecretFind{Name: &esv1.FindName{RegExp: "^sys-.*$"}},
+		},
+		{
+			name: "invalid regex", wantErrMsg: "invalid name pattern",
+			client: func() *Client { return ssClient(makeStore(), "ns1", objA) },
+			find:   esv1.ExternalSecretFind{Name: &esv1.FindName{RegExp: "("}},
+		},
+		{
+			name: "whitelist name rule", wantKeys: []string{"app-a"},
+			client: func() *Client { return ssClient(makeStore(wlRule("^app-.*$")), "ns1", objA, objB) },
+		},
+		{
+			name: "CSS namespaced kind uses namespace/name keys", wantKeys: []string{"ns1/app-a", "ns2/sys-b"},
+			client: func() *Client {
+				return cssClient(makeStore(),
+					widget("app-a", "ns1", map[string]any{"password": "a"}),
+					widget("sys-b", "ns2", map[string]any{"password": "b"}))
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.client().GetAllSecrets(context.Background(), tt.find)
+			if tt.wantErrMsg != "" {
+				if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
+					t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if len(got) != len(tt.wantKeys) {
+				t.Fatalf("len = %d, want %d; keys: %v", len(got), len(tt.wantKeys), got)
+			}
+			for _, k := range tt.wantKeys {
+				if _, ok := got[k]; !ok {
+					t.Fatalf("missing key %q", k)
+				}
+			}
+		})
+	}
+}
+
+func TestClientMiscMethods(t *testing.T) {
+	c := ssClient(makeStore(), "ns1")
+	if err := c.PushSecret(context.Background(), nil, testPushSecretData{}); err == nil {
+		t.Fatal("PushSecret() expected error")
+	}
+	if err := c.DeleteSecret(context.Background(), testPushSecretRemoteRef{}); err == nil {
+		t.Fatal("DeleteSecret() expected error")
+	}
+	if got, err := c.Validate(); err != nil || got != esv1.ValidationResultReady {
+		t.Fatalf("Validate() = (%v, %v), want (%v, nil)", got, err, esv1.ValidationResultReady)
+	}
+	if err := c.Close(context.Background()); err != nil {
+		t.Fatalf("Close() unexpected error: %v", err)
+	}
+}
+
+func TestReadsRejectReferentStub(t *testing.T) {
+	// The referent stub (returned by newClient for a ClusterSecretStore whose SA
+	// namespace is not yet known) has no kube client. Every read must return
+	// errClientNotReady instead of nil-panicking on c.kube.
+	c := &Client{referent: true, storeKind: esv1.ClusterSecretStoreKind}
+	ctx := context.Background()
+
+	if _, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "widget"}); !errors.Is(err, errClientNotReady) {
+		t.Fatalf("GetSecret() err = %v, want errClientNotReady", err)
+	}
+	if _, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "widget"}); !errors.Is(err, errClientNotReady) {
+		t.Fatalf("GetSecretMap() err = %v, want errClientNotReady", err)
+	}
+	if _, err := c.GetAllSecrets(ctx, esv1.ExternalSecretFind{}); !errors.Is(err, errClientNotReady) {
+		t.Fatalf("GetAllSecrets() err = %v, want errClientNotReady", err)
+	}
+	if _, err := c.SecretExists(ctx, testPushSecretRemoteRef{remoteKey: "widget"}); !errors.Is(err, errClientNotReady) {
+		t.Fatalf("SecretExists() err = %v, want errClientNotReady", err)
+	}
+}
+
+func TestClientSecretExists(t *testing.T) {
+	obj := widget("item-a", "ns1", map[string]any{"password": "pw1"})
+	c := ssClient(makeStore(), "ns1", obj)
+
+	if exists, err := c.SecretExists(context.Background(), testPushSecretRemoteRef{remoteKey: "item-a"}); err != nil || !exists {
+		t.Fatalf("SecretExists(item-a) = (%v, %v), want (true, nil)", exists, err)
+	}
+	if exists, err := c.SecretExists(context.Background(), testPushSecretRemoteRef{remoteKey: "missing"}); err != nil || exists {
+		t.Fatalf("SecretExists(missing) = (%v, %v), want (false, nil)", exists, err)
+	}
+}
+
+// TestWhitelistMatching covers all whitelist filter dimensions: name, namespace,
+// properties, and combinations — as a single table-driven test.
+func TestWhitelistMatching(t *testing.T) {
+	obj := widget("item-a", "ns1", map[string]any{"password": "pw1"})
+
+	tests := []struct {
+		name       string
+		client     func() *Client
+		ref        esv1.ExternalSecretDataRemoteRef
+		wantVal    string
+		wantErrMsg string
+	}{
+		// ── name-only rules (SecretStore) ──
+		{
+			name: "denied when no rule matches", wantErrMsg: "denied by whitelist",
+			client: func() *Client { return ssClient(makeStore(wlRule("^allowed-.*$")), "ns1", obj) },
+			ref:    ref("item-a", "spec.password"),
+		},
+		{
+			name: "allowed by name rule", wantVal: "pw1",
+			client: func() *Client { return ssClient(makeStore(wlRule("^item-.*$")), "ns1", obj) },
+			ref:    ref("item-a", "spec.password"),
+		},
+
+		// ── name + properties ──
+		{
+			name: "denied when property does not match", wantErrMsg: "denied by whitelist",
+			client: func() *Client { return ssClient(makeStore(wlRule("^item-.*$", `^spec\.allowed$`)), "ns1", obj) },
+			ref:    ref("item-a", "spec.password"),
+		},
+		{
+			name: "allowed when both name and property match", wantVal: "pw1",
+			client: func() *Client { return ssClient(makeStore(wlRule("^item-.*$", `^spec\.password$`)), "ns1", obj) },
+			ref:    ref("item-a", "spec.password"),
+		},
+
+		// ── properties-only ──
+		{
+			name: "allowed when one of two properties matches", wantVal: "pw1",
+			client: func() *Client {
+				return ssClient(makeStore(esv1.CRDProviderWhitelistRule{Properties: []string{`^spec\.username$`, `^spec\.password$`}}), "ns1", obj)
+			},
+			ref: ref("item-a", "spec.password"),
+		},
+		{
+			name: "denied when no property matches", wantErrMsg: "denied by whitelist",
+			client: func() *Client {
+				return ssClient(makeStore(esv1.CRDProviderWhitelistRule{Properties: []string{`^spec\.username$`, `^spec\.token$`}}), "ns1", obj)
+			},
+			ref: ref("item-a", "spec.password"),
+		},
+
+		// ── namespace rules (ClusterSecretStore) ──
+		{
+			name: "CSS: namespace allows matching NS", wantVal: "pw1",
+			client: func() *Client { return cssClient(makeStore(wlRuleNS("^ns1$", "")), obj) },
+			ref:    ref("ns1/item-a", "spec.password"),
+		},
+		{
+			name: "CSS: namespace denies non-matching NS", wantErrMsg: "denied by whitelist",
+			client: func() *Client { return cssClient(makeStore(wlRuleNS("^prod$", "")), obj) },
+			ref:    ref("ns1/item-a", "spec.password"),
+		},
+		{
+			name: "CSS: namespace regex pattern", wantVal: "pw1",
+			client: func() *Client { return cssClient(makeStore(wlRuleNS("^ns.*$", "")), obj) },
+			ref:    ref("ns1/item-a", "spec.password"),
+		},
+		{
+			name: "CSS: namespace + name both must match", wantErrMsg: "denied by whitelist",
+			client: func() *Client { return cssClient(makeStore(wlRuleNS("^ns1$", "^other-.*$")), obj) },
+			ref:    ref("ns1/item-a", "spec.password"),
+		},
+		{
+			name: "SecretStore ignores namespace rule", wantVal: "pw1",
+			client: func() *Client { return ssClient(makeStore(wlRuleNS("^prod$", "")), "ns1", obj) },
+			ref:    ref("item-a", "spec.password"),
+		},
+		{
+			// Regression: namespace rule must not match cluster-scoped objects.
+			name: "CSS: namespace rule does not match cluster-scoped object", wantErrMsg: "denied by whitelist",
+			client: func() *Client {
+				return newTestClient(makeStore(wlRuleNS("^prod$", "")), esv1.ClusterSecretStoreKind, "", false,
+					widget("item-a", "", map[string]any{"password": "pw1"}))
+			},
+			ref: ref("item-a", "spec.password"),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := tt.client().GetSecret(context.Background(), tt.ref)
+			if tt.wantErrMsg != "" {
+				if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
+					t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if string(got) != tt.wantVal {
+				t.Fatalf("= %q, want %q", string(got), tt.wantVal)
+			}
+		})
+	}
+}
+
+// TestWhitelistGetAllSecrets verifies namespace whitelist filtering in GetAllSecrets.
+func TestWhitelistGetAllSecrets(t *testing.T) {
+	o1 := widget("app-a", "ns1", map[string]any{"password": "a"})
+	o2 := widget("app-b", "ns2", map[string]any{"password": "b"})
+
+	tests := []struct {
+		name     string
+		rules    []esv1.CRDProviderWhitelistRule
+		wantKeys []string
+	}{
+		{name: "allow only ns1", rules: []esv1.CRDProviderWhitelistRule{wlRuleNS("^ns1$", "")}, wantKeys: []string{"ns1/app-a"}},
+		{name: "ns1 + name rule", rules: []esv1.CRDProviderWhitelistRule{wlRuleNS("^ns1$", ""), wlRuleNS("", "^app-b$")}, wantKeys: []string{"ns1/app-a", "ns2/app-b"}},
+		{name: "filter to ns2", rules: []esv1.CRDProviderWhitelistRule{wlRuleNS("^ns2$", "")}, wantKeys: []string{"ns2/app-b"}},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := cssClient(makeStore(tt.rules...), o1, o2)
+			got, err := c.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{})
+			if err != nil {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if len(got) != len(tt.wantKeys) {
+				t.Fatalf("len = %d, want %d; keys: %v", len(got), len(tt.wantKeys), got)
+			}
+			for _, k := range tt.wantKeys {
+				if _, ok := got[k]; !ok {
+					t.Fatalf("missing key %q; got %v", k, got)
+				}
+			}
+		})
+	}
+}

+ 100 - 0
providers/v1/crd/go.mod

@@ -0,0 +1,100 @@
+module github.com/external-secrets/external-secrets/providers/v1/crd
+
+go 1.26.5
+
+require (
+	github.com/external-secrets/external-secrets/apis v0.0.0
+	github.com/external-secrets/external-secrets/runtime v0.0.0
+	github.com/tidwall/gjson v1.18.0
+	k8s.io/api v0.35.2
+	k8s.io/apiextensions-apiserver v0.35.2
+	k8s.io/apimachinery v0.35.2
+	k8s.io/client-go v0.35.2
+	sigs.k8s.io/controller-runtime v0.23.3
+)
+
+require (
+	dario.cat/mergo v1.0.2 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.4.0 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
+	github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-openapi/jsonpointer v0.22.5 // indirect
+	github.com/go-openapi/jsonreference v0.21.5 // indirect
+	github.com/go-openapi/swag v0.25.5 // indirect
+	github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
+	github.com/go-openapi/swag/conv v0.25.5 // indirect
+	github.com/go-openapi/swag/fileutils v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
+	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
+	github.com/go-openapi/swag/loading v0.25.5 // indirect
+	github.com/go-openapi/swag/mangling v0.25.5 // indirect
+	github.com/go-openapi/swag/netutils v0.25.5 // indirect
+	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
+	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
+	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/google/btree v1.1.3 // indirect
+	github.com/google/gnostic-models v0.7.1 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/huandu/xstrings v1.5.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/lestrrat-go/httprc v1.0.6 // indirect
+	github.com/lestrrat-go/iter v1.0.2 // indirect
+	github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect
+	github.com/lestrrat-go/option v1.0.1 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.23.2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.67.5 // indirect
+	github.com/prometheus/procfs v0.20.1 // indirect
+	github.com/segmentio/asm v1.2.1 // indirect
+	github.com/shopspring/decimal v1.4.0 // indirect
+	github.com/spf13/cast v1.10.0 // indirect
+	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
+	github.com/x448/float16 v0.8.4 // indirect
+	go.yaml.in/yaml/v2 v2.4.4 // indirect
+	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/crypto v0.53.0 // indirect
+	golang.org/x/net v0.56.0 // indirect
+	golang.org/x/oauth2 v0.36.0 // indirect
+	golang.org/x/sync v0.21.0 // indirect
+	golang.org/x/sys v0.46.0 // indirect
+	golang.org/x/term v0.44.0 // indirect
+	golang.org/x/text v0.38.0 // indirect
+	golang.org/x/time v0.15.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+	google.golang.org/protobuf v1.36.11 // indirect
+	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	k8s.io/klog/v2 v2.140.0 // indirect
+	k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
+	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
+	sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+	sigs.k8s.io/randfill v1.0.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
+	software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
+)
+
+replace (
+	github.com/external-secrets/external-secrets/apis => ../../../apis
+	github.com/external-secrets/external-secrets/runtime => ../../../runtime
+)

+ 244 - 0
providers/v1/crd/go.sum

@@ -0,0 +1,244 @@
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg=
+github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ=
+github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
+github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
+github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
+github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
+github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
+github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
+github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
+github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
+github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
+github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
+github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
+github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
+github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=
+github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
+github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
+github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
+github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
+github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
+github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
+github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
+github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
+github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
+github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
+github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
+github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
+github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
+github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=
+github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=
+github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
+github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
+github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
+github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
+github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
+github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
+github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
+github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
+github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
+github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
+github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
+github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
+github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
+github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
+github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
+github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
+github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
+github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=
+github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
+github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
+github.com/oracle/oci-go-sdk/v65 v65.103.0 h1:HfyZx+JefCPK3At0Xt45q+wr914jDXuoyzOFX3XCbno=
+github.com/oracle/oci-go-sdk/v65 v65.103.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
+go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
+golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
+golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
+gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
+gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
+k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
+k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
+k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
+k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
+k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
+k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
+k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
+k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
+k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf h1:btPscg4cMql0XdYK2jLsJcNEKmACJz8l+U7geC06FiM=
+k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
+k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
+sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
+sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
+sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
+sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
+software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

+ 64 - 0
providers/v1/crd/key.go

@@ -0,0 +1,64 @@
+/*
+Copyright © The ESO 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
+
+    https://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 crd
+
+import (
+	"fmt"
+	"strings"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+// parseRemoteRefKey interprets ExternalSecret remoteRef.key (and PushSecret remote key).
+//
+// SecretStore: '/' is not allowed; the object name is the full key. The API
+// namespace comes only from the store namespace, never from the key.
+//
+// ClusterSecretStore: if the key contains '/', it must be namespace/objectName (first
+// slash separates). If there is no '/', the key is the object name for a cluster-scoped
+// resource only.
+//
+// Returns objectName, keyNamespace (non-nil when namespace/objectName form was used), err.
+func parseRemoteRefKey(storeKind, remoteKey string) (objectName string, keyNamespace *string, err error) {
+	if remoteKey == "" {
+		return "", nil, fmt.Errorf("crd: remoteRef.key must not be empty")
+	}
+	switch storeKind {
+	case esv1.SecretStoreKind:
+		if strings.Contains(remoteKey, "/") {
+			return "", nil, fmt.Errorf("crd: remoteRef.key must not contain '/' for SecretStore; namespace is fixed to the store namespace")
+		}
+		return remoteKey, nil, nil
+	case esv1.ClusterSecretStoreKind:
+		ns, name, ok := strings.Cut(remoteKey, "/")
+		if !ok {
+			return remoteKey, nil, nil
+		}
+		if ns == "" {
+			return "", nil, fmt.Errorf("crd: invalid remoteRef.key %q: namespace segment before '/' must not be empty", remoteKey)
+		}
+		if name == "" {
+			return "", nil, fmt.Errorf("crd: invalid remoteRef.key %q: object name after '/' must not be empty", remoteKey)
+		}
+		if strings.Contains(name, "/") {
+			return "", nil, fmt.Errorf("crd: invalid remoteRef.key %q: must be in \"namespace/objectName\" form (exactly one '/')", remoteKey)
+		}
+		return name, &ns, nil
+	default:
+		return remoteKey, nil, nil
+	}
+}

+ 70 - 0
providers/v1/crd/key_test.go

@@ -0,0 +1,70 @@
+/*
+Copyright © The ESO 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
+
+    https://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 crd
+
+import (
+	"strings"
+	"testing"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestParseRemoteRefKey(t *testing.T) {
+	tests := []struct {
+		name        string
+		storeKind   string
+		key         string
+		wantObj     string
+		wantNS      *string
+		wantErrSubs string
+	}{
+		{name: "SecretStore bare name", storeKind: esv1.SecretStoreKind, key: "myobj", wantObj: "myobj"},
+		{name: "SecretStore rejects slash", storeKind: esv1.SecretStoreKind, key: "ns/obj", wantErrSubs: "must not contain '/'"},
+		{name: "SecretStore empty key", storeKind: esv1.SecretStoreKind, key: "", wantErrSubs: "must not be empty"},
+		{name: "ClusterSecretStore bare name", storeKind: esv1.ClusterSecretStoreKind, key: "cluster-only", wantObj: "cluster-only"},
+		{name: "ClusterSecretStore namespace/name", storeKind: esv1.ClusterSecretStoreKind, key: "default/myobj", wantObj: "myobj", wantNS: new("default")},
+		{name: "ClusterSecretStore empty namespace segment", storeKind: esv1.ClusterSecretStoreKind, key: "/obj", wantErrSubs: "namespace segment"},
+		{name: "ClusterSecretStore empty name segment", storeKind: esv1.ClusterSecretStoreKind, key: "ns/", wantErrSubs: "object name after '/'"},
+		{name: "ClusterSecretStore extra slash", storeKind: esv1.ClusterSecretStoreKind, key: "ns/foo/bar", wantErrSubs: "exactly one '/'"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			obj, ns, err := parseRemoteRefKey(tt.storeKind, tt.key)
+			if tt.wantErrSubs != "" {
+				if err == nil || !strings.Contains(err.Error(), tt.wantErrSubs) {
+					t.Fatalf("parseRemoteRefKey() error = %v, want substring %q", err, tt.wantErrSubs)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("parseRemoteRefKey() unexpected error: %v", err)
+			}
+			if obj != tt.wantObj {
+				t.Fatalf("objectName = %q, want %q", obj, tt.wantObj)
+			}
+			if tt.wantNS == nil {
+				if ns != nil {
+					t.Fatalf("keyNamespace = %v, want nil", ns)
+				}
+				return
+			}
+			if ns == nil || *ns != *tt.wantNS {
+				t.Fatalf("keyNamespace = %v, want %q", ns, *tt.wantNS)
+			}
+		})
+	}
+}

+ 384 - 0
providers/v1/crd/provider.go

@@ -0,0 +1,384 @@
+/*
+Copyright © The ESO 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
+
+    https://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 crd
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
+	authv1 "k8s.io/api/authorization/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
+)
+
+var (
+	errMissingStore       = errors.New("missing store")
+	errMissingCRDProvider = errors.New("missing CRD provider configuration")
+	errMissingKind        = errors.New("resource.kind is required")
+	errMissingVersion     = errors.New("resource.version is required")
+	errKindIsSecret       = errors.New("kind \"Secret\" is not allowed: use the Kubernetes provider to read Kubernetes Secrets")
+	errEmptyWhitelistRule = errors.New("whitelist rule must define name, namespace, or properties")
+	errNotImplemented     = errors.New("not implemented")
+	errClientNotReady     = errors.New("crd: client has no active connection; a referent ClusterSecretStore is resolved per-ExternalSecret at reconcile")
+)
+
+// isCoreV1Secret reports whether the configured resource is the core
+// Kubernetes Secret (group "" or "core", version "v1", kind "Secret"). The
+// case-insensitive Kind match guards against the lowercase / mixed-case
+// variants the CRD discovery API will canonicalise. CRDs in custom groups
+// that happen to be named "Secret" are not affected.
+func isCoreV1Secret(res esv1.CRDProviderResource) bool {
+	if !strings.EqualFold(res.Kind, "Secret") {
+		return false
+	}
+	if res.Version != "v1" {
+		return false
+	}
+	return res.Group == "" || res.Group == "core"
+}
+
+// Provider is the top-level CRD provider that implements esv1.Provider.
+type Provider struct {
+	// buildClientFn builds a controller-runtime client from the authenticated
+	// config and resolves the target resource's plural name and scope (namespaced
+	// vs cluster-scoped) via a RESTMapper. Overridable in tests without a live
+	// cluster.
+	buildClientFn func(cfg *rest.Config, res esv1.CRDProviderResource) (kclient.Client, string, bool, error)
+	// accessCheckFn verifies that the caller can perform the requested verbs
+	// on the resolved resource (used for SSAR-based preflight + per-call list
+	// checks).
+	accessCheckFn func(ctx context.Context, cfg *rest.Config, res esv1.CRDProviderResource, plural, namespace string, verbs []string) error
+}
+
+var _ esv1.Provider = &Provider{}
+
+// newProvider returns a Provider with the default (real) client builder.
+func newProvider() *Provider {
+	return &Provider{
+		buildClientFn: buildClientFromCluster,
+		accessCheckFn: ensureResourceAccess,
+	}
+}
+
+// Capabilities returns ReadOnly - this provider never writes secrets.
+func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
+	return esv1.SecretStoreReadOnly
+}
+
+// NewClient constructs a CRD client from the store configuration.
+func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
+	ctrlCfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, fmt.Errorf("crd: failed to get kubeconfig: %w", err)
+	}
+	clientset, err := kubernetes.NewForConfig(ctrlCfg)
+	if err != nil {
+		return nil, fmt.Errorf("crd: failed to create kubernetes clientset: %w", err)
+	}
+	return p.newClient(ctx, store, kube, clientset, namespace)
+}
+
+// newClient builds the CRD provider client. Every store authenticates the same
+// way as the Kubernetes provider: via server + auth (serviceAccount, token, or
+// cert) or a kubeconfig authRef. In-cluster stores omit server (the URL defaults
+// to kubernetes.default) and set auth.serviceAccount.
+func (p *Provider) newClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, clientset kubernetes.Interface, namespace string) (esv1.SecretsClient, error) {
+	provSpec, err := getProvider(store)
+	if err != nil {
+		return nil, err
+	}
+
+	storeKind := store.GetKind()
+
+	// A referent ClusterSecretStore (auth without an explicit namespace) resolves
+	// its ServiceAccount in the consuming ExternalSecret's namespace, unknown ("")
+	// at store-validation time. Return a stub so validation passes; the operational
+	// client is rebuilt per-ExternalSecret at reconcile, when the namespace is known.
+	if storeKind == esv1.ClusterSecretStoreKind && namespace == "" && esutils.IsReferentKubernetesAuth(provSpec.Auth) {
+		return &Client{store: provSpec, storeKind: storeKind, referent: true}, nil
+	}
+
+	cfg, err := esutils.BuildRESTConfigFromKubernetesConnection(
+		ctx,
+		kube,
+		clientset.CoreV1(),
+		storeKind,
+		namespace,
+		provSpec.Server,
+		provSpec.Auth,
+		provSpec.AuthRef,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("crd: failed to prepare api connection: %w", err)
+	}
+	return p.newClientWithRESTConfig(ctx, store, cfg, namespace)
+}
+
+// Client holds the runtime state for a single SecretStore/ClusterSecretStore.
+type Client struct {
+	store *esv1.CRDProvider
+	// kube is a controller-runtime client bound to the store's authenticated
+	// connection. Reads target arbitrary CRs as unstructured objects; the client's
+	// RESTMapper resolves GroupVersionKind to the correct resource and scope.
+	kube      kclient.Client
+	namespace string
+	// namespaced is true when the API resource is namespace-scoped (from the RESTMapper).
+	namespaced bool
+	// storeKind is SecretStore or ClusterSecretStore (controls remoteRef.key parsing).
+	storeKind string
+	// whitelistRules is the pre-compiled form of store.Whitelist.Rules, built once
+	// at construction time so per-read calls do not recompile regexes.
+	whitelistRules []compiledWhitelistRule
+	// listAccessCheck performs a SelfSubjectAccessReview for "list" against
+	// the same scope used for actual listing. Called by GetAllSecrets so the
+	// "list" permission is only required when listing is actually used.
+	// nil when no access check is configured (test/no-op).
+	listAccessCheck func(ctx context.Context) error
+	// referent marks a stub client returned at store-validation time for a
+	// referent ClusterSecretStore (no explicit SA namespace). It has no
+	// kube client and only answers Validate() with an "unknown" result; the
+	// operational client is rebuilt per-ExternalSecret at reconcile.
+	referent bool
+}
+
+var _ esv1.SecretsClient = &Client{}
+
+// newClientWithRESTConfig builds the Client from a fully authenticated REST config.
+// Exposed for tests that inject a token or explicit connection config without a live cluster.
+func (p *Provider) newClientWithRESTConfig(ctx context.Context, store esv1.GenericStore, authedCfg *rest.Config, targetNamespace string) (esv1.SecretsClient, error) {
+	provSpec, err := getProvider(store)
+	if err != nil {
+		return nil, err
+	}
+
+	// Build the controller-runtime client and resolve the requested
+	// group/version/kind to its plural resource name and scope via a RESTMapper.
+	// A mapping error means the kind is not registered in the target cluster.
+	kubeClient, plural, resourceNamespaced, err := p.buildClientFn(authedCfg, provSpec.Resource)
+	if err != nil {
+		return nil, err
+	}
+	// accessNS is the namespace passed to SelfSubjectAccessReview. For a
+	// ClusterSecretStore listing a namespaced resource the controller operates
+	// across all namespaces; falsely scoping the SSAR to the controller's own
+	// namespace would let a SA with only-local access pass preflight and then
+	// fail at request time. Use "" (cluster-wide).
+	accessNS := targetNamespace
+	if !resourceNamespaced {
+		accessNS = ""
+	} else if store.GetKind() == esv1.ClusterSecretStoreKind {
+		accessNS = ""
+	}
+	if p.accessCheckFn != nil {
+		// Preflight checks only "get". The "list" permission is checked lazily
+		// in GetAllSecrets so a SA that only ever does GetSecret does not need
+		// list rights at store bootstrap time.
+		if err := p.accessCheckFn(ctx, authedCfg, provSpec.Resource, plural, accessNS, []string{"get"}); err != nil {
+			return nil, err
+		}
+	}
+
+	whitelistRules, err := compileWhitelistRules(provSpec.Whitelist)
+	if err != nil {
+		return nil, err
+	}
+
+	// Bind the list-permission preflight as a closure on the Client so
+	// GetAllSecrets can invoke it without holding onto cfg/plural directly.
+	var listAccessCheck func(ctx context.Context) error
+	if p.accessCheckFn != nil {
+		fn := p.accessCheckFn
+		res := provSpec.Resource
+		listAccessCheck = func(ctx context.Context) error {
+			return fn(ctx, authedCfg, res, plural, accessNS, []string{"list"})
+		}
+	}
+
+	return &Client{
+		store:           provSpec,
+		kube:            kubeClient,
+		namespace:       targetNamespace,
+		namespaced:      resourceNamespaced,
+		storeKind:       store.GetKind(),
+		whitelistRules:  whitelistRules,
+		listAccessCheck: listAccessCheck,
+	}, nil
+}
+
+// PushSecret is not supported by the CRD provider (read-only).
+func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
+	return fmt.Errorf("crd: PushSecret: %w", errNotImplemented)
+}
+
+// DeleteSecret is not supported by the CRD provider (read-only).
+func (c *Client) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
+	return fmt.Errorf("crd: DeleteSecret: %w", errNotImplemented)
+}
+
+// buildClientFromCluster builds a controller-runtime client bound to the
+// authenticated connection and resolves the requested group/version/kind to its
+// plural resource name and scope via a dynamic RESTMapper. The RESTMapping also
+// serves as registration validation: an unregistered kind yields an error. This
+// matches how the rest of ESO reads arbitrary custom resources (unstructured
+// objects through a controller-runtime client) rather than a raw dynamic client.
+func buildClientFromCluster(cfg *rest.Config, res esv1.CRDProviderResource) (kclient.Client, string, bool, error) {
+	httpClient, err := rest.HTTPClientFor(cfg)
+	if err != nil {
+		return nil, "", false, fmt.Errorf("crd: failed to create http client: %w", err)
+	}
+	mapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient)
+	if err != nil {
+		return nil, "", false, fmt.Errorf("crd: failed to create rest mapper: %w", err)
+	}
+	mapping, err := mapper.RESTMapping(schema.GroupKind{Group: res.Group, Kind: res.Kind}, res.Version)
+	if err != nil {
+		return nil, "", false, fmt.Errorf("crd: group %q version %q kind %q is not registered in the cluster: %w", res.Group, res.Version, res.Kind, err)
+	}
+	c, err := kclient.New(cfg, kclient.Options{Mapper: mapper, HTTPClient: httpClient})
+	if err != nil {
+		return nil, "", false, fmt.Errorf("crd: failed to create client: %w", err)
+	}
+	namespaced := mapping.Scope.Name() == meta.RESTScopeNameNamespace
+	return c, mapping.Resource.Resource, namespaced, nil
+}
+
+// ensureResourceAccess performs a SelfSubjectAccessReview for each of the
+// supplied verbs against the target resource, returning the first denial as an
+// error. Callers pass {"get"} at preflight and {"list"} from GetAllSecrets so
+// "list" permission is only required for callers that actually list.
+func ensureResourceAccess(ctx context.Context, cfg *rest.Config, res esv1.CRDProviderResource, plural, namespace string, verbs []string) error {
+	cs, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return fmt.Errorf("crd: failed to create kubernetes client for access review: %w", err)
+	}
+
+	for _, verb := range verbs {
+		review := &authv1.SelfSubjectAccessReview{
+			Spec: authv1.SelfSubjectAccessReviewSpec{
+				ResourceAttributes: &authv1.ResourceAttributes{
+					Group:     res.Group,
+					Version:   res.Version,
+					Resource:  plural,
+					Verb:      verb,
+					Namespace: namespace,
+				},
+			},
+		}
+
+		resp, err := cs.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, review, metav1.CreateOptions{})
+		if err != nil {
+			return fmt.Errorf("crd: failed to verify %q permission for resource %q: %w", verb, plural, err)
+		}
+		if !resp.Status.Allowed {
+			return fmt.Errorf("crd: serviceaccount is not allowed to %q resource %q in apiGroup %q", verb, plural, res.Group)
+		}
+	}
+
+	return nil
+}
+
+// ValidateStore checks the store configuration.
+func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.CRD == nil {
+		return nil, nil
+	}
+	prov := spec.Provider.CRD
+
+	// server.url requires credentials (auth or authRef) to connect with.
+	if prov.Server.URL != "" && prov.Auth == nil && prov.AuthRef == nil {
+		return nil, errors.New("server.url requires auth or authRef when set")
+	}
+
+	// The server/auth/authRef fields reuse the Kubernetes provider's connection
+	// types, so their validation is shared via esutils rather than duplicated.
+	warnings, err := esutils.ValidateKubernetesConnection(store, prov.Server, prov.Auth, prov.AuthRef)
+	if err != nil {
+		return warnings, err
+	}
+
+	if prov.Resource.Version == "" {
+		return nil, errMissingVersion
+	}
+	if prov.Resource.Kind == "" {
+		return nil, errMissingKind
+	}
+	// Only block reading the core v1 Kubernetes Secret resource; CRDs that
+	// happen to be named "Secret" in a different API group are legitimate.
+	if isCoreV1Secret(prov.Resource) {
+		return nil, errKindIsSecret
+	}
+	if _, err := compileWhitelistRules(prov.Whitelist); err != nil {
+		return warnings, err
+	}
+	// A SecretStore only ever reads its own namespace, so a whitelist rule that
+	// constrains the namespace can never match: it looks like a restriction but
+	// silently denies everything. Reject it at admission rather than letting the
+	// misconfiguration surface as empty reads later. Namespace rules remain valid
+	// for a ClusterSecretStore, which reads across namespaces.
+	if store.GetKind() == esv1.SecretStoreKind && prov.Whitelist != nil {
+		for i, r := range prov.Whitelist.Rules {
+			if r.Namespace != "" {
+				return warnings, fmt.Errorf("crd: whitelist.rules[%d].namespace is not supported for a SecretStore (it only reads its own namespace); remove it or use a ClusterSecretStore", i)
+			}
+		}
+	}
+	return warnings, nil
+}
+
+// getProvider extracts the CRDProvider spec from a GenericStore, returning an
+// error if the store is nil or the CRD provider block is missing.
+func getProvider(store esv1.GenericStore) (*esv1.CRDProvider, error) {
+	if store == nil {
+		return nil, errMissingStore
+	}
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.CRD == nil {
+		return nil, errMissingCRDProvider
+	}
+	return spec.Provider.CRD, nil
+}
+
+// NewProvider creates a new Provider instance.
+func NewProvider() esv1.Provider {
+	return newProvider()
+}
+
+// ProviderSpec returns the SecretStoreProvider spec used for registration.
+func ProviderSpec() *esv1.SecretStoreProvider {
+	return &esv1.SecretStoreProvider{
+		CRD: &esv1.CRDProvider{},
+	}
+}
+
+// MaintenanceStatus returns the maintenance status for this provider.
+func MaintenanceStatus() esv1.MaintenanceStatus {
+	return esv1.MaintenanceStatusMaintained
+}

+ 510 - 0
providers/v1/crd/provider_test.go

@@ -0,0 +1,510 @@
+/*
+Copyright © The ESO 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
+
+    https://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 crd
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+	"testing"
+
+	"k8s.io/client-go/rest"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// fakeBuildClient returns a buildClientFn that succeeds with the given plural and
+// scope, backed by a controller-runtime fake client (no objects). This bypasses
+// both the RESTMapper and the real cluster.
+func fakeBuildClient(plural string, namespaced bool) func(*rest.Config, esv1.CRDProviderResource) (kclient.Client, string, bool, error) {
+	return func(_ *rest.Config, _ esv1.CRDProviderResource) (kclient.Client, string, bool, error) {
+		return fakeCRDClient(namespaced), plural, namespaced, nil
+	}
+}
+
+// fakeBuildClientErr returns a buildClientFn that always fails with the given error.
+func fakeBuildClientErr(err error) func(*rest.Config, esv1.CRDProviderResource) (kclient.Client, string, bool, error) {
+	return func(_ *rest.Config, _ esv1.CRDProviderResource) (kclient.Client, string, bool, error) {
+		return nil, "", true, err
+	}
+}
+
+// providerWithFakeClient returns a Provider with a fake client builder injected,
+// bypassing both token fetch and the real cluster.
+// namespaced defaults to true when omitted (namespace-scoped CRD).
+func providerWithFakeClient(plural string, namespaced ...bool) *Provider {
+	ns := true
+	if len(namespaced) > 0 {
+		ns = namespaced[0]
+	}
+	return &Provider{buildClientFn: fakeBuildClient(plural, ns)}
+}
+
+func makeStoreWithCRDProvider(prov *esv1.CRDProvider) esv1.GenericStore {
+	return &esv1.SecretStore{
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				CRD: prov,
+			},
+		},
+	}
+}
+
+func makeClusterStoreWithCRDProvider(prov *esv1.CRDProvider) esv1.GenericStore {
+	return &esv1.ClusterSecretStore{
+		Spec: esv1.SecretStoreSpec{
+			Provider: &esv1.SecretStoreProvider{
+				CRD: prov,
+			},
+		},
+	}
+}
+
+// widgetResource is a valid CRDProviderResource used across tests.
+var widgetResource = esv1.CRDProviderResource{
+	Group:   "example.io",
+	Version: "v1alpha1",
+	Kind:    "Widget",
+}
+
+// saAuth builds a KubernetesAuth that authenticates as the given ServiceAccount,
+// mirroring the in-cluster connection model shared with the Kubernetes provider.
+func saAuth(name string) *esv1.KubernetesAuth {
+	return &esv1.KubernetesAuth{
+		ServiceAccount: &esmeta.ServiceAccountSelector{Name: name},
+	}
+}
+
+// defaultRESTCfg returns a minimal REST config used in provider construction tests.
+func defaultRESTCfg() *rest.Config {
+	return &rest.Config{Host: "https://example.com", BearerToken: "tok"}
+}
+
+func TestProviderCapabilities(t *testing.T) {
+	p := &Provider{}
+	if got := p.Capabilities(); got != esv1.SecretStoreReadOnly {
+		t.Fatalf("Capabilities() = %v, want %v", got, esv1.SecretStoreReadOnly)
+	}
+}
+
+func TestValidateStore(t *testing.T) {
+	tests := []struct {
+		name              string
+		store             esv1.GenericStore
+		wantErr           error
+		wantMsg           string
+		wantWarnSubstring string
+	}{
+		{
+			name:  "missing provider config is ignored",
+			store: &esv1.SecretStore{},
+		},
+		{
+			// In-cluster: auth.serviceAccount with no server is the canonical
+			// local-read configuration; the URL defaults to kubernetes.default.
+			name: "in-cluster auth.serviceAccount without server is accepted",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Auth:     saAuth("reader"),
+				Resource: widgetResource,
+			}),
+		},
+		{
+			name:    "missing version",
+			store:   makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "example.io", Kind: "Widget"}}),
+			wantErr: errMissingVersion,
+		},
+		{
+			name:    "missing kind",
+			store:   makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "example.io", Version: "v1alpha1"}}),
+			wantErr: errMissingKind,
+		},
+		{
+			name:  "empty group is valid (core resource e.g. ConfigMap)",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "", Version: "v1", Kind: "ConfigMap"}}),
+		},
+		{
+			name:    "core v1 Secret is denied (exact case)",
+			store:   makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "", Version: "v1", Kind: "Secret"}}),
+			wantErr: errKindIsSecret,
+		},
+		{
+			name:    "core v1 secret is denied (lowercase)",
+			store:   makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "", Version: "v1", Kind: "secret"}}),
+			wantErr: errKindIsSecret,
+		},
+		{
+			name:    "core v1 SECRET is denied (uppercase)",
+			store:   makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "", Version: "v1", Kind: "SECRET"}}),
+			wantErr: errKindIsSecret,
+		},
+		{
+			// Same Kind name on a different API group is a legitimate CRD;
+			// only the core v1 Secret is blocked.
+			name:  "Secret kind in a non-core group is allowed",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "example.io", Version: "v1", Kind: "Secret"}}),
+		},
+		{
+			// Different version of core "Secret" — also legitimate (no such
+			// thing exists today, but the block is intentionally narrow).
+			name:  "core v2 Secret is allowed",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "", Version: "v2", Kind: "Secret"}}),
+		},
+		{
+			name:    "core group alias \"core\" still denies v1 Secret",
+			store:   makeStoreWithCRDProvider(&esv1.CRDProvider{Resource: esv1.CRDProviderResource{Group: "core", Version: "v1", Kind: "Secret"}}),
+			wantErr: errKindIsSecret,
+		},
+		{
+			name: "invalid whitelist name regex",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource:  widgetResource,
+				Whitelist: &esv1.CRDProviderWhitelist{Rules: []esv1.CRDProviderWhitelistRule{{Name: "("}}},
+			}),
+			wantMsg: "invalid whitelist.rules[0].name regex",
+		},
+		{
+			name: "invalid whitelist property regex",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource:  widgetResource,
+				Whitelist: &esv1.CRDProviderWhitelist{Rules: []esv1.CRDProviderWhitelistRule{{Properties: []string{"("}}}},
+			}),
+			wantMsg: "invalid whitelist.rules[0].properties[0] regex",
+		},
+		{
+			name: "empty whitelist rule is invalid",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource:  widgetResource,
+				Whitelist: &esv1.CRDProviderWhitelist{Rules: []esv1.CRDProviderWhitelistRule{{}}},
+			}),
+			wantErr: errEmptyWhitelistRule,
+		},
+		{
+			name: "valid config",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Auth:     saAuth("reader"),
+				Resource: widgetResource,
+				Whitelist: &esv1.CRDProviderWhitelist{Rules: []esv1.CRDProviderWhitelistRule{{
+					Name:       "^app-.*$",
+					Properties: []string{"^spec\\..+$"},
+				}}},
+			}),
+		},
+		{
+			name: "remote server with token auth is accepted",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource: widgetResource,
+				Server: esv1.KubernetesServer{
+					URL:      "https://k8s.example",
+					CABundle: []byte("fake-ca"),
+				},
+				Auth: &esv1.KubernetesAuth{
+					Token: &esv1.TokenAuth{
+						BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "k"},
+					},
+				},
+			}),
+		},
+		{
+			// server.url set without any credentials is a misconfiguration.
+			name: "server.url without auth or authRef is rejected",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource: widgetResource,
+				Server:   esv1.KubernetesServer{URL: "https://k8s.example"},
+			}),
+			wantMsg: "server.url requires auth or authRef",
+		},
+		{
+			// authRef embeds a kubeconfig with the server address, so a
+			// separate server.url is not required.
+			name: "authRef without server.url is allowed",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource: widgetResource,
+				AuthRef:  &esmeta.SecretKeySelector{Name: "kubeconfig", Key: "config"},
+			}),
+		},
+		{
+			name: "explicit connection TLS CA warning",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Resource: widgetResource,
+				Server: esv1.KubernetesServer{
+					URL: "https://k8s.example",
+				},
+				Auth: &esv1.KubernetesAuth{
+					Token: &esv1.TokenAuth{
+						BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "k"},
+					},
+				},
+			}),
+			wantWarnSubstring: "system certificate roots",
+		},
+		{
+			name: "ClusterSecretStore CAProvider needs namespace",
+			store: &esv1.ClusterSecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						CRD: &esv1.CRDProvider{
+							Resource: widgetResource,
+							Server: esv1.KubernetesServer{
+								URL: "https://x",
+								CAProvider: &esv1.CAProvider{
+									Type: esv1.CAProviderTypeSecret,
+									Name: "ca",
+									Key:  "k",
+								},
+							},
+							Auth: &esv1.KubernetesAuth{
+								Token: &esv1.TokenAuth{
+									BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "k"},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: "CAProvider.namespace must not be empty",
+		},
+		{
+			name: "SecretStore rejects CAProvider.namespace",
+			store: &esv1.SecretStore{
+				Spec: esv1.SecretStoreSpec{
+					Provider: &esv1.SecretStoreProvider{
+						CRD: &esv1.CRDProvider{
+							Resource: widgetResource,
+							Server: esv1.KubernetesServer{
+								URL: "https://x",
+								CAProvider: &esv1.CAProvider{
+									Type:      esv1.CAProviderTypeSecret,
+									Name:      "ca",
+									Key:       "k",
+									Namespace: new("ns"),
+								},
+							},
+							Auth: &esv1.KubernetesAuth{
+								Token: &esv1.TokenAuth{
+									BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "k"},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantMsg: "CAProvider.namespace must be empty with SecretStore",
+		},
+		{
+			// A SecretStore reads only its own namespace, so a namespace rule
+			// can never match and is rejected to surface the misconfiguration.
+			name: "SecretStore rejects whitelist namespace rule",
+			store: makeStoreWithCRDProvider(&esv1.CRDProvider{
+				Auth:      saAuth("reader"),
+				Resource:  widgetResource,
+				Whitelist: &esv1.CRDProviderWhitelist{Rules: []esv1.CRDProviderWhitelistRule{{Namespace: "^prod$"}}},
+			}),
+			wantMsg: "whitelist.rules[0].namespace is not supported for a SecretStore",
+		},
+		{
+			// A ClusterSecretStore reads across namespaces, so a namespace rule
+			// is a legitimate restriction and must be accepted.
+			name: "ClusterSecretStore allows whitelist namespace rule",
+			store: makeClusterStoreWithCRDProvider(&esv1.CRDProvider{
+				Auth:      saAuth("reader"),
+				Resource:  widgetResource,
+				Whitelist: &esv1.CRDProviderWhitelist{Rules: []esv1.CRDProviderWhitelistRule{{Namespace: "^prod$"}}},
+			}),
+		},
+	}
+
+	p := &Provider{}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			warnings, err := p.ValidateStore(tt.store)
+			if tt.wantErr != nil || tt.wantMsg != "" {
+				if err == nil {
+					t.Fatalf("ValidateStore() error = nil, want error")
+				}
+				if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
+					t.Fatalf("ValidateStore() error = %v, want %v", err, tt.wantErr)
+				}
+				if tt.wantMsg != "" && !strings.Contains(err.Error(), tt.wantMsg) {
+					t.Fatalf("ValidateStore() error = %q, want substring %q", err.Error(), tt.wantMsg)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("ValidateStore() unexpected error: %v", err)
+			}
+			if tt.wantWarnSubstring != "" {
+				var b strings.Builder
+				for _, w := range warnings {
+					b.WriteString(w)
+				}
+				if !strings.Contains(b.String(), tt.wantWarnSubstring) {
+					t.Fatalf("ValidateStore() warnings = %v, want substring %q", warnings, tt.wantWarnSubstring)
+				}
+			}
+		})
+	}
+}
+
+func TestGetProvider(t *testing.T) {
+	tests := []struct {
+		name    string
+		store   esv1.GenericStore
+		wantErr error
+	}{
+		{name: "nil store", store: nil, wantErr: errMissingStore},
+		{name: "missing provider", store: &esv1.SecretStore{}, wantErr: errMissingCRDProvider},
+		{name: "missing crd provider", store: &esv1.SecretStore{Spec: esv1.SecretStoreSpec{Provider: &esv1.SecretStoreProvider{}}}, wantErr: errMissingCRDProvider},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := getProvider(tt.store)
+			if !errors.Is(err, tt.wantErr) {
+				t.Fatalf("getProvider() error = %v, want %v", err, tt.wantErr)
+			}
+		})
+	}
+
+	t.Run("valid store", func(t *testing.T) {
+		want := &esv1.CRDProvider{Auth: saAuth("reader"), Resource: widgetResource}
+		got, err := getProvider(makeStoreWithCRDProvider(want))
+		if err != nil {
+			t.Fatalf("getProvider() unexpected error: %v", err)
+		}
+		if got != want {
+			t.Fatalf("getProvider() returned wrong provider pointer")
+		}
+	})
+}
+
+func TestProviderMetadata(t *testing.T) {
+	if _, ok := NewProvider().(*Provider); !ok {
+		t.Fatalf("NewProvider() did not return *Provider")
+	}
+
+	spec := ProviderSpec()
+	if spec == nil || spec.CRD == nil {
+		t.Fatalf("ProviderSpec() returned nil CRD provider")
+	}
+
+	if got := MaintenanceStatus(); got != esv1.MaintenanceStatusMaintained {
+		t.Fatalf("MaintenanceStatus() = %v, want %v", got, esv1.MaintenanceStatusMaintained)
+	}
+}
+
+func TestNewClientInternal(t *testing.T) {
+	ctx := context.Background()
+	store := makeStoreWithCRDProvider(&esv1.CRDProvider{Auth: saAuth("reader"), Resource: widgetResource})
+
+	t.Run("newClient returns getProvider error on nil store", func(t *testing.T) {
+		_, err := providerWithFakeClient("widgets").newClient(ctx, nil, nil, nil, "default")
+		if !errors.Is(err, errMissingStore) {
+			t.Fatalf("newClient() error = %v, want %v", err, errMissingStore)
+		}
+	})
+
+	t.Run("referent ClusterSecretStore returns a validation stub at store bootstrap", func(t *testing.T) {
+		// ClusterSecretStore, auth.serviceAccount with no explicit namespace, and
+		// an empty namespace (the store-validation call). newClient must short
+		// circuit before building any REST connection and return a referent stub
+		// whose Validate() reports "unknown".
+		clusterStore := makeClusterStoreWithCRDProvider(&esv1.CRDProvider{
+			Auth:     saAuth("reader"),
+			Resource: widgetResource,
+		})
+		client, err := (&Provider{}).newClient(ctx, clusterStore, nil, nil, "")
+		if err != nil {
+			t.Fatalf("newClient() unexpected error: %v", err)
+		}
+		c, ok := client.(*Client)
+		if !ok {
+			t.Fatalf("returned %T, want *Client", client)
+		}
+		if !c.referent {
+			t.Fatalf("expected a referent stub client")
+		}
+		if c.kube != nil {
+			t.Fatalf("referent stub must not carry a client")
+		}
+		res, err := c.Validate()
+		if err != nil {
+			t.Fatalf("Validate() unexpected error: %v", err)
+		}
+		if res != esv1.ValidationResultUnknown {
+			t.Fatalf("Validate() = %v, want %v", res, esv1.ValidationResultUnknown)
+		}
+	})
+
+	t.Run("newClientWithRESTConfig returns getProvider error on nil store", func(t *testing.T) {
+		_, err := providerWithFakeClient("widgets").newClientWithRESTConfig(context.Background(), nil, defaultRESTCfg(), "default")
+		if !errors.Is(err, errMissingStore) {
+			t.Fatalf("newClientWithRESTConfig() error = %v, want %v", err, errMissingStore)
+		}
+	})
+
+	t.Run("resource resolution error is propagated", func(t *testing.T) {
+		mapErr := fmt.Errorf("group/version/kind not registered")
+		_, err := (&Provider{buildClientFn: fakeBuildClientErr(mapErr)}).newClientWithRESTConfig(context.Background(), store, defaultRESTCfg(), "default")
+		if !errors.Is(err, mapErr) {
+			t.Fatalf("newClientWithRESTConfig() error = %v, want %v", err, mapErr)
+		}
+	})
+
+	t.Run("creates client for namespaced store", func(t *testing.T) {
+		client, err := providerWithFakeClient("widgets").newClientWithRESTConfig(context.Background(), store, defaultRESTCfg(), "app-ns")
+		if err != nil {
+			t.Fatalf("newClientWithRESTConfig() unexpected error: %v", err)
+		}
+		c, ok := client.(*Client)
+		if !ok {
+			t.Fatalf("returned %T, want *Client", client)
+		}
+		if c.store.Auth.ServiceAccount.Name != "reader" {
+			t.Fatalf("client SA = %q, want %q", c.store.Auth.ServiceAccount.Name, "reader")
+		}
+		if c.namespace != "app-ns" || c.kube == nil {
+			t.Fatalf("client: ns=%q kube=%v", c.namespace, c.kube)
+		}
+	})
+
+	t.Run("creates client for cluster store (empty namespace)", func(t *testing.T) {
+		client, err := providerWithFakeClient("widgets").newClientWithRESTConfig(context.Background(), store, defaultRESTCfg(), "")
+		if err != nil {
+			t.Fatalf("newClientWithRESTConfig() unexpected error: %v", err)
+		}
+		c, ok := client.(*Client)
+		if !ok {
+			t.Fatalf("returned %T, want *Client", client)
+		}
+		if c.namespace != "" {
+			t.Fatalf("client namespace = %q, want empty", c.namespace)
+		}
+	})
+
+	t.Run("cluster-scoped resource sets namespaced false on client", func(t *testing.T) {
+		client, err := providerWithFakeClient("clusterdbspecs", false).newClientWithRESTConfig(context.Background(), store, defaultRESTCfg(), "default")
+		if err != nil {
+			t.Fatalf("newClientWithRESTConfig() unexpected error: %v", err)
+		}
+		if client.(*Client).namespaced {
+			t.Fatalf("expected cluster-scoped client (namespaced=false)")
+		}
+	})
+}

+ 2 - 27
providers/v1/kubernetes/provider.go

@@ -32,6 +32,7 @@ import (
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
 )
 
 // https://github.com/external-secrets/external-secrets/issues/644
@@ -126,7 +127,7 @@ func (p *Provider) newClient(ctx context.Context, store esv1.GenericStore, ctrlC
 
 	// allow SecretStore controller validation to pass
 	// when using referent namespace.
-	if client.storeKind == esv1.ClusterSecretStoreKind && client.namespace == "" && isReferentSpec(storeSpecKubernetes) {
+	if client.storeKind == esv1.ClusterSecretStoreKind && client.namespace == "" && esutils.IsReferentKubernetesAuth(storeSpecKubernetes.Auth) {
 		return client, nil
 	}
 
@@ -153,32 +154,6 @@ func (c *Client) secretsClientFor(namespace string) KClient {
 	return c.userSecretClient
 }
 
-func isReferentSpec(prov *esv1.KubernetesProvider) bool {
-	if prov.Auth == nil {
-		return false
-	}
-
-	if prov.Auth.Cert != nil {
-		if prov.Auth.Cert.ClientCert.Namespace == nil {
-			return true
-		}
-		if prov.Auth.Cert.ClientKey.Namespace == nil {
-			return true
-		}
-	}
-	if prov.Auth.ServiceAccount != nil {
-		if prov.Auth.ServiceAccount.Namespace == nil {
-			return true
-		}
-	}
-	if prov.Auth.Token != nil {
-		if prov.Auth.Token.BearerToken.Namespace == nil {
-			return true
-		}
-	}
-	return false
-}
-
 // Close cleans up any resources used by the Kubernetes provider.
 func (p *Provider) Close(_ context.Context) error {
 	return nil

+ 6 - 49
providers/v1/kubernetes/validate.go

@@ -32,56 +32,13 @@ import (
 	"github.com/external-secrets/external-secrets/runtime/metrics"
 )
 
-const (
-	warnNoCAConfigured = "No caBundle or caProvider specified; TLS connections will use system certificate roots."
-)
-
 // ValidateStore validates the Kubernetes SecretStore configuration.
 func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
-	storeSpec := store.GetSpec()
-	k8sSpec := storeSpec.Provider.Kubernetes
-	var warnings admission.Warnings
-	if k8sSpec.AuthRef == nil && k8sSpec.Server.CABundle == nil && k8sSpec.Server.CAProvider == nil {
-		warnings = append(warnings, warnNoCAConfigured)
-	}
-	if store.GetObjectKind().GroupVersionKind().Kind == esv1.ClusterSecretStoreKind &&
-		k8sSpec.Server.CAProvider != nil &&
-		k8sSpec.Server.CAProvider.Namespace == nil {
-		return warnings, errors.New("CAProvider.namespace must not be empty with ClusterSecretStore")
-	}
-	if store.GetObjectKind().GroupVersionKind().Kind == esv1.SecretStoreKind &&
-		k8sSpec.Server.CAProvider != nil &&
-		k8sSpec.Server.CAProvider.Namespace != nil {
-		return warnings, errors.New("CAProvider.namespace must be empty with SecretStore")
-	}
-	if k8sSpec.Auth != nil && k8sSpec.Auth.Cert != nil {
-		if k8sSpec.Auth.Cert.ClientCert.Name == "" {
-			return warnings, errors.New("ClientCert.Name cannot be empty")
-		}
-		if k8sSpec.Auth.Cert.ClientCert.Key == "" {
-			return warnings, errors.New("ClientCert.Key cannot be empty")
-		}
-		if err := esutils.ValidateSecretSelector(store, k8sSpec.Auth.Cert.ClientCert); err != nil {
-			return warnings, err
-		}
-	}
-	if k8sSpec.Auth != nil && k8sSpec.Auth.Token != nil {
-		if k8sSpec.Auth.Token.BearerToken.Name == "" {
-			return warnings, errors.New("BearerToken.Name cannot be empty")
-		}
-		if k8sSpec.Auth.Token.BearerToken.Key == "" {
-			return warnings, errors.New("BearerToken.Key cannot be empty")
-		}
-		if err := esutils.ValidateSecretSelector(store, k8sSpec.Auth.Token.BearerToken); err != nil {
-			return warnings, err
-		}
-	}
-	if k8sSpec.Auth != nil && k8sSpec.Auth.ServiceAccount != nil {
-		if err := esutils.ValidateReferentServiceAccountSelector(store, *k8sSpec.Auth.ServiceAccount); err != nil {
-			return warnings, err
-		}
-	}
-	return warnings, nil
+	k8sSpec := store.GetSpec().Provider.Kubernetes
+	// server/auth/authRef validation is shared with the CRD provider, which
+	// reuses the same connection types.
+	warnings, err := esutils.ValidateKubernetesConnection(store, k8sSpec.Server, k8sSpec.Auth, k8sSpec.AuthRef)
+	return warnings, err
 }
 
 // Validate checks if the client has the necessary permissions to access secrets in the target namespace.
@@ -89,7 +46,7 @@ func (c *Client) Validate() (esv1.ValidationResult, error) {
 	// when using referent namespace we can not validate the token
 	// because the namespace is not known yet when Validate() is called
 	// from the SecretStore controller.
-	if c.storeKind == esv1.ClusterSecretStoreKind && isReferentSpec(c.store) {
+	if c.storeKind == esv1.ClusterSecretStoreKind && esutils.IsReferentKubernetesAuth(c.store.Auth) {
 		return esv1.ValidationResultUnknown, nil
 	}
 	ctx := context.Background()

+ 3 - 2
providers/v1/kubernetes/validate_test.go

@@ -27,6 +27,7 @@ import (
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/runtime/esutils"
 )
 
 type fakeReviewClient struct {
@@ -367,8 +368,8 @@ func TestValidateStore(t *testing.T) {
 				if len(warnings) != 1 {
 					t.Fatalf("ProviderKubernetes.ValidateStore() expected exactly 1 warning, got %d: %v", len(warnings), warnings)
 				}
-				if warnings[0] != warnNoCAConfigured {
-					t.Errorf("ProviderKubernetes.ValidateStore() warning = %q, want %q", warnings[0], warnNoCAConfigured)
+				if warnings[0] != esutils.WarnNoCAConfigured {
+					t.Errorf("ProviderKubernetes.ValidateStore() warning = %q, want %q", warnings[0], esutils.WarnNoCAConfigured)
 				}
 			} else if len(warnings) > 0 {
 				t.Errorf("ProviderKubernetes.ValidateStore() unexpected warnings: %v", warnings)

+ 116 - 0
runtime/esutils/k8s_connection_validate.go

@@ -0,0 +1,116 @@
+/*
+Copyright © The ESO 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
+
+    https://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 esutils
+
+import (
+	"errors"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// WarnNoCAConfigured is the admission warning emitted when a Kubernetes-style
+// connection configures neither an inline CA bundle nor a CA provider, so TLS
+// falls back to the system certificate roots. Shared by the kubernetes and CRD
+// providers so the wording stays identical across both.
+const WarnNoCAConfigured = "No caBundle or caProvider specified; TLS connections will use system certificate roots."
+
+// IsReferentKubernetesAuth reports whether a Kubernetes-style auth spec uses
+// referent authentication: any credential selector whose namespace is omitted
+// is resolved against the consuming ExternalSecret's namespace, which is not
+// known at store-validation time. Shared by the kubernetes and CRD providers,
+// which both embed the KubernetesAuth type.
+func IsReferentKubernetesAuth(auth *esv1.KubernetesAuth) bool {
+	if auth == nil {
+		return false
+	}
+	if auth.Cert != nil {
+		if auth.Cert.ClientCert.Namespace == nil {
+			return true
+		}
+		if auth.Cert.ClientKey.Namespace == nil {
+			return true
+		}
+	}
+	if auth.ServiceAccount != nil {
+		if auth.ServiceAccount.Namespace == nil {
+			return true
+		}
+	}
+	if auth.Token != nil {
+		if auth.Token.BearerToken.Namespace == nil {
+			return true
+		}
+	}
+	return false
+}
+
+// ValidateKubernetesConnection validates the server/auth/authRef fields common
+// to any provider that reaches a Kubernetes API using the Kubernetes provider's
+// connection model (currently the kubernetes and CRD providers). It returns the
+// admission warnings accumulated so far alongside the first validation error, so
+// callers can surface warnings even when validation fails. The return type is a
+// plain []string, assignable to admission.Warnings, to keep this package free of
+// the webhook import.
+func ValidateKubernetesConnection(store esv1.GenericStore, server esv1.KubernetesServer, auth *esv1.KubernetesAuth, authRef *esmeta.SecretKeySelector) ([]string, error) {
+	var warnings []string
+	if authRef == nil && server.CABundle == nil && server.CAProvider == nil {
+		warnings = append(warnings, WarnNoCAConfigured)
+	}
+	// GetKind returns the store kind reliably (a constant per concrete type),
+	// unlike GetObjectKind().GroupVersionKind().Kind which depends on TypeMeta
+	// being populated on the decoded object.
+	kind := store.GetKind()
+	if kind == esv1.ClusterSecretStoreKind &&
+		server.CAProvider != nil &&
+		server.CAProvider.Namespace == nil {
+		return warnings, errors.New("CAProvider.namespace must not be empty with ClusterSecretStore")
+	}
+	if kind == esv1.SecretStoreKind &&
+		server.CAProvider != nil &&
+		server.CAProvider.Namespace != nil {
+		return warnings, errors.New("CAProvider.namespace must be empty with SecretStore")
+	}
+	if auth != nil && auth.Cert != nil {
+		if auth.Cert.ClientCert.Name == "" {
+			return warnings, errors.New("ClientCert.Name cannot be empty")
+		}
+		if auth.Cert.ClientCert.Key == "" {
+			return warnings, errors.New("ClientCert.Key cannot be empty")
+		}
+		if err := ValidateSecretSelector(store, auth.Cert.ClientCert); err != nil {
+			return warnings, err
+		}
+	}
+	if auth != nil && auth.Token != nil {
+		if auth.Token.BearerToken.Name == "" {
+			return warnings, errors.New("BearerToken.Name cannot be empty")
+		}
+		if auth.Token.BearerToken.Key == "" {
+			return warnings, errors.New("BearerToken.Key cannot be empty")
+		}
+		if err := ValidateSecretSelector(store, auth.Token.BearerToken); err != nil {
+			return warnings, err
+		}
+	}
+	if auth != nil && auth.ServiceAccount != nil {
+		if err := ValidateReferentServiceAccountSelector(store, *auth.ServiceAccount); err != nil {
+			return warnings, err
+		}
+	}
+	return warnings, nil
+}

+ 254 - 0
runtime/esutils/k8s_connection_validate_test.go

@@ -0,0 +1,254 @@
+/*
+Copyright © The ESO 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
+
+    https://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 esutils
+
+import (
+	"strings"
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// storeOfKind returns a minimal GenericStore whose GetKind() reports the
+// requested kind. TypeMeta is set as well so the selector validators, which
+// read GetObjectKind().GroupVersionKind().Kind, agree with GetKind().
+func storeOfKind(kind string) esv1.GenericStore {
+	if kind == esv1.ClusterSecretStoreKind {
+		return &esv1.ClusterSecretStore{TypeMeta: metav1.TypeMeta{Kind: kind}}
+	}
+	return &esv1.SecretStore{TypeMeta: metav1.TypeMeta{Kind: kind}}
+}
+
+func TestIsReferentKubernetesAuth(t *testing.T) {
+	tests := []struct {
+		name string
+		auth *esv1.KubernetesAuth
+		want bool
+	}{
+		{name: "nil auth", auth: nil, want: false},
+		{name: "empty auth", auth: &esv1.KubernetesAuth{}, want: false},
+		{
+			name: "cert clientCert without namespace is referent",
+			auth: &esv1.KubernetesAuth{Cert: &esv1.CertAuth{
+				ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt"},
+				ClientKey:  esmeta.SecretKeySelector{Name: "c", Key: "tls.key", Namespace: new("ns")},
+			}},
+			want: true,
+		},
+		{
+			name: "cert clientKey without namespace is referent",
+			auth: &esv1.KubernetesAuth{Cert: &esv1.CertAuth{
+				ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt", Namespace: new("ns")},
+				ClientKey:  esmeta.SecretKeySelector{Name: "c", Key: "tls.key"},
+			}},
+			want: true,
+		},
+		{
+			name: "cert with both namespaces set is not referent",
+			auth: &esv1.KubernetesAuth{Cert: &esv1.CertAuth{
+				ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt", Namespace: new("ns")},
+				ClientKey:  esmeta.SecretKeySelector{Name: "c", Key: "tls.key", Namespace: new("ns")},
+			}},
+			want: false,
+		},
+		{
+			name: "serviceAccount without namespace is referent",
+			auth: &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa"}},
+			want: true,
+		},
+		{
+			name: "serviceAccount with namespace is not referent",
+			auth: &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa", Namespace: new("ns")}},
+			want: false,
+		},
+		{
+			name: "token without namespace is referent",
+			auth: &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token"}}},
+			want: true,
+		},
+		{
+			name: "token with namespace is not referent",
+			auth: &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token", Namespace: new("ns")}}},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsReferentKubernetesAuth(tt.auth); got != tt.want {
+				t.Errorf("IsReferentKubernetesAuth() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestValidateKubernetesConnection(t *testing.T) {
+	tests := []struct {
+		name        string
+		kind        string
+		server      esv1.KubernetesServer
+		auth        *esv1.KubernetesAuth
+		authRef     *esmeta.SecretKeySelector
+		wantErr     bool
+		errContains string
+		wantWarning bool
+	}{
+		{
+			name:        "no CA and no authRef warns",
+			kind:        esv1.SecretStoreKind,
+			wantWarning: true,
+		},
+		{
+			name:    "authRef suppresses the no-CA warning",
+			kind:    esv1.SecretStoreKind,
+			authRef: &esmeta.SecretKeySelector{Name: "kubeconfig", Key: "config"},
+		},
+		{
+			name:   "CABundle suppresses the no-CA warning",
+			kind:   esv1.SecretStoreKind,
+			server: esv1.KubernetesServer{CABundle: []byte("ca")},
+		},
+		{
+			name:   "SecretStore with CAProvider (no namespace) is valid",
+			kind:   esv1.SecretStoreKind,
+			server: esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca"}},
+		},
+		{
+			name:        "ClusterSecretStore CAProvider requires namespace",
+			kind:        esv1.ClusterSecretStoreKind,
+			server:      esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca"}},
+			wantErr:     true,
+			errContains: "CAProvider.namespace must not be empty",
+		},
+		{
+			name:        "SecretStore rejects CAProvider namespace",
+			kind:        esv1.SecretStoreKind,
+			server:      esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca", Namespace: new("ns")}},
+			wantErr:     true,
+			errContains: "CAProvider.namespace must be empty",
+		},
+		{
+			name:   "ClusterSecretStore CAProvider with namespace is valid",
+			kind:   esv1.ClusterSecretStoreKind,
+			server: esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca", Namespace: new("ns")}},
+		},
+		{
+			name:        "cert missing clientCert name",
+			kind:        esv1.SecretStoreKind,
+			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:        &esv1.KubernetesAuth{Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Key: "tls.crt"}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}}},
+			wantErr:     true,
+			errContains: "ClientCert.Name cannot be empty",
+		},
+		{
+			name:        "cert missing clientCert key",
+			kind:        esv1.SecretStoreKind,
+			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:        &esv1.KubernetesAuth{Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Name: "c"}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}}},
+			wantErr:     true,
+			errContains: "ClientCert.Key cannot be empty",
+		},
+		{
+			name:   "cert valid",
+			kind:   esv1.SecretStoreKind,
+			server: esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:   &esv1.KubernetesAuth{Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt"}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}}},
+		},
+		{
+			name:        "token missing name",
+			kind:        esv1.SecretStoreKind,
+			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:        &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Key: "token"}}},
+			wantErr:     true,
+			errContains: "BearerToken.Name cannot be empty",
+		},
+		{
+			name:        "token missing key",
+			kind:        esv1.SecretStoreKind,
+			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:        &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t"}}},
+			wantErr:     true,
+			errContains: "BearerToken.Key cannot be empty",
+		},
+		{
+			name:   "token valid",
+			kind:   esv1.SecretStoreKind,
+			server: esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:   &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token"}}},
+		},
+		{
+			name:   "referent serviceAccount on ClusterSecretStore is allowed",
+			kind:   esv1.ClusterSecretStoreKind,
+			server: esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:   &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa"}},
+		},
+		{
+			// ClientCert has a cross-namespace selector on a SecretStore, which
+			// the delegated ValidateSecretSelector rejects. Exercises the cert
+			// selector error-propagation path.
+			name:   "cert selector namespace mismatch is rejected",
+			kind:   esv1.SecretStoreKind,
+			server: esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth: &esv1.KubernetesAuth{
+				Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt", Namespace: new("other")}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}},
+			},
+			wantErr: true,
+		},
+		{
+			name:    "token selector namespace mismatch is rejected",
+			kind:    esv1.SecretStoreKind,
+			server:  esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:    &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token", Namespace: new("other")}}},
+			wantErr: true,
+		},
+		{
+			name:    "serviceAccount selector namespace mismatch is rejected",
+			kind:    esv1.SecretStoreKind,
+			server:  esv1.KubernetesServer{CABundle: []byte("ca")},
+			auth:    &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa", Namespace: new("other")}},
+			wantErr: true,
+		},
+		{
+			name:        "valid config without CA warns only",
+			kind:        esv1.SecretStoreKind,
+			auth:        &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token"}}},
+			wantWarning: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			store := storeOfKind(tt.kind)
+			warnings, err := ValidateKubernetesConnection(store, tt.server, tt.auth, tt.authRef)
+			if (err != nil) != tt.wantErr {
+				t.Fatalf("ValidateKubernetesConnection() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if tt.errContains != "" && (err == nil || !strings.Contains(err.Error(), tt.errContains)) {
+				t.Errorf("ValidateKubernetesConnection() error = %v, want contains %q", err, tt.errContains)
+			}
+			if tt.wantWarning {
+				if len(warnings) != 1 || warnings[0] != WarnNoCAConfigured {
+					t.Errorf("ValidateKubernetesConnection() warnings = %v, want [%q]", warnings, WarnNoCAConfigured)
+				}
+			} else if len(warnings) > 0 {
+				t.Errorf("ValidateKubernetesConnection() unexpected warnings: %v", warnings)
+			}
+		})
+	}
+}

+ 41 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -275,6 +275,47 @@ spec:
         namespace: string
         type: "Secret" # "Secret", "ConfigMap"
       url: string
+    crd:
+      auth:
+        cert:
+          clientCert:
+            key: string
+            name: string
+            namespace: string
+          clientKey:
+            key: string
+            name: string
+            namespace: string
+        serviceAccount:
+          audiences: [] # minItems 0 of type string
+          name: string
+          namespace: string
+        token:
+          bearerToken:
+            key: string
+            name: string
+            namespace: string
+      authRef:
+        key: string
+        name: string
+        namespace: string
+      resource:
+        group: string
+        kind: string
+        version: string
+      server:
+        caBundle: c3RyaW5n
+        caProvider:
+          key: string
+          name: string
+          namespace: string
+          type: "Secret" # "Secret", "ConfigMap"
+        url: "kubernetes.default"
+      whitelist:
+        rules:
+        - name: string
+          namespace: string
+          properties: [] # minItems 0 of type string
     delinea:
       clientId:
         secretRef:

+ 41 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -275,6 +275,47 @@ spec:
         namespace: string
         type: "Secret" # "Secret", "ConfigMap"
       url: string
+    crd:
+      auth:
+        cert:
+          clientCert:
+            key: string
+            name: string
+            namespace: string
+          clientKey:
+            key: string
+            name: string
+            namespace: string
+        serviceAccount:
+          audiences: [] # minItems 0 of type string
+          name: string
+          namespace: string
+        token:
+          bearerToken:
+            key: string
+            name: string
+            namespace: string
+      authRef:
+        key: string
+        name: string
+        namespace: string
+      resource:
+        group: string
+        kind: string
+        version: string
+      server:
+        caBundle: c3RyaW5n
+        caProvider:
+          key: string
+          name: string
+          namespace: string
+          type: "Secret" # "Secret", "ConfigMap"
+        url: "kubernetes.default"
+      whitelist:
+        rules:
+        - name: string
+          namespace: string
+          properties: [] # minItems 0 of type string
     delinea:
       clientId:
         secretRef: