HashiCorp Vault on Kubernetes: complete installation guide for 2026
HashiCorp Vault is the reference tool for secrets management in Cloud Native architectures. Combined with Kubernetes, it centralises passwords, certificates, API keys and tokens, and distributes them to applications dynamically, with full audit and security. This complete guide walks you, step by step, through deploying Vault in high availability on Kubernetes in 2026 — with integrated Raft backend, TLS, native integration via the Vault Agent Injector, S3 backups and operational best practices.
Why HashiCorp Vault on Kubernetes?
Native Kubernetes Secrets are encrypted at rest (provided encryption-config.yaml is enabled on the API server, which is not the default everywhere): not enough for production. Vault delivers:
- centralisation of secrets for Kubernetes and the rest of the IT estate (CI/CD, legacy apps, databases);
- dynamic rotation of secrets (DB passwords generated on the fly, short-lived PKI certificates);
- a complete audit log of every access;
- a rich RBAC via HCL policies;
- native Kubernetes integration via the
kubernetesauth method, the Vault Agent Injector and the CSI Driver.
From experience, rolling out Vault is also setting a standard: developers stop committing .env files to Git and every application receives its secrets through injection at startup.
Prerequisites
- Kubernetes 1.28+ or OpenShift 4.14+ cluster with at least 3 worker nodes (for Raft HA).
- Helm 3.14+ installed locally.
- A dynamic StorageClass (NFS, NetApp, Longhorn, EBS, Ceph…) supporting
ReadWriteOncePVCs. - A cert-manager or cluster Issuer to provision TLS certificates (otherwise self-signed).
- An S3 bucket (or compatible — MinIO, Scaleway Object Storage) for Raft snapshots.
- The
vaultCLI (≥ 1.18) installed on your workstation.
Target architecture
The recommended production architecture rests on four points:
- 3 Vault replicas spread across 3 nodes (anti-affinity) to tolerate the loss of one node.
- Integrated Raft backend: no need for Consul, Vault handles the quorum itself.
- End-to-end TLS between Vault pods, managed by cert-manager.
- Auto-unseal via a KMS key (AWS KMS, GCP KMS) or a transit Vault, to avoid manual unsealing after every restart.
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes cluster (3 workers) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ vault-0 │◄──►│ vault-1 │◄──►│ vault-2 │ (Raft quorum) │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ PVC (dynamic StorageClass) │ │
│ └──────────────────────────────────────┘ │
│ │
│ Vault Agent Injector ──► injects secrets into app pods │
└─────────────────────────────────────────────────────────────┘
│
▼ Raft snapshots (daily cron)
┌──────────┐
│ S3 bucket│
└──────────┘Step-by-step Helm install
1. Prepare the namespace
kubectl create namespace vault
kubectl label namespace vault pod-security.kubernetes.io/enforce=restricted
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update2. Provision TLS certificates with cert-manager
# vault-tls.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: vault-tls
namespace: vault
spec:
secretName: vault-tls
duration: 8760h # 1 year
renewBefore: 720h # renew 30 days before expiry
commonName: vault.vault.svc.cluster.local
dnsNames:
- vault
- vault.vault
- vault.vault.svc
- vault.vault.svc.cluster.local
- "*.vault-internal.vault.svc.cluster.local"
issuerRef:
name: vault-ca-issuer
kind: ClusterIssuerkubectl apply -f vault-tls.yaml
kubectl -n vault get secret vault-tls # NAME: vault-tls TYPE: kubernetes.io/tls3. Prepare the Vault values.yaml
# values-vault.yaml
global:
enabled: true
tlsDisable: false
injector:
enabled: true
replicas: 2
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 250m, memory: 256Mi }
server:
image:
repository: hashicorp/vault
tag: "1.18.3"
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { cpu: "1", memory: "1Gi" }
ha:
enabled: true
replicas: 3
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
tls_disable = false
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/vault/userconfig/vault-tls/tls.crt"
tls_key_file = "/vault/userconfig/vault-tls/tls.key"
tls_client_ca_file = "/vault/userconfig/vault-tls/ca.crt"
}
storage "raft" {
path = "/vault/data"
}
service_registration "kubernetes" {}
seal "awskms" {
region = "eu-west-3"
kms_key_id = "alias/vault-unseal"
}
extraEnvironmentVars:
VAULT_CACERT: /vault/userconfig/vault-tls/ca.crt
volumes:
- name: vault-tls
secret:
secretName: vault-tls
volumeMounts:
- name: vault-tls
mountPath: /vault/userconfig/vault-tls
readOnly: true
dataStorage:
enabled: true
size: 20Gi
storageClass: "standard-rwo"
affinity: |
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: {{ template "vault.name" . }}
app.kubernetes.io/instance: "{{ .Release.Name }}"
topologyKey: kubernetes.io/hostname
ui:
enabled: true
serviceType: ClusterIPA few things to keep in mind:
- AWS KMS auto-unseal: if you don’t have KMS, drop the
sealstanza and unseal manually (see below). - Anti-affinity: essential to avoid all 3 replicas landing on the same node.
- Resources: those values are a floor. Tune them under load.
4. Install Vault
helm install vault hashicorp/vault
-n vault
-f values-vault.yaml
# Verify
kubectl -n vault get pods -w
# vault-0 0/1 Running (sealed)
# vault-1 0/1 Running (sealed)
# vault-2 0/1 Running (sealed)
# vault-agent-injector-xxx 1/1 RunningVault pods stay 0/1 Running until the cluster is initialised — that’s expected.
Cluster initialisation and unsealing
# Initialise from the vault-0 pod
kubectl -n vault exec -it vault-0 -- vault operator init
-key-shares=5 -key-threshold=3 -format=json > vault-init.json
# vault-init.json contains the 5 unseal keys and the root token
# →→→ STORE IT IN AN OFFLINE VAULT OR ANOTHER VAULT INSTANCE ←←←
# If you don't use auto-unseal KMS, unseal manually (3 keys out of 5)
for K in $(jq -r '.unseal_keys_b64[0,1,2]' vault-init.json); do
kubectl -n vault exec vault-0 -- vault operator unseal "$K"
done
# Join vault-1 and vault-2 to the Raft cluster
for POD in vault-1 vault-2; do
kubectl -n vault exec $POD --
vault operator raft join
-address=https://$POD.vault-internal:8200
-leader-ca-cert="$(cat ca.crt)"
https://vault-0.vault-internal:8200
for K in $(jq -r '.unseal_keys_b64[0,1,2]' vault-init.json); do
kubectl -n vault exec $POD -- vault operator unseal "$K"
done
done
# Verify the quorum
kubectl -n vault exec vault-0 -- vault operator raft list-peers
# 3 nodes expected, one of them is the leaderYou can now open the UI: kubectl -n vault port-forward svc/vault 8200:8200 then browse to https://localhost:8200 and sign in with the root token from vault-init.json.
Configuring Kubernetes authentication
The goal: let a pod (via its ServiceAccount) authenticate to Vault without a shared password.
# Enable the kubernetes method
vault auth enable kubernetes
# Configure Vault to validate tokens against the API server
TOKEN_REVIEW_JWT=$(kubectl -n vault create token vault
--duration=8760h --audience=https://kubernetes.default.svc)
vault write auth/kubernetes/config
token_reviewer_jwt="$TOKEN_REVIEW_JWT"
kubernetes_host="https://kubernetes.default.svc"
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
disable_iss_validation=true
# Enable a KV engine for the "demo" application
vault secrets enable -path=secret kv-v2
vault kv put secret/demo/config api_token="changeme123"
# Policy granting read access to secret/demo/*
vault policy write demo-read - <<EOF
path "secret/data/demo/*" {
capabilities = ["read"]
}
EOF
# Bind ServiceAccount → policy
vault write auth/kubernetes/role/demo
bound_service_account_names=demo
bound_service_account_namespaces=demo
policies=demo-read
ttl=24hInjecting secrets into a pod (Vault Agent Injector)
# demo-app.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: demo
namespace: demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
namespace: demo
spec:
replicas: 1
selector:
matchLabels: { app: demo }
template:
metadata:
labels: { app: demo }
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "demo"
vault.hashicorp.com/agent-inject-secret-config.env: "secret/data/demo/config"
vault.hashicorp.com/agent-inject-template-config.env: |
{{- with secret "secret/data/demo/config" -}}
API_TOKEN={{ .Data.data.api_token }}
{{- end -}}
spec:
serviceAccountName: demo
containers:
- name: app
image: ghcr.io/example/demo:1.0
command: ["/bin/sh", "-c", "source /vault/secrets/config.env && exec /app"]The injector starts a vault-agent sidecar, authenticates to Vault using the ServiceAccount token, retrieves the secret and materialises it in /vault/secrets/config.env. No secret variables in the manifest, no kubectl create secret to run.
Alternative: Secrets Store CSI Driver
The Secrets Store CSI Driver is more modern: it mounts secrets as volumes (and optionally syncs them as Kubernetes Secrets). No more sidecar, but a dependency on the CSI Driver and the Vault Provider.
helm repo add secrets-store-csi-driver
https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi -n kube-system secrets-store-csi-driver/secrets-store-csi-driver
--set syncSecret.enabled=true
helm install vault hashicorp/vault
-n vault --reuse-values
--set csi.enabled=trueThe CSI Driver shines when applications expect a mounted file (TLS cert, kubeconfig, Java keystore) rather than an environment variable.
Backup and restore
Vault Raft offers a native snapshot mechanism. Best practice: a daily cron pushing the snapshot to S3.
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-snapshot
namespace: vault
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
serviceAccountName: vault-snapshot
containers:
- name: snapshot
image: hashicorp/vault:1.18.3
env:
- { name: VAULT_ADDR, value: https://vault.vault.svc:8200 }
- { name: VAULT_CACERT, value: /tls/ca.crt }
- name: VAULT_TOKEN
valueFrom: { secretKeyRef: { name: vault-snapshot-token, key: token } }
command:
- /bin/sh
- -c
- |
vault operator raft snapshot save /tmp/snap.db
aws s3 cp /tmp/snap.db s3://my-bucket/vault/$(date +%Y%m%d).db
volumeMounts:
- { name: tls, mountPath: /tls, readOnly: true }
volumes:
- { name: tls, secret: { secretName: vault-tls } }To restore, do the reverse:
kubectl -n vault cp snap.db vault-0:/tmp/snap.db
kubectl -n vault exec vault-0 -- vault operator raft snapshot restore /tmp/snap.dbTest your restores at least once a quarter on a pre-production cluster. An untested backup is not a backup.
What about OpenBao, the open source alternative?
Since Vault switched to the Business Source License (BSL) in 2023, the Linux Foundation forked the project under the name OpenBao. The 1.0 release shipped in 2024 and the project is essentially compatible 1-to-1 with Vault.
- The Helm chart is
openbao/openbao. - The CLI commands are identical:
baois an alias ofvault. - Policy format, auth methods and secrets engines are the same.
OpenBao is an excellent option if you want to stay 100% open source or avoid the BSL. It is the path I now recommend for most new installs at customers without an existing HashiCorp subscription.
FAQ
Do I really need 3 Vault replicas?
For Raft quorum, yes. With 1 replica, Vault is unavailable as soon as the pod restarts. With 3, you can lose a pod or a node without interruption.
Vault Enterprise or Vault Community?
The Community version covers most needs (auth methods, KV, PKI, audit). Vault Enterprise adds multi-datacenter replication (DR + Performance), namespaces and HSM support. For a single site, Community is enough.
How do I audit access to secrets?
Enable a Vault audit device, e.g. vault audit enable file file_path=/vault/audit/log or vault audit enable socket address=splunk:9000 socket_type=tcp. Every API call is recorded (with hashed values).
How long does it take to ship Vault to production?
From experience, plan for 2 to 3 days for a clean HA deployment (TLS, auto-unseal, monitoring, backup), then 1 to 2 weeks to onboard the first applications. Policy governance and team enablement often take more time than the install itself.
Need help deploying Vault on your side?
HA architecture, Kubernetes integration, internal PKI, team enablement — this is one of my main areas of expertise. Tell me about your context and I’ll come back with a clear plan within 24 hours.
Going further: Kubernetes vs OpenShift, what’s the difference in 2026?