All credentials live in Vault. Never put secrets in Helm values, ConfigMaps, plain env values, or the repo. An ExternalSecret tells ESO which Vault entry to copy into a normal Kubernetes Secret, and your pods read that Secret.
Vault (secrets-k8s-ranch/apps/myapp) ──ESO──▶ Secret myapp-secrets ──▶ Pod env / volume
| Setting | Value |
|---|---|
| Vault | https://hcl-vault.rachelski.internal |
| KV mount | secrets-k8s-ranch (KV v2) |
| Path convention | apps/<app-name>, one path per app |
| ClusterSecretStore | vault-ranch-external-secrets-operator-secrets (available in every namespace) |
| API version | external-secrets.io/v1 (v1alpha1 and v1beta1 are no longer served) |
The app owner creates the values in Vault. The CLI and the UI both work:
vault kv put -mount=secrets-k8s-ranch apps/myapp \
API_TOKEN='...' \
OIDC_CLIENT_ID='myapp' \
OIDC_CLIENT_SECRET='...'
If the Secret will be loaded with env_from, name the keys as environment variables (UPPER_SNAKE_CASE). The Kubernetes Secret keys match the Vault keys exactly.
Database passwords don't go in Vault. CNPG and the MariaDB operator generate those Secrets themselves (see the CNPG page).
Option A: Copy everything at the path (the default, simplest). Every key under apps/myapp becomes a key in the Secret:
resource "kubectl_manifest" "myapp_secrets" {
yaml_body = <<-YAML
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-secrets
namespace: myapp
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-ranch-external-secrets-operator-secrets
kind: ClusterSecretStore
target:
name: myapp-secrets
creationPolicy: Owner # Secret is deleted with the ExternalSecret
dataFrom:
- extract:
key: apps/myapp # relative to the secrets-k8s-ranch mount
YAML
}
Option B: Pick and rename specific keys. Use this when the app expects different key names, or when you want separate Secrets for different purposes from one Vault path:
resource "kubectl_manifest" "myapp_oidc_secret" {
yaml_body = <<-YAML
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-oidc
namespace: myapp
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-ranch-external-secrets-operator-secrets
kind: ClusterSecretStore
target:
name: myapp-oidc
data:
- secretKey: client-id # key in the K8s Secret
remoteRef:
key: apps/myapp # Vault path
property: OIDC_CLIENT_ID # key in Vault
- secretKey: client-secret
remoteRef:
key: apps/myapp
property: OIDC_CLIENT_SECRET
YAML
}
Option C: Build a value from several keys. Use a template when the app wants a connection string or a small config file:
target:
name: myapp-config
template:
data:
REDIS_URL: "redis://:{{ .REDIS_PASSWORD }}@myapp-redis.myapp:6379/0"
dataFrom:
- extract:
key: apps/myapp
All of a Secret's keys as env vars (with Option A):
env_from {
secret_ref {
name = "myapp-secrets"
}
}
A single key:
env {
name = "OIDC_CLIENT_SECRET"
value_from {
secret_key_ref {
name = "myapp-oidc"
key = "client-secret"
}
}
}
For Helm charts, use the chart's existingSecret option (or similar) and point it at the Secret name. Don't copy values into the values file.
Add depends_on = [kubectl_manifest.myapp_secrets] to the Deployment or helm_release so the Secret exists before pods start. Otherwise they fail with CreateContainerConfigError until ESO catches up.
refreshInterval (1h in the examples). To sync immediately, run:kubectl annotate externalsecret myapp-secrets -n myapp force-sync=$(date +%s) --overwritekubectl rollout restart deploy/myapp -n myappsubPath) update in place, but the app still has to re-read them.kubectl get externalsecret -n myapp # STATUS should be SecretSynced, READY True
kubectl describe externalsecret myapp-secrets -n myapp # events show the Vault error
| Symptom | Usual cause |
|---|---|
SecretSyncedError: could not get secret data |
Wrong path or property, or the key doesn't exist in Vault yet. Paths are relative to the mount: apps/myapp, not secrets-k8s-ranch/apps/myapp or secrets-k8s-ranch/data/apps/myapp. |
Pod stuck in CreateContainerConfigError |
The Secret hasn't been created yet. Check the ExternalSecret status and add depends_on. |
| Changed in Vault but the app still has the old value | Wait for or force a refresh, then restart the Deployment. |
apps/<app-name>). Only reference your own app's path. The ClusterSecretStore can technically read every apps/* path from any namespace, so this rule is a convention, not something the platform enforces..tfvars, Helm values, ConfigMaps or git.external-secrets.io/v1 with dataFrom.extract or data.remoteRef. Older examples using v1alpha1 or dataFrom: - key: don't work on this cluster.