HashiCorp Vault on Kubernetes: complete installation guide for 2026

Install guide · HA · 2026

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.

Estimated reading time: 14 minutes — target audience: Kubernetes / DevOps administrators planning a production Vault rollout.

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 kubernetes auth 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 ReadWriteOnce PVCs.
  • 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 vault CLI (≥ 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 update

2. 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: ClusterIssuer
kubectl apply -f vault-tls.yaml
    kubectl -n vault get secret vault-tls   # NAME: vault-tls TYPE: kubernetes.io/tls

3. 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: ClusterIP

A few things to keep in mind:

  • AWS KMS auto-unseal: if you don’t have KMS, drop the seal stanza 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 Running

Vault 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 leader

You 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=24h

Injecting 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=true

The 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.db

Test 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: bao is an alias of vault.
  • 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?

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top