External Secrets Operator + HashiCorp Vault: Kubernetes auth flow

Published: 2026-03-19

SealedSecrets work well for most cluster credentials. But some secrets — like shared service credentials managed by a security team — need to live in Vault and be automatically synchronized into Kubernetes. External Secrets Operator (ESO) bridges the two.


The components

  • HashiCorp Vault — runs on infra cluster, deployed via a custom charts/vault/ Helm chart.
  • External Secrets Operator — deployed via base/external-secrets/helmrelease.yaml on infra.
  • ClusterSecretStore — cluster-scoped ESO resource that defines how to connect to Vault.
  • ExternalSecret — defines which Vault path to sync into which Kubernetes Secret.

ESO HelmRelease

yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: external-secrets
spec:
  chart:
    spec:
      chart: external-secrets
      version: "0.14.4"
      sourceRef:
        kind: HelmRepository
        name: external-secrets
        namespace: flux-system
  values:
    installCRDs: true
    replicaCount: 1
    serviceMonitor:
      enabled: true
      interval: 60s
      labels:
        release: kube-prom-stack

Vault Kubernetes auth setup

Before ESO can authenticate to Vault, the Kubernetes auth method must be configured once per cluster:

bashvault auth enable -path=kubernetes-infra kubernetes

vault write auth/kubernetes-infra/config \
  kubernetes_host="https://172.16.57.10:6443" \
  kubernetes_ca_cert=@/tmp/infra-ca.crt \
  token_reviewer_jwt="$(kubectl create token -n external-secrets external-secrets)"

vault policy write infra-default - <<EOF
path "secret/data/infra/*" { capabilities = ["read"] }
EOF

vault write auth/kubernetes-infra/role/external-secrets \
  bound_service_account_names=external-secrets \
  bound_service_account_namespaces=external-secrets \
  policies=infra-default \
  ttl=1h

The token_reviewer_jwt is used by Vault to call the Kubernetes TokenReview API and validate incoming tokens.


ClusterSecretStore

yamlapiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-infra
spec:
  provider:
    vault:
      server: "http://vault.vault.svc.cluster.local:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes-infra"
          role: "external-secrets"
          serviceAccountRef:
            name: "external-secrets"
            namespace: "external-secrets"

Vault is accessible inside the infra cluster by its service ClusterIP. No external endpoint needed.


ExternalSecret

yamlapiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: reportportal-creds
  namespace: infra
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-infra
    kind: ClusterSecretStore
  target:
    name: reportportal-creds
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef:
        key: secret/infra/reportportal
        property: password

ESO pulls secret/infra/reportportal from Vault's KV v2 engine every hour and creates/updates the reportportal-creds Secret. creationPolicy: Owner means if the ExternalSecret is deleted, the Secret is deleted too.


Syncing multiple fields from one Vault path

yamlspec:
  dataFrom:
    - extract:
        key: secret/infra/postgres

dataFrom.extract pulls all fields from the Vault path as separate keys in the Secret. Useful when a Vault path contains username, password, host, port — all become separate Secret keys.


Sealed Secrets vs External Secrets

Use case Sealed Secrets External Secrets
kubeconfig files
Docker registry creds
TLS certificates
Shared service creds (DB passwords)
Secrets rotated by security team
Secrets that need audit trail ✓ (via Vault audit)

Both tools coexist in the same cluster.


Checking sync status

bashkubectl get externalsecret -A

kubectl describe externalsecret reportportal-creds -n infra
# Look for: Status.Conditions - Ready=True

SecretSyncedError fires if Vault is sealed or the path doesn't exist.


Troubleshooting

ESO cannot authenticate to Vault:

bashkubectl logs -n external-secrets deploy/external-secrets
# Look for: "authentication error"

Common cause: token_reviewer_jwt expired or the SA was recreated after auth setup.

Vault path not found:

bashvault kv get secret/infra/reportportal

The path in remoteRef.key must match exactly (without the data/ prefix for KV v2 — ESO handles this automatically).