BlogSecrets

Vault Kubernetes auth: secrets without static tokens

Let pods authenticate to Vault with their service account token and get short-lived secrets — no bootstrap secret to leak.

Jun 9, 2026·4 min readAdvanced·By the SecOpsLog team · command-tested

Every secrets manager has a bootstrap problem: the workload needs a credential to fetch its credentials. Hard-coding a Vault token in a Kubernetes Secret just moves the leak one hop left. Vault's Kubernetes auth method breaks the loop — the pod presents the service account token it already has (preferably a short-lived projected token), Vault verifies it against the cluster's TokenReview API, maps the caller to a role with bound service account name and namespace, and returns a Vault token scoped by policy with a TTL measured in minutes.

The trust chain runs pod → API server TokenReview → Vault role → policy → secret path. Tighten any link and the whole chain holds. A wildcard on bound_service_account_namespaces means any pod in any namespace with a matching SA name can authenticate — that is how a compromised dev namespace reads production database credentials. The Vault from dev to production track covers auth methods, policies, and Kubernetes integration hands-on.

Pod → Vault via Kubernetes auth

The SA JWT is identity. The Vault role is permission. TokenReview sits in the middle.

Pod SA JWT kube-apiserver TokenReview Vault k8s auth method Secret short TTL 1 Login with SA token vault write auth/kubernetes/login role bound to SA + namespace Vault calls TokenReview 2 Policy + role read-only on one path never reuse cluster-admin audience-bound JWT preferred 3 Fetch + revoke Vault token → secret lease TTL or pod exit no long-lived static creds Identity is the SA JWT. Permissions are the Vault role — not the K8s ClusterRole. pod → TokenReview → Vault role → short-lived secret
Login — SA JWTAuthorize — Vault roleSecret — short TTL
Kubernetes auth trust chain

The Vault role is the security boundary. Copy-pasting an admin policy onto every workload role defeats the entire design.

1Enable authvault auth enable kubernetes2ConfigureK8s API + CA + reviewer JWT3Policyread-only on one path4Rolebound SA + namespace5Pod loginSA token → Vault token6Fetch secretdynamic or KV7Revokelease TTL or pod exit

Enable and configure the auth method

Vault needs three things to talk to the API server: the Kubernetes API URL, the cluster CA certificate, and a JWT with permission to call TokenReview. On Vault running inside the cluster, the in-cluster config works. External Vault needs explicit values and a dedicated reviewer service account — never reuse a cluster-admin token for this.

vault-k8s-config.sh
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token

Bind a service account to a least-privilege policy

The role ties a Kubernetes identity to a Vault policy. Set bound_service_account_names and bound_service_account_namespaces to exact values — not *. Keep TTL short; a 20-minute token limits blast radius if a pod is compromised. Use dynamic database credentials where possible so the fetched secret is also short-lived.

vault-k8s-role.sh
vault policy write payments-read - <<EOF
path "database/creds/payments" {
capabilities = ["read"]
}
EOF
vault write auth/kubernetes/role/payments \
bound_service_account_names=payments \
bound_service_account_namespaces=shop \
policies=payments-read \
ttl=20m
Scope the binding tightly
bound_service_account_namespaces is the security boundary. A wildcard namespace binding means any pod in any namespace with that service account name can assume the role. Separate roles per namespace and per workload — never one payments-read role for the entire cluster.

Consume secrets from the pod

The Vault Agent Injector adds a sidecar that logs in via Kubernetes auth and writes secrets to a shared in-memory volume. Your application reads a file — no Vault SDK required, no static token in a Secret. For GitOps-heavy teams, External Secrets Operator uses the same auth backend to materialize native Kubernetes Secrets on a refresh interval.

deploy-injector.yaml
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "payments"
vault.hashicorp.com/agent-inject-secret-db: "database/creds/payments"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/payments" -}}
username={{ .Data.username }}
password={{ .Data.password }}
{{- end -}}
bash — inside the podlive
kubectl exec -n shop payments-0 -- cat /vault/secrets/db
username=v-kubernetes-shop-payments-x9f2
password=A1a-3f7c9d2e...
vault token lookup -format=json | jq .data.ttl
fresh DB user, leased — revoked when pod or TTL ends

Where this goes next

Static KV secrets synced via Kubernetes auth are a stepping stone. Next: dynamic credentials from Vault's database and cloud secrets engines so each pod gets a unique lease, and External Secrets or CSI drivers for GitOps-native delivery. The Vault from dev to production path covers auth methods, dynamic secrets, HA, and rotation without midnight restarts.

Go deeper in a courseVault from dev to productionAuth methods, dynamic secrets, Kubernetes integration, and HA.View course

Related posts