Example of using External Secrets with Vault and Argo CD. The demo application is a Go web server that reads database credentials from a mounted Kubernetes secret and automatically reloads them when they change—no pod restart required.
Read the full blog at https://codefresh.io/blog/gitops-secrets-with-argo-cd-hashicorp-vault-and-the-external-secret-operator/
HashiCorp Vault → External Secrets Operator → Kubernetes Secret → Application
ESO polls Vault every 15 seconds and writes values into a native Kubernetes Secret. The app mounts that secret as a file and uses fsnotify to reload it without restarting.
- A Kubernetes cluster with
cluster-adminpermissions kubectland Argo CD installed in theargocdnamespace
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent
.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
kubectl get all -n argocd
argocd admin initial-password -n argocd
argocd login localhost:80Install Vault in dev mode (root token = root, pre-unsealed) via Argo CD:
argocd app create vault \
--project default \
--repo https://helm.releases.hashicorp.com \
--helm-chart vault \
--revision 0.28.0 \
--sync-policy auto \
--sync-option CreateNamespace=true \
--parameter server.dev.enabled=true \
--dest-namespace vault \
--dest-server https://kubernetes.default.svcExec into the pod and configure it:
kubectl exec -it vault-0 -n vault -- /bin/shvault login root
# Write the demo credentials
vault kv put secret/mysql_credentials \
url="mysql.example.com:3306" \
username="my_demo_user" \
password="my_demo_password"
# Enable Kubernetes auth
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT"
# Policy and role for ESO
vault policy write eso-read-policy - <<EOF
path "secret/*" {
capabilities = [ "read", "list" ]
}
EOF
vault write auth/kubernetes/role/demo \
bound_service_account_names=* \
bound_service_account_namespaces=* \
policies=eso-read-policy \
ttl=24h
exitargocd app create eso \
--project default \
--repo https://charts.external-secrets.io \
--helm-chart external-secrets \
--revision 0.9.19 \
--sync-policy auto \
--sync-option CreateNamespace=true \
--dest-namespace external-secrets \
--dest-server https://kubernetes.default.svcApply the ClusterSecretStore (connects ESO to Vault using the demo Kubernetes auth role):
argocd app create vault-secret-store \
--project default \
--repo https://github.com/kostis-codefresh/external-secrets-gitops-example.git \
--path "./manifests/vault-integration" \
--sync-policy auto \
--dest-namespace external-secrets \
--dest-server https://kubernetes.default.svc
kubectl get clustersecretstore vault-backend # should show READY: TruePoint an Argo CD application at manifests/app in this repo. That directory contains the Deployment, Service, and the ExternalSecret that creates the mysql-credentials Kubernetes secret.
argocd app create my-secret-app \
--project default \
--repo https://github.com/kostis-codefresh/external-secrets-gitops-example.git \
--path "./manifests/app" \
--sync-policy auto \
--dest-namespace default \
--dest-server https://kubernetes.default.svcOnce synced, verify and access the app:
kubectl get externalsecret my-db-credentials # STATUS: SecretSynced
kubectl port-forward svc/gitops-secrets-service 8080:8080Open http://localhost:8080 to see the current credentials.
Update the secret in Vault:
kubectl exec -it vault-0 -n vault -- vault kv put secret/mysql_credentials \
url="mysql.example.com:3306" \
username="rotated_user" \
password="new_super_secret_password"You can also use the Vault UI but at http://localhost:8200/ui/vault/secrets/secret/kv/mysql_credentials/metadata/versions.
Port forward with kubectl port-forward -n vault vault-0 8200:8200
Then login using root as token.
Within ~15 seconds the app reloads automatically—no restart, no Argo CD sync:
kubectl logs -f -l app=gitops-secrets-app
# Config file changed: /secrets/credentials
# Username is rotated_user
# Password is new_super_secret_passwordRefresh http://localhost:8080 to confirm.