DevSecOps / Kubernetes

Mastering Zero-Trust Secrets: Integrating HashiCorp Vault with Kubernetes

1. The Challenge: Beyond Base64

In modern cloud-native environments, the "standard" way of handling secrets (Kubernetes Secret objects) is often insufficient. Kubernetes Secrets are merely Base64 encoded, not encrypted at rest by default. For any organization serious about security, we need Zero-Trust: no hardcoded keys in Git, no sensitive data in Helm charts, and centralized audit trails.

This guide documents the implementation of a High-Availability (HA) Vault cluster and its seamless integration with Spring Boot microservices.

2. The Architecture

Our setup utilizes the Vault Agent Sidecar Injection pattern. Instead of the application "asking" Vault for secrets via API code, Vault "pushes" secrets into the pod as local files.

The Secret Flow:
1. Pod starts with Vault annotations.
2. Injector adds an init-container and a sidecar.
3. Init-container logs into Vault, fetches secrets, and writes them to a shared memory volume (/vault/secrets).
4. Main App container starts and reads the secrets as local files.
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#eff6ff', 'edgeLabelBackground':'#ffffff', 'tertiaryColor': '#f8fafc'}}}%% graph TD A[Vault Server HA Cluster
Raft Storage] -.->|Webhook| B(Vault Injector) B -.->|Monitors Annotations & Injects| C[Pod: Microservice] subgraph K8s Pod C1[Init Container: vault-agent-init] -->|Writes to| C2[(Shared Volume
/vault/secrets)] C2 -->|Reads from| C3[Main App Container
Spring Boot] end C --- C1 C1 == Kubernetes Auth (SA JWT) ==> A A == Returns Secrets ==> C1
Vault HA Dashboard Dashboard Showing Nodes
Vault Dashboard: HA Cluster Nodes

Phase 1: Clean Deployment (Helm)

To install Vault with High Availability (HA) and the Web UI enabled, we use the official Helm chart.

Bash
# 1. Add repo
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

# 2. Install HA Cluster with Raft Storage
helm install vault hashicorp/vault \
  --namespace vault \
  --create-namespace \
  --set "server.ui.enabled=true" \
  --set "server.ui.serviceType=NodePort" \
  --set "server.ha.enabled=true" \
  --set "server.ha.raft.enabled=true" \
  --set "server.ha.raft.setNodeId=true"
Helm Installation Output
Successful Vault Helm deployment showing release status

Phase 2: The "Fortress" Initialization

Vault starts in a "Sealed" state. It cannot read its own data until it is unsealed.

Initializing the Leader

Bash
# 1. Initialize (Generating the Master Keys)
# In production, use -key-shares=5 -key-threshold=3
kubectl exec -n vault vault-0 -- vault operator init -key-shares=1 -key-threshold=1 -format=json > cluster-keys.json

# 2. Extract keys for convenience
export UNSEAL_KEY=$(jq -r ".unseal_keys_b64[0]" cluster-keys.json)
export ROOT_TOKEN=$(jq -r ".root_token" cluster-keys.json)

# 3. Unseal the leader (vault-0)
kubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY

Joining HA Nodes (vault-1 and vault-2)

For a true HA cluster, the other pods must join the Raft cluster:

Bash
# Repeat for vault-1 and vault-2
kubectl exec -n vault vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY

kubectl exec -n vault vault-2 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY

Phase 3: Core Configuration

Log into the Vault CLI to enable the basic engines required for Kubernetes Integration.

Bash
# Enter the Vault container
kubectl exec -it -n vault vault-0 -- /bin/sh

# Login as Admin
vault login $ROOT_TOKEN

# 1. Enable KV-V2 Secrets Engine (The 'internal' path)
vault secrets enable -path=internal kv-v2

# 2. Enable Kubernetes Authentication
vault auth enable kubernetes

# 3. Configure Auth to talk to the K8s API
vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc:443"

Phase 4: Application Onboarding

Follow these steps for every new microservice you deploy to fetch secrets dynamically.

1. Define the Policy (app-policy.hcl)

Create a policy that limits the app to its own folder. Note: KV-V2 requires the data/ prefix in the path.

Bash
vault policy write my-app-policy - <<EOF
path "internal/data/my-app/config" {
  capabilities = ["read"]
}
EOF
Vault Policies Config
Vault Policies & Access Controls Configuration

2. Create the Role

Binds the Kubernetes ServiceAccount to the Policy.

Bash
vault write auth/kubernetes/role/my-app-role \
    bound_service_account_names=my-app-sa \
    bound_service_account_namespaces=production \
    policies=my-app-policy \
    ttl=24h
Vault Kubernetes Role Setup
Binding the Kubernetes ServiceAccount to the Vault Policy

3. Seed the Secrets

Push the actual sensitive data into Vault.

Bash
vault kv put internal/my-app/config \
    DB_PASSWORD="super-secret-pass" \
    API_KEY="AIzaSyB..." \
    S3_SECRET="a6DuZ0E..."
Vault Write KV Secrets
Creating Key-Value secrets in the Vault GUI/CLI

Phase 5: Troubleshooting & Best Practices

The "Permission Denied" (403) Checklist

If a pod is stuck in Init:0/1, check these 3 things:
1. Policy Path: Does it have /data/? (e.g., internal/data/...)
2. Role Binding: Does the bound_service_account_namespaces include the namespace where your app is running?
3. Role Name: Does the vault.hashicorp.com/role annotation in the Deployment match the role name in Vault?

Best Practice: The Backup (Snapshot)

Vault data is encrypted, but you must still back up the encrypted database.

Bash
# Take a snapshot of the Raft database
kubectl exec -n vault vault-0 -- vault operator raft snapshot save /share/vault-backup.snap

Best Practice: Maintenance

When a Vault pod restarts (e.g., after a node upgrade), it will be Sealed.

Action: Always check kubectl get pods -n vault. If any show 0/1 Ready, you must run the unseal command again for that pod (unless auto-unseal is configured).
Summary of Architecture:
Auth: Kubernetes ServiceAccount JWT.
Transport: Vault Agent Sidecar (Injector).
Storage: Local Raft (HA).
Security: Path-based ACL Policies.

Conclusion

By moving secrets into Vault, we didn't just improve security; we drastically improved Developer Experience (DX). Developers no longer manage and pass around secret.yaml files. They simply define what they need directly in Vault, and the infrastructure handles the rest securely and transparently.

Status: Successfully Deployed Security: Hardened Complexity: Managed
#Vault #Kubernetes #ZeroTrust #DevSecOps #Security #Helm #Architecture
Hamza Hssaini
Cloud & Kubernetes Enthusiast | DevOps Engineer