{"id":122489,"date":"2026-03-19T12:00:00","date_gmt":"2026-03-19T09:00:00","guid":{"rendered":"https:\/\/computingforgeeks.com\/?p=122489"},"modified":"2026-03-24T11:56:00","modified_gmt":"2026-03-24T08:56:00","slug":"deploy-hashicorp-vault-kubernetes","status":"publish","type":"post","link":"https:\/\/computingforgeeks.com\/deploy-hashicorp-vault-kubernetes\/","title":{"rendered":"Deploy HashiCorp Vault for Secrets Management in Kubernetes"},"content":{"rendered":"\n<p>HashiCorp Vault is a secrets management tool that centralizes the storage, access control, and encryption of sensitive data such as API keys, passwords, certificates, and tokens. Native Kubernetes secrets are base64-encoded (not encrypted) and lack fine-grained access policies, audit logging, and automatic rotation. Vault solves all of these problems with a single platform that integrates directly into Kubernetes workloads.<\/p>\n\n\n\n<p>This guide walks through deploying HashiCorp Vault on Kubernetes using the official Helm chart, initializing and unsealing the cluster, enabling Kubernetes authentication, injecting secrets into pods with the Vault Agent Sidecar Injector, using the Vault CSI Provider, configuring secret engines (KV, database, PKI), setting up high availability with the Raft storage backend, configuring auto-unseal with cloud KMS, monitoring Vault, and performing backup and restore operations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p>Before starting, ensure you have the following in place:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A running Kubernetes cluster (v1.26+) with at least 3 worker nodes and 4 GB RAM each. You can set one up using <a href=\"https:\/\/computingforgeeks.com\/install-kubernetes-cluster-ubuntu-jammy\/\" target=\"_blank\" rel=\"noreferrer noopener\">kubeadm on Ubuntu<\/a> or any managed Kubernetes service (EKS, GKE, AKS).<\/li>\n\n\n\n<li><code>kubectl<\/code> installed and configured with cluster access<\/li>\n\n\n\n<li>Helm v3 installed on your workstation<\/li>\n\n\n\n<li>A default StorageClass configured in the cluster (required for persistent volumes)<\/li>\n\n\n\n<li>Ports 8200 (API) and 8201 (cluster) open between Vault pods<\/li>\n\n\n\n<li>Root or sudo access to the machine running kubectl<\/li>\n<\/ul>\n\n\n\n<p>Install kubectl if it is not already present on your system.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -LO \"https:\/\/dl.k8s.io\/release\/$(curl -Ls https:\/\/dl.k8s.io\/release\/stable.txt)\/bin\/linux\/amd64\/kubectl\"\nchmod +x kubectl\nsudo mv kubectl \/usr\/local\/bin\/<\/code><\/pre>\n\n\n\n<p>Install Helm 3.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -fsSL https:\/\/raw.githubusercontent.com\/helm\/helm\/main\/scripts\/get-helm-3 | bash<\/code><\/pre>\n\n\n\n<p>Verify both tools are working.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl version --client\nClient Version: v1.31.4\n\n$ helm version --short\nv3.16.4+gdb969a8<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Install HashiCorp Vault on Kubernetes with Helm<\/h2>\n\n\n\n<p>The official HashiCorp Vault Helm chart is the recommended way to deploy Vault on Kubernetes. It handles the StatefulSet, services, service accounts, and optional components like the Agent Injector and CSI Provider.<\/p>\n\n\n\n<p>Add the HashiCorp Helm repository and create the vault namespace.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm repo add hashicorp https:\/\/helm.releases.hashicorp.com\nhelm repo update\nkubectl create namespace vault<\/code><\/pre>\n\n\n\n<p>For a basic single-server deployment, install with default values.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm install vault hashicorp\/vault \\\n  --namespace vault \\\n  --set server.dataStorage.size=10Gi<\/code><\/pre>\n\n\n\n<p>Check the pod status. The vault-0 pod will show 0\/1 READY because it has not been initialized or unsealed yet.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl get pods -n vault\nNAME                                   READY   STATUS    RESTARTS   AGE\nvault-0                                0\/1     Running   0          45s\nvault-agent-injector-6f8d4b5c9-k7xfp   1\/1     Running   0          45s<\/code><\/pre>\n\n\n\n<p>Verify the services created by the Helm chart.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl get svc -n vault\nNAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE\nvault                      ClusterIP   10.96.45.120    &lt;none&gt;        8200\/TCP,8201\/TCP   50s\nvault-agent-injector-svc   ClusterIP   10.96.120.88    &lt;none&gt;        443\/TCP             50s\nvault-internal             ClusterIP   None            &lt;none&gt;        8200\/TCP,8201\/TCP   50s<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Initialize and Unseal Vault<\/h2>\n\n\n\n<p>Vault starts in a sealed state and must be initialized before it can store or retrieve any secrets. Initialization generates the master key shares (Shamir&#8217;s secret sharing) and the initial root token.<\/p>\n\n\n\n<p>Initialize Vault with 5 key shares and a threshold of 3 (any 3 of the 5 keys are required to unseal).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl exec -n vault vault-0 -- vault operator init \\\n  -key-shares=5 \\\n  -key-threshold=3 \\\n  -format=json > vault-init.json<\/code><\/pre>\n\n\n\n<p>The output file contains unseal keys and the root token. Store this file securely. Losing these keys means permanent loss of access to Vault data.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cat vault-init.json | jq -r '.unseal_keys_b64[]'\nvjwrDznfPk\/7kHWY8L4OQL4PwXSuYFo3z45lt5SHolxj\nmnBo0TJGqDI1Qld18gM4kg6b58GYjLzKMAWaSX9uVwEg\n6QWSei7R7re4sFlyz7os1TNpdxoJzpFOCvmhk09xIMWD\niGZm2RiEQK3\/\/RtUosUftb5dFU1R1YlqZmLQJJk7+I1I\ncnC9fyyxb4cBgKAKUbjTXT2R+y0CmyP\/Ve7AlNvKZbut<\/code><\/pre>\n\n\n\n<p>Unseal Vault by providing 3 of the 5 keys. Run this command three times, each time with a different unseal key.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UNSEAL_KEY_1=$(cat vault-init.json | jq -r '.unseal_keys_b64[0]')\nUNSEAL_KEY_2=$(cat vault-init.json | jq -r '.unseal_keys_b64[1]')\nUNSEAL_KEY_3=$(cat vault-init.json | jq -r '.unseal_keys_b64[2]')\n\nkubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY_1\nkubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY_2\nkubectl exec -n vault vault-0 -- vault operator unseal $UNSEAL_KEY_3<\/code><\/pre>\n\n\n\n<p>After the third unseal operation, check the status. The Sealed field should show false.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl exec -n vault vault-0 -- vault status\nKey             Value\n---             -----\nSeal Type       shamir\nInitialized     true\nSealed          false\nTotal Shares    5\nThreshold       3\nVersion         1.18.3\nBuild Date      2025-01-29T13:41:09Z\nStorage Type    file\nCluster Name    vault-cluster-a3b2c1d4\nCluster ID      be268c68-646d-e4bd-9acf-c20c2ace1a91\nHA Enabled      false<\/code><\/pre>\n\n\n\n<p>The vault-0 pod should now show 1\/1 READY.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Enable Kubernetes Authentication<\/h2>\n\n\n\n<p>Kubernetes authentication allows pods to authenticate to Vault using their Kubernetes service account tokens. This eliminates the need to distribute Vault tokens to individual pods manually.<\/p>\n\n\n\n<p>Export the root token and exec into the Vault pod.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')\nkubectl exec -n vault -it vault-0 -- \/bin\/sh<\/code><\/pre>\n\n\n\n<p>Inside the Vault pod, log in and enable the Kubernetes auth method.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault login $ROOT_TOKEN\nvault auth enable kubernetes<\/code><\/pre>\n\n\n\n<p>Configure the auth method to communicate with the Kubernetes API server.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault write auth\/kubernetes\/config \\\n  token_reviewer_jwt=\"$(cat \/var\/run\/secrets\/kubernetes.io\/serviceaccount\/token)\" \\\n  kubernetes_host=\"https:\/\/$KUBERNETES_PORT_443_TCP_ADDR:443\" \\\n  kubernetes_ca_cert=@\/var\/run\/secrets\/kubernetes.io\/serviceaccount\/ca.crt<\/code><\/pre>\n\n\n\n<p>Vault will now validate Kubernetes service account tokens by querying the Kubernetes TokenReview API. Every pod that needs to access Vault secrets will authenticate using its bound service account.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Configure Secret Engines<\/h2>\n\n\n\n<p>Vault supports multiple secret engines, each designed for a specific type of secret. The three most commonly used engines in Kubernetes environments are KV (key-value), Database, and PKI.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">KV Secret Engine (Version 2)<\/h3>\n\n\n\n<p>The KV v2 engine stores static secrets with versioning and soft-delete support. Enable it at a custom path and store a test secret. Run these commands inside the Vault pod shell.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault secrets enable -path=internal kv-v2<\/code><\/pre>\n\n\n\n<p>Store database credentials as a KV secret.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault kv put internal\/database\/config \\\n  username=\"app_db_user\" \\\n  password=\"S3cureP@ss2025\"<\/code><\/pre>\n\n\n\n<p>Verify the secret was stored correctly.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ vault kv get internal\/database\/config\n======== Secret Path ========\ninternal\/data\/database\/config\n\n======= Metadata =======\nKey                Value\n---                -----\ncreated_time       2025-03-15T10:22:31.456789Z\ncustom_metadata    &lt;nil&gt;\ndeletion_time      n\/a\ndestroyed          false\nversion            1\n\n====== Data ======\nKey         Value\n---         -----\npassword    S3cureP@ss2025\nusername    app_db_user<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Database Secret Engine<\/h3>\n\n\n\n<p>The database engine generates short-lived, on-demand database credentials. This is more secure than static credentials because each pod gets unique credentials that expire automatically. If you have a <a href=\"https:\/\/computingforgeeks.com\/install-and-configure-vault-server-linux\/\" target=\"_blank\" rel=\"noreferrer noopener\">standalone Vault server<\/a>, the same engine configuration applies.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault secrets enable database<\/code><\/pre>\n\n\n\n<p>Configure a PostgreSQL connection (replace the connection URL with your actual database address).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault write database\/config\/mydb \\\n  plugin_name=postgresql-database-plugin \\\n  allowed_roles=\"app-role\" \\\n  connection_url=\"postgresql:\/\/{{username}}:{{password}}@postgres.default.svc.cluster.local:5432\/appdb?sslmode=disable\" \\\n  username=\"vault_admin\" \\\n  password=\"VaultDBAdmin2025\"<\/code><\/pre>\n\n\n\n<p>Create a role that defines what credentials Vault generates.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault write database\/roles\/app-role \\\n  db_name=mydb \\\n  creation_statements=\"CREATE ROLE \\\"{{name}}\\\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \\\"{{name}}\\\";\" \\\n  default_ttl=\"1h\" \\\n  max_ttl=\"24h\"<\/code><\/pre>\n\n\n\n<p>Test generating dynamic credentials.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ vault read database\/creds\/app-role\nKey                Value\n---                -----\nlease_id           database\/creds\/app-role\/abc123def456\nlease_duration     1h\nusername           v-kube-app-role-xyz789\npassword           A1b2C3d4E5f6G7h8<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">PKI Secret Engine<\/h3>\n\n\n\n<p>The PKI engine generates X.509 certificates on demand. This is useful for mTLS between microservices and internal TLS termination.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault secrets enable pki\nvault secrets tune -max-lease-ttl=87600h pki<\/code><\/pre>\n\n\n\n<p>Generate a root CA certificate.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault write -field=certificate pki\/root\/generate\/internal \\\n  common_name=\"vault-ca.internal\" \\\n  issuer_name=\"root-ca\" \\\n  ttl=87600h > root_ca.crt<\/code><\/pre>\n\n\n\n<p>Configure a role for issuing certificates.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault write pki\/roles\/internal-certs \\\n  allowed_domains=\"svc.cluster.local\" \\\n  allow_subdomains=true \\\n  max_ttl=72h<\/code><\/pre>\n\n\n\n<p>Issue a test certificate.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ vault write pki\/issue\/internal-certs \\\n  common_name=\"myapp.default.svc.cluster.local\" \\\n  ttl=24h<\/code><\/pre>\n\n\n\n<p>The output includes the certificate, private key, and CA chain, all generated dynamically.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Inject Secrets into Pods with Vault Agent Sidecar<\/h2>\n\n\n\n<p>The Vault Agent Injector is a Kubernetes mutating admission webhook that automatically injects a Vault Agent sidecar into pods. The sidecar authenticates to Vault and writes secrets to a shared volume that the application container can read. For a deeper walkthrough, see the guide on <a href=\"https:\/\/computingforgeeks.com\/use-vault-agent-sidecar-to-inject-secrets-in-vault-to-kubernetes-pod\/\" target=\"_blank\" rel=\"noreferrer noopener\">using Vault Agent sidecar to inject secrets into Kubernetes pods<\/a>.<\/p>\n\n\n\n<p>First, create a Vault policy and role for the application. Run these inside the Vault pod shell.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault policy write app-policy - &lt;&lt;EOF\npath \"internal\/data\/database\/config\" {\n  capabilities = [\"read\"]\n}\nEOF\n\nvault write auth\/kubernetes\/role\/app-role \\\n  bound_service_account_names=app-sa \\\n  bound_service_account_namespaces=default \\\n  policies=app-policy \\\n  ttl=1h<\/code><\/pre>\n\n\n\n<p>Exit the Vault pod shell.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>exit<\/code><\/pre>\n\n\n\n<p>Create a service account for the application.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl create serviceaccount app-sa -n default<\/code><\/pre>\n\n\n\n<p>Deploy a sample application with the Vault Agent Injector annotations. Create the deployment manifest.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -n default -f - &lt;&lt;EOF\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: myapp\n  labels:\n    app: myapp\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: myapp\n  template:\n    metadata:\n      annotations:\n        vault.hashicorp.com\/agent-inject: \"true\"\n        vault.hashicorp.com\/role: \"app-role\"\n        vault.hashicorp.com\/agent-inject-secret-db-creds.txt: \"internal\/data\/database\/config\"\n        vault.hashicorp.com\/agent-inject-template-db-creds.txt: |\n          {{- with secret \"internal\/data\/database\/config\" -}}\n          DB_USER={{ .Data.data.username }}\n          DB_PASS={{ .Data.data.password }}\n          {{- end }}\n      labels:\n        app: myapp\n    spec:\n      serviceAccountName: app-sa\n      containers:\n        - name: myapp\n          image: nginx:alpine\n          ports:\n            - containerPort: 80\nEOF<\/code><\/pre>\n\n\n\n<p>The key annotations explained:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>vault.hashicorp.com\/agent-inject: \"true\"<\/code> &#8211; enables the sidecar injector<\/li>\n\n\n\n<li><code>vault.hashicorp.com\/role<\/code> &#8211; the Vault Kubernetes auth role to authenticate with<\/li>\n\n\n\n<li><code>vault.hashicorp.com\/agent-inject-secret-*<\/code> &#8211; the secret path in Vault<\/li>\n\n\n\n<li><code>vault.hashicorp.com\/agent-inject-template-*<\/code> &#8211; a Go template for formatting the secret file<\/li>\n<\/ul>\n\n\n\n<p>Wait for the pod to become ready. It should show 2\/2 containers (the app + the vault-agent sidecar).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl get pods -n default -l app=myapp\nNAME                     READY   STATUS    RESTARTS   AGE\nmyapp-7b9f4d5c6-x2m8k   2\/2     Running   0          30s<\/code><\/pre>\n\n\n\n<p>Verify the secrets were injected into the pod.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl exec myapp-7b9f4d5c6-x2m8k -c myapp -- cat \/vault\/secrets\/db-creds.txt\nDB_USER=app_db_user\nDB_PASS=S3cureP@ss2025<\/code><\/pre>\n\n\n\n<p>The secrets are available as a file inside the container at <code>\/vault\/secrets\/<\/code>. The Vault Agent sidecar handles token renewal and secret rotation automatically.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Use the Vault CSI Provider<\/h2>\n\n\n\n<p>The Vault CSI Provider is an alternative to the Agent Injector. It uses the Kubernetes Secrets Store CSI Driver to mount Vault secrets as volumes. This approach does not require a sidecar container in each pod.<\/p>\n\n\n\n<p>Install the Secrets Store CSI Driver first.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm repo add secrets-store-csi-driver https:\/\/kubernetes-sigs.github.io\/secrets-store-csi-driver\/charts\nhelm install csi-secrets-store secrets-store-csi-driver\/secrets-store-csi-driver \\\n  --namespace kube-system \\\n  --set syncSecret.enabled=true<\/code><\/pre>\n\n\n\n<p>Upgrade the Vault Helm release to enable the CSI provider.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm upgrade vault hashicorp\/vault \\\n  --namespace vault \\\n  --set csi.enabled=true \\\n  --set server.dataStorage.size=10Gi<\/code><\/pre>\n\n\n\n<p>Verify the CSI provider pod is running.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl get pods -n vault -l app.kubernetes.io\/name=vault-csi-provider\nNAME                              READY   STATUS    RESTARTS   AGE\nvault-csi-provider-7g5k2          2\/2     Running   0          60s<\/code><\/pre>\n\n\n\n<p>Create a SecretProviderClass that tells the CSI driver where to find secrets in Vault.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -f - &lt;&lt;EOF\napiVersion: secrets-store.csi.x-k8s.io\/v1\nkind: SecretProviderClass\nmetadata:\n  name: vault-db-creds\n  namespace: default\nspec:\n  provider: vault\n  parameters:\n    vaultAddress: \"http:\/\/vault.vault.svc.cluster.local:8200\"\n    roleName: \"app-role\"\n    objects: |\n      - objectName: \"db-username\"\n        secretPath: \"internal\/data\/database\/config\"\n        secretKey: \"username\"\n      - objectName: \"db-password\"\n        secretPath: \"internal\/data\/database\/config\"\n        secretKey: \"password\"\nEOF<\/code><\/pre>\n\n\n\n<p>Deploy an application using the CSI volume mount.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -f - &lt;&lt;EOF\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: myapp-csi\n  namespace: default\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: myapp-csi\n  template:\n    metadata:\n      labels:\n        app: myapp-csi\n    spec:\n      serviceAccountName: app-sa\n      containers:\n        - name: myapp\n          image: nginx:alpine\n          volumeMounts:\n            - name: secrets\n              mountPath: \"\/mnt\/secrets\"\n              readOnly: true\n      volumes:\n        - name: secrets\n          csi:\n            driver: secrets-store.csi.k8s.io\n            readOnly: true\n            volumeAttributes:\n              secretProviderClass: \"vault-db-creds\"\nEOF<\/code><\/pre>\n\n\n\n<p>Verify the secrets are mounted in the pod.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl exec deploy\/myapp-csi -- ls \/mnt\/secrets\/\ndb-password\ndb-username\n\n$ kubectl exec deploy\/myapp-csi -- cat \/mnt\/secrets\/db-username\napp_db_user<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: High Availability with Raft Storage Backend<\/h2>\n\n\n\n<p>For production workloads, a single Vault instance is not sufficient. The integrated Raft storage backend provides built-in high availability with leader election and data replication across multiple Vault nodes, with no external storage dependency like Consul required.<\/p>\n\n\n\n<p>Create a custom values file for the HA deployment.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat > vault-ha-values.yaml &lt;&lt;EOF\nserver:\n  ha:\n    enabled: true\n    replicas: 3\n    raft:\n      enabled: true\n      setNodeId: true\n      config: |\n        ui = true\n\n        listener \"tcp\" {\n          tls_disable     = 1\n          address         = \"[::]:8200\"\n          cluster_address = \"[::]:8201\"\n        }\n\n        storage \"raft\" {\n          path = \"\/vault\/data\"\n\n          retry_join {\n            leader_api_addr = \"http:\/\/vault-0.vault-internal:8200\"\n          }\n          retry_join {\n            leader_api_addr = \"http:\/\/vault-1.vault-internal:8200\"\n          }\n          retry_join {\n            leader_api_addr = \"http:\/\/vault-2.vault-internal:8200\"\n          }\n        }\n\n        service_registration \"kubernetes\" {}\n\n  dataStorage:\n    enabled: true\n    size: 10Gi\n\nui:\n  enabled: true\n  serviceType: ClusterIP\nEOF<\/code><\/pre>\n\n\n\n<p>Deploy the HA cluster (or upgrade an existing installation).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>helm upgrade --install vault hashicorp\/vault \\\n  --namespace vault \\\n  --values vault-ha-values.yaml<\/code><\/pre>\n\n\n\n<p>After deployment, initialize vault-0 as described in Step 2, then unseal it. The remaining nodes (vault-1, vault-2) automatically join the Raft cluster but each must be unsealed individually.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY_1\nkubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY_2\nkubectl exec -n vault vault-1 -- vault operator unseal $UNSEAL_KEY_3\n\nkubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY_1\nkubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY_2\nkubectl exec -n vault vault-2 -- vault operator unseal $UNSEAL_KEY_3<\/code><\/pre>\n\n\n\n<p>Verify all nodes are part of the Raft cluster.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl exec -n vault vault-0 -- vault operator raft list-peers\nNode       Address                        State       Voter\n----       -------                        -----       -----\nvault-0    vault-0.vault-internal:8201    leader      true\nvault-1    vault-1.vault-internal:8201    follower    true\nvault-2    vault-2.vault-internal:8201    follower    true<\/code><\/pre>\n\n\n\n<p>All three pods should be running and ready.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ kubectl get pods -n vault -l app.kubernetes.io\/name=vault\nNAME      READY   STATUS    RESTARTS   AGE\nvault-0   1\/1     Running   0          5m\nvault-1   1\/1     Running   0          5m\nvault-2   1\/1     Running   0          5m<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 8: Auto-Unseal with Cloud KMS<\/h2>\n\n\n\n<p>Manual unsealing is impractical in production. Auto-unseal delegates the unseal operation to a cloud KMS (Key Management Service) so that Vault pods can restart and unseal automatically without human intervention.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AWS KMS Auto-Unseal<\/h3>\n\n\n\n<p>Create a KMS key in AWS and note the key ID. Then update the Vault HA values to include the seal stanza.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server:\n  ha:\n    enabled: true\n    replicas: 3\n    raft:\n      enabled: true\n      setNodeId: true\n      config: |\n        ui = true\n\n        listener \"tcp\" {\n          tls_disable     = 1\n          address         = \"[::]:8200\"\n          cluster_address = \"[::]:8201\"\n        }\n\n        seal \"awskms\" {\n          region     = \"us-east-1\"\n          kms_key_id = \"arn:aws:kms:us-east-1:123456789012:key\/abcd1234-ab12-cd34-ef56-abcdef123456\"\n        }\n\n        storage \"raft\" {\n          path = \"\/vault\/data\"\n        }\n\n        service_registration \"kubernetes\" {}\n\n  extraEnvironmentVars:\n    AWS_ACCESS_KEY_ID: \"AKIAIOSFODNN7EXAMPLE\"\n    AWS_SECRET_ACCESS_KEY: \"wJalrXUtnFEMI\/K7MDENG\/bPxRfiCYEXAMPLEKEY\"<\/code><\/pre>\n\n\n\n<p>For better security, use IAM Roles for Service Accounts (IRSA) on EKS instead of embedding static credentials. The same auto-unseal concept works with GCP Cloud KMS and Azure Key Vault. Just replace the <code>seal<\/code> stanza with the appropriate provider block.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GCP Cloud KMS Auto-Unseal<\/h3>\n\n\n\n<p>For GCP, the seal configuration looks like this.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>seal \"gcpckms\" {\n  project     = \"my-gcp-project\"\n  region      = \"global\"\n  key_ring    = \"vault-keyring\"\n  crypto_key  = \"vault-unseal-key\"\n}<\/code><\/pre>\n\n\n\n<p>When auto-unseal is configured, Vault initialization produces a recovery key instead of unseal keys. The recovery key is used for certain administrative operations but is not needed for day-to-day unsealing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 9: Monitoring HashiCorp Vault on Kubernetes<\/h2>\n\n\n\n<p>Vault exposes Prometheus-compatible metrics at <code>\/v1\/sys\/metrics<\/code> when telemetry is enabled. Add the following to the Vault configuration inside the Helm values.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>telemetry {\n  prometheus_retention_time = \"30s\"\n  disable_hostname          = true\n}<\/code><\/pre>\n\n\n\n<p>Add Prometheus scrape annotations to the Vault pods in your Helm values.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>server:\n  annotations:\n    prometheus.io\/scrape: \"true\"\n    prometheus.io\/port: \"8200\"\n    prometheus.io\/path: \"\/v1\/sys\/metrics\"\n    prometheus.io\/param-format: \"prometheus\"<\/code><\/pre>\n\n\n\n<p>Create a Vault policy that allows the Prometheus service account to read metrics.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault policy write prometheus-metrics - &lt;&lt;EOF\npath \"sys\/metrics\" {\n  capabilities = [\"read\"]\n}\nEOF<\/code><\/pre>\n\n\n\n<p>Key metrics to monitor include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>vault.core.handle_request.count<\/code> &#8211; total number of requests handled<\/li>\n\n\n\n<li><code>vault.core.handle_request.duration<\/code> &#8211; request latency<\/li>\n\n\n\n<li><code>vault.expire.num_leases<\/code> &#8211; active lease count<\/li>\n\n\n\n<li><code>vault.runtime.alloc_bytes<\/code> &#8211; memory allocation<\/li>\n\n\n\n<li><code>vault.raft.leader.lastContact<\/code> &#8211; Raft cluster health (HA mode)<\/li>\n\n\n\n<li><code>vault.seal<\/code> &#8211; seal status (critical for alerting)<\/li>\n<\/ul>\n\n\n\n<p>Set up alerts for seal events, high request latency (above 500ms), and lease count spikes. A Grafana dashboard for Vault is available as dashboard ID 12904 from the Grafana community.<\/p>\n\n\n\n<p>Vault also provides built-in audit logging. Enable the file audit device to capture all API interactions.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>vault audit enable file file_path=\/vault\/audit\/vault-audit.log<\/code><\/pre>\n\n\n\n<p>In production, stream these logs to a centralized logging system using a sidecar or DaemonSet log collector.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 10: Backup and Restore Vault Data<\/h2>\n\n\n\n<p>Regular backups are essential for disaster recovery. With the Raft storage backend, Vault provides built-in snapshot commands.<\/p>\n\n\n\n<p>Create a Raft snapshot from the leader node.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl exec -n vault vault-0 -- vault operator raft snapshot save \/tmp\/vault-snapshot.snap<\/code><\/pre>\n\n\n\n<p>Copy the snapshot to your local machine.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl cp vault\/vault-0:\/tmp\/vault-snapshot.snap .\/vault-snapshot-$(date +%Y%m%d).snap<\/code><\/pre>\n\n\n\n<p>To restore from a snapshot, copy it back to the Vault pod and run the restore command.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl cp .\/vault-snapshot-20250315.snap vault\/vault-0:\/tmp\/vault-snapshot.snap\nkubectl exec -n vault vault-0 -- vault operator raft snapshot restore \/tmp\/vault-snapshot.snap<\/code><\/pre>\n\n\n\n<p>Automate backups using a CronJob that runs the snapshot command on a schedule.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl apply -n vault -f - &lt;&lt;EOF\napiVersion: batch\/v1\nkind: CronJob\nmetadata:\n  name: vault-backup\nspec:\n  schedule: \"0 2 * * *\"\n  jobTemplate:\n    spec:\n      template:\n        spec:\n          serviceAccountName: vault\n          containers:\n            - name: backup\n              image: hashicorp\/vault\n              command:\n                - \/bin\/sh\n                - -c\n                - |\n                  export VAULT_ADDR=http:\/\/vault.vault.svc.cluster.local:8200\n                  export VAULT_TOKEN=$(cat \/var\/run\/secrets\/vault-token\/token)\n                  vault operator raft snapshot save \/backup\/vault-$(date +%Y%m%d-%H%M%S).snap\n              volumeMounts:\n                - name: backup-storage\n                  mountPath: \/backup\n          volumes:\n            - name: backup-storage\n              persistentVolumeClaim:\n                claimName: vault-backup-pvc\n          restartPolicy: OnFailure\nEOF<\/code><\/pre>\n\n\n\n<p>Store snapshots in external object storage (S3, GCS, or Azure Blob) for offsite disaster recovery. Retain at least 7 daily snapshots and test restores quarterly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exposing the Vault UI<\/h2>\n\n\n\n<p>The Vault web UI provides a visual interface for managing secrets, policies, and auth methods. To access it outside the cluster, change the service type or use an Ingress resource.<\/p>\n\n\n\n<p>For quick testing, use port-forwarding.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>kubectl port-forward -n vault svc\/vault 8200:8200<\/code><\/pre>\n\n\n\n<p>Then open <code>http:\/\/localhost:8200<\/code> in your browser. Unseal the Vault using the unseal keys, then sign in with the root token.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"778\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-1024x778.png\" alt=\"HashiCorp Vault unseal screen on Kubernetes\" class=\"wp-image-122569\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-1024x778.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-300x228.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-768x584.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-696x529.png 696w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-1068x812.png 1068w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-552x420.png 552w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2-80x60.png 80w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-2.png 1276w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>After authentication, the Vault dashboard displays all configured secret engines, authentication methods, and policies.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"573\" src=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-1024x573.png\" alt=\"HashiCorp Vault dashboard showing secret engines on Kubernetes\" class=\"wp-image-122571\" title=\"\" srcset=\"https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-1024x573.png 1024w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-300x168.png 300w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-768x430.png 768w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-696x390.png 696w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-1068x598.png 1068w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4-750x420.png 750w, https:\/\/computingforgeeks.com\/wp-content\/uploads\/2022\/08\/HashiCorp-Vault-for-Secrets-Management-in-Kubernetes-4.png 1252w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>For production environments, expose the UI through a Kubernetes <a href=\"https:\/\/computingforgeeks.com\/install-configure-traefik-ingress-controller-on-k0s\/\" target=\"_blank\" rel=\"noreferrer noopener\">Ingress controller<\/a> with TLS termination rather than NodePort or LoadBalancer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>We deployed HashiCorp Vault on Kubernetes with Helm, initialized and unsealed the cluster, configured Kubernetes authentication, set up KV, database, and PKI secret engines, and injected secrets into pods using both the Vault Agent Sidecar and CSI Provider. The HA Raft deployment with auto-unseal ensures Vault remains available and self-healing in production.<\/p>\n\n\n\n<p>For production hardening, enable TLS on all Vault listeners, restrict root token usage (revoke it after initial setup and use identity-based auth), implement namespace isolation for multi-tenant clusters, and rotate encryption keys regularly. Integrate Vault audit logs with your SIEM and test backup restores on a regular schedule.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Related Guides<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/computingforgeeks.com\/how-to-integrate-multiple-kubernetes-clusters-to-vault-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">Integrate Multiple Kubernetes Clusters to Vault Server<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/computingforgeeks.com\/install-and-configure-vault-server-linux\/\" target=\"_blank\" rel=\"noreferrer noopener\">Install and Configure HashiCorp Vault Server on Linux<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/computingforgeeks.com\/install-vault-cluster-gke-via-helm-terraform-bitbucket-pipelines\/\" target=\"_blank\" rel=\"noreferrer noopener\">Install Vault Cluster in GKE via Helm, Terraform and BitBucket Pipelines<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/computingforgeeks.com\/gitleaks-audit-git-repos-for-secrets\/\" target=\"_blank\" rel=\"noreferrer noopener\">Gitleaks &#8211; How to Audit Git Repository for Secrets<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/computingforgeeks.com\/manage-google-cloud-service-accounts-with-vault-and-terraform\/\" target=\"_blank\" rel=\"noreferrer noopener\">Manage Google Cloud Service Accounts with Vault and Terraform<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>HashiCorp Vault is a secrets management tool that centralizes the storage, access control, and encryption of sensitive data such as API keys, passwords, certificates, and tokens. Native Kubernetes secrets are base64-encoded (not encrypted) and lack fine-grained access policies, audit logging, and automatic rotation. Vault solves all of these problems with a single platform that integrates &#8230; <a title=\"Deploy HashiCorp Vault for Secrets Management in Kubernetes\" class=\"read-more\" href=\"https:\/\/computingforgeeks.com\/deploy-hashicorp-vault-kubernetes\/\" aria-label=\"Read more about Deploy HashiCorp Vault for Secrets Management in Kubernetes\">Read more<\/a><\/p>\n","protected":false},"author":3,"featured_media":162937,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[316,299,317,75],"tags":[36276],"class_list":["post-122489","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-containers","category-how-to","category-kubernetes","category-security","tag-hashicorp-vault"],"_links":{"self":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/122489","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/comments?post=122489"}],"version-history":[{"count":2,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/122489\/revisions"}],"predecessor-version":[{"id":164102,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/posts\/122489\/revisions\/164102"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media\/162937"}],"wp:attachment":[{"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/media?parent=122489"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/categories?post=122489"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/computingforgeeks.com\/wp-json\/wp\/v2\/tags?post=122489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}