HashiCorp Vault — Project: Kubernetes Secrets

3 minute read

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

Next: Encryption as a Service Project