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.