How It Works
The Kubernetes auth flow:
- Kubernetes mounts a service account JWT at
/var/run/secrets/kubernetes.io/serviceaccount/tokenin every Pod. - The Pod (or Vault Agent sidecar) sends this JWT to Vault's
/v1/auth/kubernetes/loginendpoint along with the role name. - Vault calls the Kubernetes API to validate the JWT signature and check the token hasn't been revoked.
- Vault matches the service account name and namespace against a configured role.
- Vault returns a short-lived Vault token scoped to the policies attached to that role.
- The Pod uses the Vault token to read secrets. The token expires; the Pod re-authenticates as needed.
Zero credential bootstrap: The service account JWT is automatically injected by Kubernetes — no manual secret distribution. This is the core advantage of Kubernetes auth over alternatives like AppRole.
Configure Kubernetes Auth
Enable the Auth Method
export VAULT_ADDR=http://127.0.0.1:8200 # Or via port-forward
export VAULT_TOKEN=
# Enable the Kubernetes auth method
vault auth enable kubernetes
# If Vault is running inside Kubernetes, it can detect the K8s API automatically.
# If Vault is outside (e.g., cloud-hosted HCP Vault), provide the K8s API URL.
Configure the Backend
# Option 1: Vault is running INSIDE Kubernetes
# Vault can use its own pod's service account to talk to the K8s API.
# No manual token_reviewer_jwt needed (Kubernetes 1.21+ with bound SA tokens).
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
# Verify
vault read auth/kubernetes/config
# Option 2: Vault is OUTSIDE Kubernetes (e.g., HCP Vault, separate VM)
# Create a dedicated service account for Vault to call the K8s API:
kubectl create serviceaccount vault-auth -n vault
# In Kubernetes 1.24+, service account tokens are not auto-created.
# Create a long-lived token explicitly:
kubectl apply -f - <
Creating Roles
Binding to Namespace + ServiceAccount
# First, write the policy for grade-api
vault policy write grade-api - <
# Read back the role to verify
vault read auth/kubernetes/role/grade-api
# Key Value
# --- -----
# bound_service_account_names [grade-api-sa]
# bound_service_account_namespaces [grade-api]
# policies [grade-api]
# ttl 1h
# token_max_ttl 0s
Multi-Namespace Roles
# Allow multiple service accounts across multiple namespaces
vault write auth/kubernetes/role/shared-reader \
bound_service_account_names="grade-api-sa,analytics-sa" \
bound_service_account_namespaces="grade-api,analytics" \
policies="shared-secrets-reader" \
ttl=1h
# Wildcard — allow any service account in a namespace (use carefully)
vault write auth/kubernetes/role/namespace-wide \
bound_service_account_names="*" \
bound_service_account_namespaces=grade-api \
policies=grade-api \
ttl=1h
# Wildcard — allow same SA name in any namespace (fine for sidecar use cases)
vault write auth/kubernetes/role/any-ns \
bound_service_account_names=vault-reader \
bound_service_account_namespaces="*" \
policies=readonly \
ttl=1h
Testing Authentication
# Run a test Pod in the grade-api namespace with the correct service account
kubectl run vault-test \
--image=hashicorp/vault:latest \
--serviceaccount=grade-api-sa \
--namespace=grade-api \
--rm -it \
--restart=Never \
-- sh
# Inside the Pod:
# The JWT is automatically mounted here:
JWT=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# Authenticate with Vault
curl -s \
--request POST \
--data "{\"jwt\": \"$JWT\", \"role\": \"grade-api\"}" \
http://vault.vault.svc.cluster.local:8200/v1/auth/kubernetes/login | jq .
# Response includes the Vault token:
# {
# "auth": {
# "client_token": "hvs.CAESIB...",
# "accessor": "...",
# "policies": ["default", "grade-api"],
# "ttl": 3600,
# ...
# }
# }
# Use the token to read a secret
VAULT_TOKEN=hvs.CAESIB...
curl -s \
--header "X-Vault-Token: $VAULT_TOKEN" \
http://vault.vault.svc.cluster.local:8200/v1/secret/data/grade-api/db | jq .data.data
ServiceAccount Setup
# Create the ServiceAccount for grade-api
apiVersion: v1
kind: ServiceAccount
metadata:
name: grade-api-sa
namespace: grade-api
annotations:
# For EKS with IRSA — Vault can also verify AWS IAM identity
# eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/grade-api
---
# Use the ServiceAccount in the Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: grade-api
namespace: grade-api
spec:
template:
spec:
serviceAccountName: grade-api-sa # Use the dedicated SA
automountServiceAccountToken: true # Must be true (default)
containers:
- name: grade-api
image: ghcr.io/your-org/grade-api:latest
# Part 3: Vault Agent sidecar will inject secrets as files or env vars
Exercises
Exercise 1 — Configure Kubernetes Auth: Enable the Kubernetes auth method on your Vault instance. Configure it using Vault's own service account (Vault running inside the cluster). Create a role bound to a test service account in the default namespace.
Exercise 2 — Test Authentication: Create a ServiceAccount in the default namespace. Run a curl-based Pod with that service account. Use the mounted JWT to authenticate to Vault and read a test secret. Verify the token TTL is 1 hour.
Exercise 3 — Least Privilege: Create two roles:
grade-api bound to namespace grade-api with read-only access to secret/grade-api/*, and payment-api bound to namespace payment with read-only access to secret/payment/*. Verify cross-namespace access is denied.
Key Takeaways & Next Steps
Key Takeaways:
- Kubernetes auth uses the Pod's auto-mounted service account JWT — no manual credential distribution
- Vault verifies JWTs with the Kubernetes API server — the trust anchor is the cluster's OIDC/SA signing key
- Roles bind a Vault policy to specific Kubernetes service accounts and namespaces
- Always use
bound_service_account_namespaces— don't wildcard namespaces in production unless you understand the blast radius - Short TTLs (1h or less) limit the damage if a Vault token is leaked
- Each microservice should have its own ServiceAccount and its own Vault role — principle of least privilege
Next in This Track
In Part 3: Secret Injection, we use the Vault Agent Injector and the Vault Secrets Operator to inject secrets directly into Pods as environment variables or mounted files — with automatic renewal and no application code changes.