External Secrets Operator
A Kubernetes operator that copies secrets from an external manager (OpenBao, HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Azure Key Vault, 1Password and many more) into native Kubernetes Secrets. A SecretStore describes how to reach and log in to the provider, an ExternalSecret names the keys to fetch, and the controller re-reads them on an interval so the Secret follows the source. Apache 2.0 and free. It fits when the source of truth for secrets lives outside the cluster and Git should hold only references.
External Secrets Operator (ESO) is a Kubernetes controller that reads secrets from an external secret manager and writes them into ordinary Kubernetes Secrets. The values stay in the manager, and the manifests in Git carry only a reference such as "key airflow/core, all properties". Workloads keep reading a normal Secret and need no client library for the manager.
What it is
ESO is an Apache 2.0 project under the CNCF, written in Go and installed with a Helm chart. It grew from the merge of several earlier "external secrets" controllers, and it now ships providers for a long list of backends: OpenBao and HashiCorp Vault, AWS Secrets Manager and Parameter Store, Google Secret Manager, Azure Key Vault, IBM Cloud, CyberArk Conjur, 1Password, Bitwarden, Infisical, Doppler, Akeyless, GitLab and GitHub variables, a generic webhook, and others.
The current line is v2.13 (v2.13.0 and chart 2.13.0 shipped 2026-10-16). Minor releases arrive every two to three weeks, and the stability and support page states that only the newest minor is supported: each release ends the life of the one before it. Recent 2.x releases are tested against Kubernetes 1.36. Milestones worth knowing:
| Release | Date | Change |
|---|---|---|
| v0.16.0 | 2025-04 | external-secrets.io/v1 API added; v1alpha1 and the conversion webhooks removed |
| v0.17.0 | 2025-05 | v1beta1 no longer served; manifests must use v1 |
| v0.19.2 → v0.20.0 | 2025-08 → 2025-09 | Releases paused for lack of maintainers, then resumed |
| v1.0.0 | 2025-11 | First 1.x release |
| v2.0.0 | 2026-02 | Unmaintained Alibaba and Device42 providers removed |
| v2.7 / v2.10 | 2026-06 / 2026-08 | Dedicated openbao provider added in v2.7; Kubernetes auth for it in v2.10 |
How it works
The Helm chart installs three Deployments: the controller that reconciles the custom resources, a webhook that validates them, and a cert-controller that issues and rotates the webhook's TLS certificate. Everything else is configuration in custom resources:
- A SecretStore (namespaced) or ClusterSecretStore (cluster-wide) holds the provider address and the login method.
- An ExternalSecret points at a store, lists the remote keys, and names the target Secret.
- The controller logs in to the provider, fetches the keys, renders an optional template, and creates or updates the target Secret. It repeats this every
refreshInterval.
The figure follows one sync from manifest to pod, and then a value change made at the source.
Step 1 of 5. Argo CD or kubectl applies the ExternalSecret. The controller sees the new object and starts a reconcile.
Shown with OpenBao behind a ClusterSecretStore that uses Kubernetes auth, and Reloader rolling the workload.
The controller never pushes into the pod. Its job ends at the Kubernetes Secret, and the usual Kubernetes rules apply from there: a mounted Secret volume is refreshed by the kubelet after a delay, while environment variables are fixed when the container starts. That is why a restart tool such as Reloader usually sits next to ESO.
ESO also runs the reverse direction. A PushSecret copies a Kubernetes Secret out to a provider, and generators (passwords, ECR and GCR tokens, UUIDs and others) create values that an ExternalSecret can use as a source.
When to use it
Reach for it when:
- The source of truth for secrets is a manager outside the cluster, and the cluster should hold copies only.
- Git holds every manifest (GitOps), and secret values must not be in it, encrypted or not.
- Several clusters need the same secret from one place, each with its own scoped login.
- Rotation happens at the source (a rotation job, a cloud console, a database engine), and the cluster should follow without a manual
kubectlstep. - Workloads expect plain Kubernetes Secrets and should not take a dependency on a vault SDK.
Skip it when:
- There is one cluster, a handful of secrets and no external manager. Sealed Secrets or SOPS keep the encrypted value in Git with less moving infrastructure.
- Policy forbids the value ever landing in etcd. The Secrets Store CSI Driver can mount values straight into the pod as files.
- The manager offers short-lived, per-pod credentials (database leases, cloud workload identity). An agent sidecar or workload identity hands those to the pod directly, which a copied Secret cannot match.
At a glance
Core concepts
- SecretStore / ClusterSecretStore — how to reach and authenticate to a provider. Namespaced stores keep credentials inside one namespace; cluster stores serve all of them.
- ExternalSecret — the mapping from remote keys to one target Secret.
datapicks single keys;dataFrom.extractpulls every property of a key;dataFrom.findmatches keys by name or tag. - ClusterExternalSecret — stamps an ExternalSecret into every namespace that matches a selector.
- refreshPolicy —
Periodic(default: re-read onrefreshInterval),OnChange(only when the ExternalSecret changes),CreatedOnce(sync once). - creationPolicy —
Owner(default; the Secret is deleted with the ExternalSecret),Orphan,Merge(into an existing Secret),None. - deletionPolicy — what happens when the keys disappear at the provider:
Retain(default),DeleteorMerge. - Templates — Go templates in
target.templatereshape the data, for example into a connection string or a config file.
Key facts
| Item | Value |
|---|---|
| Licence | Apache 2.0 |
| Current line | v2.13 (2026-10-16) |
| Support window | Newest minor only |
| API group / version | external-secrets.io/v1 (v1beta1 unserved since v0.17) |
| Components | controller, webhook, cert-controller |
| Install | Helm chart external-secrets from charts.external-secrets.io |
| CRD apply | Server-side apply; the CRDs exceed the 256 KB client-side limit |
ESO vs the common alternatives
| Aspect | External Secrets Operator | Secrets Store CSI Driver | Sealed Secrets |
|---|---|---|---|
| Where the value lives | External manager, copied into a Secret | External manager, mounted as files | Encrypted in Git, decrypted into a Secret |
| Kubernetes Secret created | Yes | Optional (sync feature) | Yes |
| Rotation | Re-read on interval | Optional rotation poll | Re-encrypt and commit |
| Needs an external manager | Yes | Yes | No |
| Works with env vars | Yes | Only through the synced Secret | Yes |
How to integrate
Install the operator with Helm. The chart manages the CRDs by default (installCRDs: true):
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace --version 2.13.0
A ClusterSecretStore for OpenBao or Vault with Kubernetes auth. The vault provider talks to OpenBao because the API is compatible; the dedicated openbao provider takes the same shape under spec.provider.openbao and requires a serviceAccountRef or secretRef for Kubernetes auth.
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: openbao
spec:
provider:
vault:
server: https://openbao.example.com:8200
path: kv # KV mount
version: v2
auth:
kubernetes:
mountPath: kubernetes # auth mount for this cluster
role: eso
serviceAccountRef:
name: external-secrets
namespace: external-secrets
The matching OpenBao side: a policy that can read only this cluster's subtree, and a Kubernetes auth role bound to the controller's service account.
bao policy write eso-prod - <<'EOF'
path "kv/data/prod/*" { capabilities = ["read"] }
EOF
bao write auth/kubernetes/role/eso \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
policies=eso-prod ttl=1h
An ExternalSecret that pulls every property of one key into one Secret:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: api-env
namespace: api
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: api-env
creationPolicy: Owner
dataFrom:
- extract:
key: prod/api/api-env
Check the result with kubectl -n api get externalsecret api-env: the STATUS column reads SecretSynced and READY is True. A failed login or a missing key shows in kubectl describe events. To force a re-read before the interval, change an annotation on the ExternalSecret (for example force-sync: "<timestamp>").
In the GL stack
External Secrets Operator v2.13.0 runs on the beta and prod clusters, where the product workloads run. The secret manager is OpenBao on the management cluster.
- Source of truth. Every app Secret starts in OpenBao's KV v2 engine. The manifests in Git hold ExternalSecret objects with references only, and Argo CD applies them like any other resource. A value changes in OpenBao, never with
kubectl, because ESO overwrites the cluster copy on the next hourly refresh. - Per-cluster auth. OpenBao has one Kubernetes auth mount per client cluster. Each cluster's ClusterSecretStore logs in through its own mount, and the policy behind it allows reads of that cluster's own subtree only. A login from beta therefore cannot read prod's secrets.
- Rotation reaches the pods. Stakater Reloader runs on the same two clusters. When ESO updates a Secret, Reloader rolls the Deployments that reference it through their annotation, so a rotated value takes effect without a manual restart.
- Around it. OpenBao's Raft storage is snapshotted nightly to the backup store, and a secret-age exporter warns about secrets that have not changed for too long.
Gotchas
- Hand edits do not stick. With the default
Periodicpolicy the controller rewrites the Secret on every refresh. Edit the value at the provider. - No restart on change. ESO updates the Secret only. Environment variables stay stale until the pod restarts; pair it with Reloader or roll the Deployment.
v1beta1manifests fail. Since v0.17 onlyexternal-secrets.io/v1is served. Old Helm charts and blog examples still showv1beta1.- Server-side apply for CRDs. The CRD bundle passes the 256 KB annotation limit of client-side apply. Use
kubectl apply --server-side, or theServerSideApply=truesync option in Argo CD. - Owner means delete. With
creationPolicy: Owner, deleting or pruning the ExternalSecret deletes the Secret, and pods that need it fail on their next start. UseOrphanfor Secrets that must outlive the mapping. CreatedOnceis tracked in status. The docs note that recreating the ExternalSecret syncs again and overwrites the Secret. Addtarget.immutable: trueif the value must never change.- Deleted keys linger. The default
deletionPolicy: Retainkeeps the last copy when the key is removed at the provider. SetDeletewhere stale credentials are a risk. - Upgrade on a short clock. Only the newest minor is supported, and a new minor ships every few weeks. Read the release notes for removed providers and chart changes before each bump.
- Leader election is off by default. The chart sets
leaderElect: false. Turn it on before raisingreplicaCountabove 1; since v2.12 the chart warns about this combination.
Risks
- Project health. ESO paused all releases in August 2025 because too few maintainers were active, and resumed with v0.20 in September 2025. Releases have been steady since, but the project depends on a small group of volunteers and sponsor companies.
- Privilege concentration. By default the controller can read and write Secrets in every namespace, and its provider login can read every path its policy allows. Scope the provider policy tightly, use
scopedNamespace/scopedRBACin the chart where one namespace is enough, and prefer namespaced SecretStores for multi-tenant clusters. - Values still land in etcd. ESO moves secrets out of Git, not out of the cluster. Turn on Secret encryption at rest and limit
get secretsRBAC. - Low lock-in. Workloads see plain Secrets, so removing ESO means replacing ExternalSecrets with another way of creating the same Secrets. The provider choice is the bigger commitment.
Alternatives
- Secrets Store CSI Driver — pick it when values should reach the pod as files without a Kubernetes Secret, or with a short-lived mount per pod.
- Sealed Secrets — pick it for a single cluster with no external manager: the encrypted value lives in Git and a controller decrypts it.
- SOPS (with Flux or an Argo CD plugin) — pick it when encrypted files in Git are the source of truth and a KMS or age key does the decryption.
- OpenBao or Vault Agent injector — pick it when pods need dynamic, leased credentials straight from the manager, with no copy in etcd.
Related
- OpenBao — the secret manager behind the ClusterSecretStore.
- Reloader — rolls workloads when a synced Secret changes.
- Argo CD — applies the ExternalSecret manifests from Git.
- K3s, Rancher and Kyverno — the clusters it runs on and the policy layer next to it.
- Official: ESO documentation, external-secrets/external-secrets releases, Stability and support, HashiCorp Vault provider.
