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).

Series Secrets Management · part 5 of 7 all posts in series →