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; remote-cluster access is covered in the 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.
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).group/version/kind) via Kubernetes discovery.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:
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.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$"
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).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 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:
name only: request is allowed when object name matches.namespace only: request is allowed when object namespace matches (ClusterSecretStore only).properties only: request is allowed when requested property keys match.name, namespace, and properties: all configured fields in the same rule must match.A rule with none of name, namespace, or properties is invalid.
properties matchingdata[].remoteRef.property, the requested key is that property path (for example spec.password).GetAllSecrets), only rules that do not require properties can match.namespace matching is only enforced for ClusterSecretStore; SecretStore ignores this field.remoteRef.property is evaluated using GJSON path syntax, the same syntax used by the Kubernetes provider and the rest of ESO, so the property dialect is consistent across providers.
Examples:
spec.userspec.passwordstatus.0.keystatus.#(key=="rotationDate").valIf the expression resolves to no value, the provider returns property not found.
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.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".
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
Use a namespace-qualified key (namespace/objectName) for namespaced resources when referencing a ClusterSecretStore.
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
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.
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.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
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
When using ClusterSecretStore:
remoteRef.key must be namespace/objectName.whitelist.rules[].namespace can be used to constrain which namespaces are readable.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.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.