
External secrets operator for Kubernetes: HashiCorp Vault GitOps setup
External secrets operator for Kubernetes: HashiCorp Vault GitOps setup
7 October 2025
Kilian Niemegeerts

This is part 3 of our HashiCorp Vault production series. In part 1, we explained why we run Vault in a separate cluster. Now comes the next challenge: how do you get secrets from Vault to your applications without storing them in Git?
Series overview:
- Production Kubernetes Architecture with HashiCorp Vault
- Terraform Infrastructure for HashiCorp Vault on EKS
- External Secrets Operator: GitOps for Kubernetes Secrets
- Dynamic PostgreSQL Credentials with HashiCorp Vault
- Vault Agent vs Secrets Operator vs CSI Provider
- Securing Vault Access with Internal NLB and VPN
What is External Secrets Operator for Vault Integration?
External Secrets Operator (ESO) is a Kubernetes operator that synchronizes secrets from external systems like HashiCorp Vault into Kubernetes secrets. It solves a fundamental GitOps problem: keeping sensitive data out of Git while maintaining declarative configuration.
In our setup, ESO:
- Reads secrets from HashiCorp Vault
- Creates native Kubernetes secrets
- Keeps them synchronized
- Integrates perfectly with FluxCD for GitOps workflows
The GitOps Secret Management Problem in Kubernetes
In a GitOps workflow, everything should be in Git. But putting secrets in Git, even encrypted, creates risks:
- Accidental exposure in pull requests
- Permanent history of sensitive data
- Complex key management for encryption
ESO solves this by storing only the reference to secrets in Git, not the secrets themselves.
Implementing External Secrets Operator with HashiCorp Vault
Here’s how we configured External Secrets Operator to work with our Vault setup.
Prerequisites
From our architecture:
- HashiCorp Vault running in tooling cluster (see part 1)
- FluxCD managing both clusters
- Vault accessible at vault.tooling.internal:8200
Installing external secrets operator
ESO is deployed via FluxCD in our application cluster:
apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmRepository metadata: name: external-secrets namespace: external-secrets-system spec: interval: 1h url: https://charts.external-secrets.io --- apiVersion: helm.toolkit.fluxcd.io/v2beta1 kind: HelmRelease metadata: name: external-secrets namespace: external-secrets-system spec: interval: 5m chart: spec: chart: external-secrets sourceRef: kind: HelmRepository name: external-secrets
Configuring ClusterSecretStore for Vault
The ClusterSecretStore tells ESO how to connect to Vault:
apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: name: vault-backend spec: provider: vault: server: "https://vault.tooling.internal:8200" path: "secret" version: "v2" auth: kubernetes: mountPath: "kubernetes" role: "external-secrets" serviceAccountRef: name: "external-secrets" namespace: "external-secrets-system"
This configuration:
- Points to our internal Vault instance
- Uses Kubernetes authentication
- References a service account that Vault trusts
Creating external secrets
With the ClusterSecretStore configured, we can create ExternalSecret resources:
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-secrets namespace: production spec: secretStoreRef: name: vault-backend kind: ClusterSecretStore refreshInterval: 15m target: name: app-secrets creationPolicy: Owner data: - secretKey: database-password remoteRef: key: secret/data/production/database property: password - secretKey: api-key remoteRef: key: secret/data/production/external-api property: key
ESO will:
- Connect to Vault using the ClusterSecretStore config
- Fetch the specified secrets
- Create a Kubernetes secret named app-secrets
- Refresh every 15 minutes
FluxCD and HashiCorp Vault integration for complete GitOps
The real power comes from combining ESO with FluxCD’s substitution feature. This allows us to use secrets in our GitOps workflow without exposing them.
Secret substitution in FluxCD with external secrets
FluxCD can substitute variables in your manifests using data from secrets:
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 kind: Kustomization metadata: name: app-deployment namespace: flux-system spec: interval: 10m path: "./apps/production" prune: true sourceRef: kind: GitRepository name: flux-system postBuild: substituteFrom: - kind: Secret name: app-secrets optional: false
Using substituted values
In your application manifests, use placeholders:
apiVersion: v1 kind: ConfigMap metadata: name: app-config data: database-url: "postgres://app:${database-password}@db:5432/app" external-api-endpoint: "https://api.example.com" external-api-key: "${api-key}"
FluxCD will replace ${database-password} and ${api-key} with values from the ESO-generated secret.
Production patterns with external secrets operator and HashiCorp Vault
Namespace isolation
We use SecretStore (namespace-scoped) instead of ClusterSecretStore for application-specific secrets:
apiVersion: external-secrets.io/v1beta1 kind: SecretStore metadata: name: team-a-vault namespace: team-a spec: provider: vault: server: "https://vault.tooling.internal:8200" path: "secret/team-a" # Rest of config similar to ClusterSecretStore
Secret rotation handling
While ESO doesn’t support automatic rotation for dynamic secrets, the refresh interval ensures updates are pulled regularly. For dynamic credentials, we use Vault Agent (covered in part 4).
Monitoring secret synchronization
Check ESO sync status:
kubectl get externalsecrets -A
Look for the READY and LAST SYNC columns to verify secrets are syncing properly.
Troubleshooting common issues
ESO can’t connect to Vault
Verify the ClusterSecretStore status:
kubectl describe clustersecretstore vault-backend
Common issues:
- Service account doesn’t exist
- Vault Kubernetes auth not configured
- Network connectivity between clusters
Secrets not updating
Check the ExternalSecret events:
kubectl describe externalsecret app-secrets -n production
The refresh interval might be too long, or there could be permission issues in Vault.
Production considerations
High Availability
ESO runs as a deployment with multiple replicas. Ensure your configuration includes:
replicaCount: 3 resources: requests: memory: 64Mi cpu: 10m
Security
- ESO service account should have minimal Vault permissions
- Use separate Vault paths per environment/team
- Enable Vault audit logging for ESO access
Performance
With hundreds of ExternalSecrets, consider:
- Increasing ESO replicas
- Tuning refresh intervals
- Using webhook triggers instead of polling
What’s Next?
We’ve covered how to sync static secrets from Vault to Kubernetes using ESO. But what about dynamic database credentials that expire? That’s where Vault’s database secret engine shines – covered in our next post.
The complete configuration shown here is available in our GitHub repository, including:
- Full ESO deployment manifests
- Example ExternalSecrets
- FluxCD integration examples
- Vault configuration for ESO
Sorry, the comment form is closed at this time.