Security
13 min readMay 7, 2026

External Secrets Operator: Syncing AWS, GCP, and Vault Secrets to Kubernetes

External Secrets Operator (ESO) syncs secrets from external stores (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault) into Kubernetes Secrets. It eliminates the practice of committing secret values to Git while keeping them available to pods as standard Kubernetes Secrets. This covers ESO installation, SecretStore and ClusterSecretStore configuration for AWS and Vault, ExternalSecret resource definitions, refreshInterval-based rotation, generators for dynamic secrets, and the PushSecret pattern for writing from Kubernetes back to external stores.

CO
Coding Protocols Team
Platform Engineering
External Secrets Operator: Syncing AWS, GCP, and Vault Secrets to Kubernetes

Kubernetes Secrets are not secret. They're base64-encoded data stored in etcd, accessible to anyone with the right RBAC permissions, and frequently end up committed to Git in Helm values or Kubernetes manifests. External Secrets Operator (ESO) solves the source-of-truth problem: secrets live in a dedicated secrets store (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager), and ESO syncs them to Kubernetes Secrets on a configurable interval. Applications consume the Kubernetes Secret as before — no code changes required.

The sync model means you never store secret values in Git. You store only the reference: which path in Secrets Manager maps to which Kubernetes Secret. The actual values stay in the external store.


Installation

bash
1helm repo add external-secrets https://charts.external-secrets.io
2helm repo update
3
4# Check https://github.com/external-secrets/external-secrets/releases for latest
5helm install external-secrets \
6  external-secrets/external-secrets \
7  --namespace external-secrets \
8  --create-namespace \
9  --version 0.10.7 \
10  --set crds.install=true    # installCRDs=true was deprecated in v0.9; crds.install=true is correct for v0.9+

Verify:

bash
kubectl get pods -n external-secrets
# external-secrets-<hash>              Running
# external-secrets-cert-controller-<hash>  Running
# external-secrets-webhook-<hash>      Running

AWS Secrets Manager: SecretStore Setup

SecretStore is namespace-scoped: it can only be used by ExternalSecret resources in the same namespace. Configure one per team namespace using IRSA or Pod Identity for per-namespace IAM isolation.

yaml
1apiVersion: external-secrets.io/v1beta1
2kind: SecretStore
3metadata:
4  name: aws-secretsmanager
5  namespace: payments
6spec:
7  provider:
8    aws:
9      service: SecretsManager
10      region: us-east-1
11      auth:
12        jwt:
13          serviceAccountRef:
14            name: payments-eso-sa    # ServiceAccount with Pod Identity or IRSA annotation

Create the ServiceAccount with Pod Identity:

bash
aws eks create-pod-identity-association \
  --cluster-name production \
  --namespace payments \
  --service-account payments-eso-sa \
  --role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/ESOPaymentsReader

The IAM role needs secretsmanager:GetSecretValue and secretsmanager:DescribeSecret on the target secrets:

json
1{
2  "Effect": "Allow",
3  "Action": [
4    "secretsmanager:GetSecretValue",
5    "secretsmanager:DescribeSecret"
6  ],
7  "Resource": "arn:aws:secretsmanager:us-east-1:*:secret:payments/*"
8}

ClusterSecretStore: Cluster-Wide Provider

ClusterSecretStore is cluster-scoped — any ExternalSecret in any namespace can reference it. Use it for centralized secrets (shared infrastructure credentials, TLS CA) while using namespace-scoped SecretStore for team-specific secrets.

yaml
1apiVersion: external-secrets.io/v1beta1
2kind: ClusterSecretStore
3metadata:
4  name: aws-global
5spec:
6  provider:
7    aws:
8      service: SecretsManager
9      region: us-east-1
10      auth:
11        jwt:
12          serviceAccountRef:
13            name: eso-cluster-sa
14            namespace: external-secrets    # ClusterSecretStore SA must specify namespace

ExternalSecret: Syncing a Secret

yaml
1apiVersion: external-secrets.io/v1beta1
2kind: ExternalSecret
3metadata:
4  name: payments-db-credentials
5  namespace: payments
6spec:
7  refreshInterval: 1h    # Re-sync from AWS every hour; rotation window
8
9  secretStoreRef:
10    name: aws-secretsmanager
11    kind: SecretStore    # or ClusterSecretStore
12
13  target:
14    name: payments-db-secret    # The Kubernetes Secret to create/update
15    creationPolicy: Owner       # ESO owns this Secret; deletes it when ExternalSecret is deleted
16    template:
17      type: Opaque
18      data:
19        # Transform: construct connection string from individual secret fields
20        DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@{{ .host }}:{{ .port }}/payments"
21
22  data:
23    # Map AWS Secrets Manager fields to Kubernetes Secret keys
24    - secretKey: username    # Key in the Kubernetes Secret
25      remoteRef:
26        key: payments/database    # Path in AWS Secrets Manager
27        property: username        # JSON field within the secret (if the secret is JSON)
28
29    - secretKey: password
30      remoteRef:
31        key: payments/database
32        property: password
33
34    - secretKey: host
35      remoteRef:
36        key: payments/database
37        property: host

Check sync status:

bash
kubectl get externalsecret payments-db-credentials -n payments
# NAME                     STORE                   REFRESH INTERVAL   STATUS   READY
# payments-db-credentials  aws-secretsmanager       1h                 Valid    True

kubectl describe externalsecret payments-db-credentials -n payments
# Events: Synchronized successfully

Bulk Sync with dataFrom

If your AWS secret is a JSON object and you want all fields as Kubernetes Secret keys without listing them individually:

yaml
spec:
  dataFrom:
    - extract:
        key: payments/database    # All JSON fields from this secret become Secret keys

This creates a Kubernetes Secret with keys username, password, host, port — one key per top-level JSON field in the AWS secret.


HashiCorp Vault SecretStore

yaml
1apiVersion: external-secrets.io/v1beta1
2kind: ClusterSecretStore
3metadata:
4  name: vault-backend
5spec:
6  provider:
7    vault:
8      server: https://vault.vault.svc.cluster.local:8200
9      path: secret    # KV v2 secrets mount path
10      version: v2     # KV v2 API
11      caBundle: <base64-vault-ca>    # Vault's TLS CA
12
13      auth:
14        kubernetes:
15          mountPath: kubernetes
16          role: eso-reader    # Vault Kubernetes auth role
17          serviceAccountRef:
18            name: eso-cluster-sa
19            namespace: external-secrets
yaml
1# ExternalSecret referencing Vault KV v2
2apiVersion: external-secrets.io/v1beta1
3kind: ExternalSecret
4metadata:
5  name: payments-stripe-keys
6  namespace: payments
7spec:
8  refreshInterval: 30m
9  secretStoreRef:
10    name: vault-backend
11    kind: ClusterSecretStore
12  target:
13    name: stripe-credentials
14    creationPolicy: Owner
15  data:
16    - secretKey: STRIPE_SECRET_KEY
17      remoteRef:
18        key: payments/stripe    # Path relative to the KV v2 mount (ESO prepends /data/ automatically; don't include it)
19        property: secret_key

Secret Rotation: refreshInterval and Reloading

ESO re-fetches the secret from the external store on every refreshInterval. If the value changed (e.g., you rotated the database password in Secrets Manager), the Kubernetes Secret is updated automatically within the interval.

Pods don't automatically restart when the Secret changes. The Kubernetes Secret is a volume mount or environment variable — pods only see the change when they restart. Two options:

  1. Stakater Reloader: a separate controller that watches Secrets and ConfigMaps and restarts Deployments when they change.
bash
helm install reloader stakater/reloader --namespace reloader-system
yaml
# Annotation on the Deployment triggers Reloader to watch the secret
metadata:
  annotations:
    reloader.stakater.com/auto: "true"
  1. Volume mount with periodic reload: mount the Secret as a volume (not envFrom). Kubernetes updates the volume when the Secret changes without restarting the pod — only if your application watches the file and reloads it.

Generators: Dynamic Secrets

ESO generators create secrets dynamically at sync time. The ECRAuthorizationToken generator fetches an ECR login token (valid for 12 hours):

yaml
1apiVersion: generators.external-secrets.io/v1alpha1
2kind: ECRAuthorizationToken
3metadata:
4  name: ecr-token
5  namespace: payments
6spec:
7  region: us-east-1

The generator produces keys username, password, and proxy_endpoint. Use a template to construct the dockerconfigjson from those values:

yaml
1apiVersion: external-secrets.io/v1beta1
2kind: ExternalSecret
3metadata:
4  name: ecr-pull-secret
5  namespace: payments
6spec:
7  refreshInterval: 6h    # Refresh before the 12h token expires
8  dataFrom:
9    - sourceRef:
10        generatorRef:
11          apiVersion: generators.external-secrets.io/v1alpha1
12          kind: ECRAuthorizationToken
13          name: ecr-token
14  target:
15    name: ecr-pull-credentials
16    template:
17      type: kubernetes.io/dockerconfigjson
18      data:
19        # Construct dockerconfigjson from the generator's username/password/proxy_endpoint fields
20        .dockerconfigjson: |
21          {"auths":{"{{ .proxy_endpoint }}":{"username":"{{ .username }}","password":"{{ .password }}","auth":"{{ printf "%s:%s" .username .password | b64enc }}"}}}

This keeps ECR pull credentials fresh without storing a static Docker config in the cluster.


PushSecret: Writing to External Stores

PushSecret is the inverse — it writes a Kubernetes Secret (typically auto-generated) to an external store. Useful for distributing a Kubernetes-generated certificate or database password to other systems:

yaml
1apiVersion: external-secrets.io/v1alpha1
2kind: PushSecret
3metadata:
4  name: push-tls-cert
5  namespace: payments
6spec:
7  refreshInterval: 24h
8  secretStoreRef:
9    name: aws-secretsmanager
10    kind: SecretStore
11  selector:
12    secret:
13      name: payments-api-tls    # Kubernetes Secret to push (e.g., cert-manager issued cert)
14  data:
15    - match:
16        secretKey: tls.crt    # Key in the Kubernetes Secret
17        remoteRef:
18          remoteKey: payments/tls/certificate    # Destination in AWS Secrets Manager
19    - match:
20        secretKey: tls.key
21        remoteRef:
22          remoteKey: payments/tls/private-key

Frequently Asked Questions

How does ESO differ from Vault Agent Injector?

The Vault Agent Injector adds a sidecar to every pod that fetches secrets directly from Vault and writes them to an in-memory volume. ESO syncs secrets to standard Kubernetes Secrets and doesn't require a sidecar per pod. ESO is simpler to operate (no sidecar lifecycle management, no Vault-specific pod annotations), supports multiple backends in the same cluster, and the secrets are visible to standard Kubernetes tooling. The tradeoff: ESO stores values in Kubernetes Secrets (etcd), so Kubernetes RBAC is your access control layer; Vault Agent provides finer-grained access using Vault policies directly.

How do I handle multi-region secret replication?

Deploy an ESO SecretStore or ClusterSecretStore per region, pointing at the Secrets Manager endpoint for that region. ExternalSecrets in each cluster reference the local region's store. AWS Secrets Manager supports cross-region replication — enable it on the source secret and use the regional endpoint in each cluster's SecretStore.

What happens if AWS Secrets Manager is unavailable?

ESO logs the error and does not update the Kubernetes Secret. The existing Secret value (from the last successful sync) remains in place. Pods continue reading the existing Secret. If the Secret doesn't exist yet (first sync failed), pods that depend on it will fail to start. ESO emits a SecretSyncError event and metric — monitor externalsecrets_sync_call_errors_total in Prometheus.


For Argo CD GitOps that manages ExternalSecret manifests alongside application resources (the recommended pattern for secret references in Git), see Argo CD: GitOps Continuous Delivery for Kubernetes. For cert-manager that generates TLS certificates which can be pushed to AWS Secrets Manager via PushSecret, see cert-manager: Automated TLS Certificates for Kubernetes. For the ESO getting-started guide covering installation, basic SecretStore configuration for AWS Secrets Manager, and ExternalSecret field mapping, see External Secrets Operator on Kubernetes.

Setting up ESO for a multi-team EKS cluster or migrating from Kubernetes Secrets stored in Git to Secrets Manager? Talk to us at Coding Protocols — we help platform teams implement secrets management that satisfies audit requirements without adding operational complexity to every application deployment.

Related Topics

External Secrets Operator
Kubernetes
Secrets Management
AWS Secrets Manager
HashiCorp Vault
Security
EKS
Platform Engineering

Read Next