HashiCorp Vault — Project: Kubernetes Secrets
Build a complete Kubernetes secrets management solution using Vault — from deploying Vault on K8s to injecting secrets into application pods.
Project Overview
┌─────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─────────┐ ┌───────────────┐ ┌────────────┐ │
│ │ Vault │ │ Vault Agent │ │ App Pods │ │
│ │ (HA) │◄──│ Injector │──►│ (secrets) │ │
│ └─────────┘ └───────────────┘ └────────────┘ │
│ │ ▲ │
│ │ ┌───────────────┐ │ │
│ └────────►│ CSI Provider │───────────┘ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────┘
Step 1 — Deploy Vault on Kubernetes
# Add Helm repo
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
# Create namespace
kubectl create namespace vault
# values.yaml
server:
ha:
enabled: true
replicas: 3
raft:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "raft" {
path = "/vault/data"
}
service_registration "kubernetes" {}
resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 512Mi
cpu: 500m
dataStorage:
enabled: true
size: 10Gi
injector:
enabled: true
resources:
requests:
memory: 64Mi
cpu: 50m
csi:
enabled: true
# Install
helm install vault hashicorp/vault \
--namespace vault \
--values values.yaml
# Initialize and unseal
kubectl exec -n vault vault-0 -- vault operator init \
-key-shares=3 -key-threshold=2 -format=json > vault-init.json
# Unseal each node
for i in 0 1 2; do
kubectl exec -n vault vault-$i -- vault operator unseal $(jq -r '.unseal_keys_b64[0]' vault-init.json)
kubectl exec -n vault vault-$i -- vault operator unseal $(jq -r '.unseal_keys_b64[1]' vault-init.json)
done
# Join node 1 and 2 to the Raft cluster
kubectl exec -n vault vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -n vault vault-2 -- vault operator raft join http://vault-0.vault-internal:8200
Step 2 — Configure Kubernetes Auth
# Exec into Vault pod
kubectl exec -n vault -it vault-0 -- /bin/sh
# Login with root token
vault login $(jq -r '.root_token' /tmp/vault-init.json)
# Enable Kubernetes auth
vault auth enable kubernetes
# Configure (auto-discovers when running in K8s)
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
Step 3 — Create Secrets and Policies
# Enable KV
vault secrets enable -path=apps kv-v2
# Create application secrets
vault kv put apps/webapp/config \
db_host="postgres.default.svc.cluster.local" \
db_name="webapp" \
db_user="webapp" \
db_password="super-secret-password" \
redis_url="redis://redis.default.svc.cluster.local:6379"
# Create policy
vault policy write webapp - <<EOF
path "apps/data/webapp/*" {
capabilities = ["read"]
}
EOF
# Create K8s auth role
vault write auth/kubernetes/role/webapp \
bound_service_account_names=webapp \
bound_service_account_namespaces=default \
policies=webapp \
ttl=1h
Step 4 — Deploy App with Agent Injector
# webapp-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: webapp
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "webapp"
vault.hashicorp.com/agent-inject-secret-config: "apps/data/webapp/config"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "apps/data/webapp/config" -}}
DB_HOST={{ .Data.data.db_host }}
DB_NAME={{ .Data.data.db_name }}
DB_USER={{ .Data.data.db_user }}
DB_PASSWORD={{ .Data.data.db_password }}
REDIS_URL={{ .Data.data.redis_url }}
{{- end }}
spec:
serviceAccountName: webapp
containers:
- name: webapp
image: myapp:latest
command: ["sh", "-c", "source /vault/secrets/config && exec node server.js"]
ports:
- containerPort: 8080
kubectl apply -f webapp-deployment.yaml
Step 5 — Deploy App with CSI Provider
# csi-webapp.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-webapp
spec:
provider: vault
parameters:
vaultAddress: "http://vault.vault.svc.cluster.local:8200"
roleName: "webapp"
objects: |
- objectName: "db-password"
secretPath: "apps/data/webapp/config"
secretKey: "db_password"
- objectName: "redis-url"
secretPath: "apps/data/webapp/config"
secretKey: "redis_url"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-csi
spec:
replicas: 1
selector:
matchLabels:
app: webapp-csi
template:
metadata:
labels:
app: webapp-csi
spec:
serviceAccountName: webapp
containers:
- name: webapp
image: myapp:latest
volumeMounts:
- name: secrets
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-webapp
Step 6 — Verify
# Check injector pods
kubectl get pods -l app=webapp
# Verify secrets are injected
kubectl exec deploy/webapp -- cat /vault/secrets/config
# Check CSI-mounted secrets
kubectl exec deploy/webapp-csi -- cat /mnt/secrets/db-password
# Check Vault Agent logs
kubectl logs deploy/webapp -c vault-agent
Architecture Comparison
| Method | Pros | Cons |
|---|---|---|
| Agent Injector | Template support, auto-renewal | Sidecar overhead |
| CSI Provider | No sidecar, native K8s | No auto-renewal, volume-based |
| External Secrets | K8s-native, reconciliation | Extra operator, polling delay |