Install via Helm
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 \
--set installCRDs=true
Within a minute, three pods run in the external-secrets namespace: controller, cert-controller, webhook.
The two CRDs that matter
- SecretStore (or ClusterSecretStore) — how to talk to the external secret backend (Vault address + auth, AWS region + IAM, 1Password vault + token).
- ExternalSecret — "fetch these keys from that SecretStore and create / update this Kubernetes Secret with them, refreshing every N minutes."
Example 1: HashiCorp Vault
Assuming Vault is set up (see that tutorial) at https://vault.lab.example.com with the Kubernetes auth method enabled. First, a SecretStore that authenticates with the cluster's ServiceAccount token:
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-prod
spec:
provider:
vault:
server: "https://vault.lab.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "prod-apps"
serviceAccountRef:
name: "external-secrets"
namespace: "external-secrets"
On the Vault side, the prod-apps role is configured to allow ServiceAccounts from specific namespaces to fetch from specific Vault paths.
Then the actual ExternalSecret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-db-credentials
namespace: myapp
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-prod
kind: ClusterSecretStore
target:
name: app-db-credentials # the K8s Secret to create
creationPolicy: Owner
data:
- secretKey: DB_USER
remoteRef:
key: prod/app/db
property: username
- secretKey: DB_PASSWORD
remoteRef:
key: prod/app/db
property: password
- secretKey: DB_HOST
remoteRef:
key: prod/app/db
property: host
ESO reads Vault's secret/data/prod/app/db, extracts username/password/host, creates / updates a Kubernetes Secret named app-db-credentials with those three keys. The deployment mounts that Secret as env vars normally:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: app
envFrom:
- secretRef: { name: app-db-credentials }
Commit the ExternalSecret + Deployment to Git. Rotate the password in Vault — within the refresh interval, ESO picks it up and updates the K8s Secret; the deployment doesn't know anything changed (until a restart for env vars, or live for mounted files).
Example 2: AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
# IAM role for ServiceAccount (IRSA)
jwt:
serviceAccountRef:
name: external-secrets
namespace: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-key
spec:
refreshInterval: 30m
secretStoreRef: { name: aws-secrets, kind: ClusterSecretStore }
target: { name: api-key }
data:
- secretKey: STRIPE_KEY
remoteRef:
key: prod/stripe
property: live_key
Example 3: 1Password
Useful pattern for solo / small-team use:
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: 1password
spec:
provider:
onepassword:
connectHost: http://onepassword-connect:8080
vaults:
prod: 1
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: github-deploy-token
spec:
refreshInterval: 1h
secretStoreRef: { name: 1password, kind: ClusterSecretStore }
target: { name: github-deploy-token }
data:
- secretKey: GITHUB_TOKEN
remoteRef:
key: "github / deploy token" # Item name
property: "credential" # Field name
The 1Password Connect server runs in-cluster with a Connect Token; users keep their team secrets in 1Password normally.
The full-secret pattern with dataFrom
For pulling many keys at once:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-env
spec:
refreshInterval: 1h
secretStoreRef: { name: vault-prod, kind: ClusterSecretStore }
target:
name: app-env
template:
data:
# Optional templating; otherwise just bulk-copy
DATABASE_URL: "postgres://{{ .username }}:{{ .password }}@{{ .host }}:5432/myapp"
STRIPE_KEY: "{{ .stripe }}"
dataFrom:
- extract:
key: prod/app/all
dataFrom + extract pulls every key from the Vault path into the K8s Secret; template: reshapes them.
PushSecret: the other direction
ESO can also push K8s Secret values out to an external store (useful for "this K8s-generated TLS key needs to be available to non-K8s consumers"):
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: cluster-cert
spec:
refreshInterval: 10m
secretStoreRefs:
- name: vault-prod
kind: ClusterSecretStore
selector:
secret: { name: cluster-tls }
data:
- match:
secretKey: tls.crt
remoteRef: { remoteKey: cluster/tls/cert }
Why this is the right shape
- Single source of truth. Secrets live in the dedicated store; Git holds only references.
- Rotation is automatic. Update once in Vault; ESO syncs everywhere within the refresh interval.
- Auditable. Vault / Secrets Manager logs every fetch; you see what ESO accessed.
- Per-environment cleanly. Dev secrets in one store / path, prod in another — same ExternalSecret shape, different SecretStore.
- GitOps-clean. Argo CD can sync ExternalSecrets to the cluster; the actual sensitive bytes never touch Git or Argo.
Alternatives in the K8s secrets space
- Sealed Secrets (Bitnami) — encrypt the K8s Secret with a public key; commit ciphertext; the cluster-resident controller decrypts. Self-contained but doesn't integrate with external stores.
- SOPS + kustomize-sops / age-decryption operators — encrypt YAML in Git with age / PGP / KMS; decrypt at render. Lighter than ESO for solo setups.
- Direct Vault Agent sidecar — each pod gets a Vault Agent sidecar that injects secrets into a shared volume. More moving parts per pod.
For multi-team Kubernetes with an existing centralized secret store, External Secrets Operator is the canonical bridge in 2026.