External Secrets Operator: The Complete Guide (2026)
Learn how External Secrets Operator syncs secrets from AWS, Vault, and GCP into Kubernetes. 2026 guide with Helm install, YAML examples, and comparisons.
External Secrets Operator: The Complete Guide (2026)
Kubernetes has a secrets problem, and it's not the one most people think of first. Yes, native Secret objects are only base64-encoded (not encrypted) by default — but the deeper problem is workflow. Your secrets already live somewhere: AWS Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault. Your cluster needs them as Kubernetes Secrets so pods can mount them or read them as environment variables. The gap between those two worlds is where teams end up with copy-pasted secrets in Git, hand-edited manifests, brittle CI scripts running kubectl create secret, and no idea which value is current.
External Secrets Operator (ESO) closes that gap. It's a Kubernetes operator that reads secrets from external secret management systems and automatically injects the values into native Kubernetes Secrets — continuously, declaratively, and without ever committing a secret to Git.
This guide covers how ESO actually works (the CRD model trips people up), installation, working YAML for AWS and Vault, refresh and templating behavior, an honest comparison with Sealed Secrets and the Secrets Store CSI Driver, and production best practices. If you're newer to this space, our primer on what secrets management is is a good companion read.
What Is External Secrets Operator?
External Secrets Operator is an open-source Kubernetes operator, hosted by the CNCF, that integrates external secret stores with Kubernetes. In the project's own words, it "reads information from external APIs and automatically injects the values into a Kubernetes Secret."
The key mental model: ESO is a synchronization engine, not a secret store. It doesn't hold your secrets — your existing provider does. ESO watches custom resources that declare which external secrets you want, where they live, and how often to refresh them. It then creates and maintains ordinary Kubernetes Secret objects that your workloads consume exactly as they always have. No application changes required.
A few project facts worth knowing in 2026:
- ESO supports 45+ providers, including AWS Secrets Manager and Parameter Store, Google Cloud Secret Manager, Azure Key Vault, HashiCorp Vault, IBM Cloud Secrets Manager, 1Password, Doppler, Infisical, Pulumi ESC, CyberArk, and more.
- It was accepted into the CNCF Sandbox in July 2022. The project went through a rough patch in spring 2025 when maintainers announced a release pause due to maintainer shortage — the community and new corporate backing rallied, releases resumed, and the project subsequently shipped its stable
v1API. As of June 2026 the latest release is v2.6.0 (Helm chart2.6.0); check the releases page for current versions before installing. - The stable CRD API group is
external-secrets.io/v1. Many older tutorials still showv1beta1— if you copy YAML from a 2023-era blog post, update theapiVersion.
How ESO Works: CRDs, Providers, and the Sync Loop
ESO's design separates authentication from consumption using two families of custom resources.
SecretStore and ClusterSecretStore
A SecretStore answers: "Which external system, and how do I authenticate to it?" It's namespaced — it lives in one namespace, and only ExternalSecret resources in that namespace can use it. The spec contains a provider block (one per store) with provider-specific configuration: region and auth for AWS, server URL and auth method for Vault, and so on.
ClusterSecretStore is the cluster-scoped variant: define the provider connection once, reference it from any namespace. This is the backbone of multi-tenant setups — a platform team manages the ClusterSecretStore (and the credentials inside it), while application teams in their own namespaces only declare which secrets they need. You can restrict which namespaces may use a ClusterSecretStore via its conditions field (matching namespace names or label selectors), which keeps tenant boundaries enforceable.
ExternalSecret
An ExternalSecret answers: "Which secrets do I want, and what Kubernetes Secret should they become?" It references a store via secretStoreRef, lists remote keys to fetch under data (or pulls entire secrets with dataFrom), and defines the target — the name and shape of the Kubernetes Secret ESO will create and own.
The reconciliation loop
Picture the flow as a continuous cycle:
- You apply an
ExternalSecretto the cluster. - The ESO controller sees it, resolves the referenced
SecretStore/ClusterSecretStore, and authenticates to the provider. - It fetches the declared keys from the external API.
- It creates (or updates) the target Kubernetes
Secretwith those values. - On every
refreshIntervaltick, it re-fetches and reconciles. Rotate the secret in AWS or Vault, and the in-cluster Secret converges to the new value automatically.
Two more CRDs round out the model: ClusterExternalSecret pushes the same ExternalSecret spec into multiple namespaces at once, and PushSecret works in reverse — syncing a Kubernetes Secret out to an external provider, useful for migrations or for secrets generated in-cluster.
Installing ESO with Helm
Helm is the standard installation method. As of June 2026:
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace
The chart installs the CRDs by default. If you manage CRDs separately (common in GitOps setups where Argo CD or Flux applies CRDs in a dedicated wave), disable that with --set installCRDs=false.
Verify the deployment:
kubectl get pods -n external-secrets
kubectl get crds | grep external-secrets.io
You should see the controller, webhook, and cert-controller pods running. One operational note from the official docs: the CRDs are large (they hit Kubernetes' 256KB annotation limit), so if you apply them manually rather than via Helm, use server-side apply (kubectl apply --server-side).
To uninstall, first delete every SecretStore, ClusterSecretStore, and ExternalSecret resource you created, then remove the release:
kubectl get SecretStores,ClusterSecretStores,ExternalSecrets --all-namespaces
helm delete external-secrets --namespace external-secrets
Example 1: Syncing from AWS Secrets Manager
Let's wire up a real sync. Assume a secret named prod/myapp/database exists in AWS Secrets Manager containing JSON: {"username": "app", "password": "s3cr3t"}.
The SecretStore
For demos, static credentials stored in a Kubernetes Secret work; for production on EKS, use IRSA (IAM Roles for Service Accounts) or EKS Pod Identity so no long-lived AWS keys exist in the cluster at all. Here's the static-credentials version, matching the official docs:
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-secretsmanager
namespace: myapp
spec:
provider:
aws:
service: SecretsManager
region: eu-central-1
auth:
secretRef:
accessKeyIDSecretRef:
name: awssm-secret
key: access-key
secretAccessKeySecretRef:
name: awssm-secret
key: secret-access-key
With IRSA, you drop the auth.secretRef block entirely (or point auth.jwt at a service account) and annotate the controller's service account with the IAM role ARN. The service field also accepts ParameterStore if you use SSM Parameter Store instead.
The ExternalSecret
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-database
namespace: myapp
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: SecretStore
target:
name: myapp-database
creationPolicy: Owner
data:
- secretKey: DB_USERNAME
remoteRef:
key: prod/myapp/database
property: username
- secretKey: DB_PASSWORD
remoteRef:
key: prod/myapp/database
property: password
Apply it, and within seconds a Kubernetes Secret named myapp-database exists with DB_USERNAME and DB_PASSWORD keys. The property field uses gjson path syntax, so you can reach nested JSON values like database.credentials.password. Check sync status with:
kubectl get externalsecret myapp-database -n myapp
# READY should be True, STATUS SecretSynced
If you want every key from the remote JSON secret without listing them individually, use dataFrom:
spec:
dataFrom:
- extract:
key: prod/myapp/database
Example 2: Syncing from HashiCorp Vault
Vault is the other provider you'll see everywhere (if you're evaluating it, our breakdown of HashiCorp Vault pricing covers what running it actually costs). ESO talks to Vault's KV engine — v2 is the default and what you almost certainly run.
The production-grade auth method is Vault's Kubernetes auth: the ESO-referenced service account proves its identity to Vault via the TokenReview API, and Vault returns a scoped token. No static Vault token stored in the cluster:
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-backend
namespace: myapp
spec:
provider:
vault:
server: "https://vault.example.org:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "myapp"
serviceAccountRef:
name: "myapp-eso"
(For local experiments, a tokenSecretRef pointing at a Kubernetes Secret holding a Vault token also works — just don't ship that to production.) Note that Kubernetes auth requires the system:auth-delegator ClusterRole binding so Vault can validate tokens.
The ExternalSecret looks nearly identical to the AWS one — that's the point. The consumption side is provider-agnostic:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-vault-secret
namespace: myapp
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-vault-secret
data:
- secretKey: API_KEY
remoteRef:
key: myapp/config # path under the "secret" mount
property: api_key
Swap the store, keep the workload manifests. This is why ESO is so popular with platform teams managing heterogeneous secret backends.
Refresh Intervals and Templating
refreshInterval
spec.refreshInterval controls how often ESO re-reads the provider and reconciles the target Secret. Two practical considerations:
- Cost and rate limits. Every refresh is an API call. AWS Secrets Manager bills per 10,000 API calls and Vault has its own throughput limits — a
15sinterval across 500 ExternalSecrets adds up fast. For most secrets,1his plenty; reserve aggressive intervals for genuinely hot-rotating credentials. - Refresh ≠ reload. ESO updates the Secret object. Pods consuming it via
envFromorenvdo not see new values until they restart — environment variables are read once at container start. Volume-mounted secrets eventually update on disk, but your app must re-read them. Pair ESO with a reload mechanism (Stakater Reloader, Argo CD sync hooks, or a checksum annotation on the pod template) if rotation needs to propagate to running workloads.
Templating
Sometimes the raw remote value isn't the shape your app needs — you need a rendered config file, a TLS secret, or a connection string assembled from parts. spec.target.template runs fetched values through Go templates (with 200+ Sprig functions) before writing the Secret:
spec:
target:
name: myapp-config
template:
engineVersion: v2
data:
DATABASE_URL: "postgres://{{ .username }}:{{ .password }}@db.internal:5432/myapp"
config.yaml: |
api:
key: {{ .api_key }}
data:
- secretKey: username
remoteRef: { key: prod/myapp/database, property: username }
- secretKey: password
remoteRef: { key: prod/myapp/database, property: password }
- secretKey: api_key
remoteRef: { key: prod/myapp/api, property: key }
You can also set template.type (e.g. kubernetes.io/tls or kubernetes.io/dockerconfigjson) to produce specially-typed Secrets, and mergePolicy: Merge to combine templated keys with raw data/dataFrom output instead of replacing it. Referencing a key that doesn't exist fails loudly rather than rendering an empty string — a deliberate and welcome design choice.
ESO vs Sealed Secrets vs Secrets Store CSI Driver
These three get lumped together, but they solve different problems. Honest summary:
| External Secrets Operator | Sealed Secrets | Secrets Store CSI Driver | |
|---|---|---|---|
| Model | Syncs from external store → K8s Secret | Encrypts secrets so they can live in Git | Mounts provider secrets directly as volumes |
| Source of truth | External provider (AWS, Vault, GCP…) | Your Git repository | External provider |
| Creates K8s Secrets? | Yes | Yes (decrypted in-cluster) | Optional (via sync feature) |
| Secret rotation | Automatic via refreshInterval | Manual: re-seal and commit | On remount/rotation reconciler |
| Works with env vars | Yes (native Secrets) | Yes | Only if K8s Secret sync is enabled |
| External dependencies | Secret provider required | None — fully self-contained | Provider + CSI driver + per-node DaemonSet |
| GitOps fit | Excellent (manifests contain no secret data) | Good (encrypted data is in Git) | Good, more moving parts |
| Best for | Teams with an existing secret store | Small teams wanting Git-only workflow, no external store | Compliance regimes that forbid secrets in etcd |
The decision logic in practice:
- Already have Vault / AWS SM / a cloud secret manager? ESO. It's the least friction and keeps one source of truth.
- No external store, small team, everything in Git? Sealed Secrets is genuinely fine — but rotation is manual, and if you lose the sealing key controller, re-encrypting everything is painful.
- Hard requirement that secret material never lands in etcd as a Secret object? The CSI driver mounts values straight into pod filesystems. The trade-off: no env-var support without enabling its Secret-sync feature (which reintroduces etcd storage, negating the benefit), plus heavier per-node infrastructure.
Many large clusters run ESO and the CSI driver side by side: ESO for the 95% of secrets consumed as env vars, CSI mounts for the handful with strict etcd-avoidance requirements.
Best Practices for Running ESO in Production
- Prefer workload identity over static credentials. IRSA/Pod Identity on EKS, Workload Identity on GKE, Managed Identity on AKS, Kubernetes auth for Vault. A
SecretStoreholding a static cloud key is just moving the bootstrap problem around. - Use
ClusterSecretStorewith namespace conditions for multi-tenancy. Platform team owns stores and credentials; app teams ownExternalSecrets. Lock down who can create or edit store resources with Kubernetes RBAC — anyone who can modify aSecretStorecan potentially redirect secret fetches. - Scope provider permissions tightly. The IAM role or Vault policy behind a store should read only the path prefix that store legitimately needs (e.g.
prod/team-a/*), notsecretsmanager:GetSecretValueon*. - Set sane refresh intervals. Default to
1h, tighten only where rotation demands it, and watch your provider's API billing. - Monitor sync health. ESO exposes Prometheus metrics and sets status conditions on every
ExternalSecret. Alert onSecretSyncedError— a silently stale secret is worse than a loud failure. - Plan for pod reloads on rotation. As covered above, syncing a new value into a Secret doesn't restart consumers. Make reload behavior explicit.
- Keep the operator updated. The project moves quickly (the API graduated from
v1beta1tov1, and major versions have shipped since). Pin your Helm chart version, read release notes, and upgrade deliberately.
For broader guidance beyond Kubernetes, see our secrets management best practices guide.
Where EnvManager Fits (and Where It Doesn't)
Full transparency: we build EnvManager, and it is not a replacement for External Secrets Operator. ESO solves in-cluster injection — getting values from a secret store into Kubernetes Secrets. EnvManager solves the layer before that: being the source of truth for environment variables and secrets across your environments, with client-side AES-256-GCM encryption, RBAC, audit logs, .env import/export, and a CLI with real-time sync.
Where the two worlds meet:
- If your platform is Kubernetes-first and your secrets already live in AWS Secrets Manager, GCP Secret Manager, or Vault — use ESO. That's the right tool. EnvManager's integrations can push secrets to AWS Secrets Manager and GCP Secret Manager, so some teams use EnvManager as the human-friendly management layer (RBAC, audit trail, environment separation) and let ESO pull from the cloud store into the cluster.
- If your deployment targets are Vercel, Railway, Render, Dokploy, Coolify, or GitHub Actions rather than raw Kubernetes, ESO doesn't apply — and running Vault plus an operator for a handful of services is heavy. EnvManager syncs directly to those platforms and costs a flat $9/month for your whole team (unlimited members, no per-seat pricing), with a 14-day free trial and no credit card required.
Different problems, honestly different tools. Teams comparing the whole landscape may find our roundup of the best secrets management tools useful.
FAQ
What does External Secrets Operator do?
External Secrets Operator is a Kubernetes operator that reads secrets from external systems — AWS Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault, and 45+ other providers — and automatically creates and updates native Kubernetes Secrets with those values. It keeps secrets out of Git while letting workloads consume them as ordinary Secrets.
Is External Secrets Operator a CNCF project?
Yes. ESO was accepted into the CNCF Sandbox in July 2022 and remains a CNCF-hosted project. After a maintainer shortage briefly paused releases in 2025, development resumed with renewed backing; as of June 2026 the project ships regular releases (v2.6.0 at time of writing). Check the CNCF project page for current maturity status.
What's the difference between SecretStore and ClusterSecretStore?
A SecretStore is namespaced: it defines provider credentials usable only by ExternalSecret resources in the same namespace. A ClusterSecretStore is cluster-scoped: defined once, referenced from any namespace (optionally restricted via conditions). Use ClusterSecretStore when a platform team manages provider access for many application teams.
Does ESO automatically rotate secrets?
ESO doesn't rotate secrets itself — rotation happens in your provider (e.g. AWS Secrets Manager rotation Lambdas or Vault dynamic secrets). What ESO does is propagate rotation: on each refreshInterval, it re-fetches values and updates the Kubernetes Secret. Remember that pods consuming secrets as environment variables must restart to pick up new values.
External Secrets Operator vs Sealed Secrets — which should I use?
Use ESO if you have (or want) an external secret store as your source of truth — you get automatic sync, rotation propagation, and no secret material in Git at all. Use Sealed Secrets if you want a Git-only workflow with no external dependencies: secrets are encrypted into the repo and decrypted in-cluster, but rotation is manual. They solve different problems and aren't direct substitutes.
Managing env vars and secrets across more than just Kubernetes? EnvManager gives your team one encrypted source of truth with RBAC, audit logs, and one-command sync to GitHub Actions, Vercel, Railway, AWS Secrets Manager, and more. Start your 14-day free trial — no credit card required, flat pricing for the whole team.