Back to blog

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.

June 12, 2026by EnvManager Team
kubernetesexternal-secrets-operatorsecrets-managementdevopssecurity

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 v1 API. As of June 2026 the latest release is v2.6.0 (Helm chart 2.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 show v1beta1 — if you copy YAML from a 2023-era blog post, update the apiVersion.

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:

  1. You apply an ExternalSecret to the cluster.
  2. The ESO controller sees it, resolves the referenced SecretStore/ClusterSecretStore, and authenticates to the provider.
  3. It fetches the declared keys from the external API.
  4. It creates (or updates) the target Kubernetes Secret with those values.
  5. On every refreshInterval tick, 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 15s interval across 500 ExternalSecrets adds up fast. For most secrets, 1h is plenty; reserve aggressive intervals for genuinely hot-rotating credentials.
  • Refresh ≠ reload. ESO updates the Secret object. Pods consuming it via envFrom or env do 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 OperatorSealed SecretsSecrets Store CSI Driver
ModelSyncs from external store → K8s SecretEncrypts secrets so they can live in GitMounts provider secrets directly as volumes
Source of truthExternal provider (AWS, Vault, GCP…)Your Git repositoryExternal provider
Creates K8s Secrets?YesYes (decrypted in-cluster)Optional (via sync feature)
Secret rotationAutomatic via refreshIntervalManual: re-seal and commitOn remount/rotation reconciler
Works with env varsYes (native Secrets)YesOnly if K8s Secret sync is enabled
External dependenciesSecret provider requiredNone — fully self-containedProvider + CSI driver + per-node DaemonSet
GitOps fitExcellent (manifests contain no secret data)Good (encrypted data is in Git)Good, more moving parts
Best forTeams with an existing secret storeSmall teams wanting Git-only workflow, no external storeCompliance 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

  1. Prefer workload identity over static credentials. IRSA/Pod Identity on EKS, Workload Identity on GKE, Managed Identity on AKS, Kubernetes auth for Vault. A SecretStore holding a static cloud key is just moving the bootstrap problem around.
  2. Use ClusterSecretStore with namespace conditions for multi-tenancy. Platform team owns stores and credentials; app teams own ExternalSecrets. Lock down who can create or edit store resources with Kubernetes RBAC — anyone who can modify a SecretStore can potentially redirect secret fetches.
  3. 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/*), not secretsmanager:GetSecretValue on *.
  4. Set sane refresh intervals. Default to 1h, tighten only where rotation demands it, and watch your provider's API billing.
  5. Monitor sync health. ESO exposes Prometheus metrics and sets status conditions on every ExternalSecret. Alert on SecretSyncedError — a silently stale secret is worse than a loud failure.
  6. Plan for pod reloads on rotation. As covered above, syncing a new value into a Secret doesn't restart consumers. Make reload behavior explicit.
  7. Keep the operator updated. The project moves quickly (the API graduated from v1beta1 to v1, 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.

Ready to manage your environment variables securely?

EnvManager helps teams share secrets safely, sync configurations across platforms, and maintain audit trails.

Start your free trial

Get DevOps tips in your inbox

Weekly security tips, environment management best practices, and product updates.

No spam. Unsubscribe anytime.