This project deploys HashiCorp Vault on Kubernetes using Helm in a production-like setup.
1. Project Structure
Create the project directory:
mkdir vault-k8s-helm-project
cd vault-k8s-helm-project
mkdir -p helm/vault
mkdir -p k8s/app
mkdir -p scripts
Final structure:
vault/
├── helm/
│ └── vault/
│ └── values.yaml
├── k8s/
│ └── app/
│ ├── namespace.yaml
│ ├── service-account.yaml
│ └── deployment.yaml
└── scripts/
├── 01-install-vault.sh
├── 02-init-vault.sh
├── 03-unseal-vault.sh
├── 04-enable-k8s-auth.sh
├── 05-create-policy-and-role.sh
├── 06-create-secret.sh
├── 07-deploy-app.sh
2. Add HashiCorp Helm Repository
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Check the chart:
helm search repo hashicorp/vault
3. Create Vault Namespace
kubectl create namespace vault
4. Vault Helm Values
Create the values file:
vi helm/vault/values.yaml
Add:
global:
enabled: true
tlsDisable: true
injector:
enabled: true
replicas: 1
server:
enabled: true
image:
repository: hashicorp/vault
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 1000m
dataStorage:
enabled: true
size: 10Gi
mountPath: /vault/data
storageClass: null
accessMode: ReadWriteOnce
standalone:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
}
storage "file" {
path = "/vault/data"
}
ha:
enabled: false
service:
enabled: true
type: ClusterIP
logLevel: "info"
ui:
enabled: true
serviceType: ClusterIP
5. Install Vault with Helm
Create the install script:
vi scripts/01-install-vault.sh
Add:
#!/bin/bash
set -e
helm upgrade --install vault hashicorp/vault \
--namespace vault \
-f helm/vault/values.yaml
kubectl -n vault get pods
kubectl -n vault get svc
Run:
chmod +x scripts/01-install-vault.sh
./scripts/01-install-vault.sh
Check pods:
kubectl -n vault get pods
Expected:
vault-0 0/1 Running
vault-agent-injector-xxxxx 1/1 Running
Vault pods will show 0/1 because Vault is not initialized and unsealed yet.
6. Initialize Vault
Create the init script:
vi scripts/02-init-vault.sh
Add:
#!/bin/bash
set -e
kubectl -n vault exec vault-0 -- vault operator init \
-key-shares=5 \
-key-threshold=3 \
-format=json > vault-init.json
echo "Vault initialized."
echo "IMPORTANT: Store vault-init.json securely."
cat vault-init.json
Run:
chmod +x scripts/02-init-vault.sh
./scripts/02-init-vault.sh
You will get:
{
"unseal_keys_b64": [
"...",
"...",
"...",
"...",
"..."
],
"root_token": "..."
}
Secure the file:
chmod 600 vault-init.json
Do not commit this file.
Create .gitignore:
vi .gitignore
Add:
vault-init.json
*.token
*.snap
7. Unseal Vault
Create the unseal script:
vi scripts/03-unseal-vault.sh
Add:
#!/bin/bash
set -e
KEY1=$(jq -r '.unseal_keys_b64[0]' vault-init.json)
KEY2=$(jq -r '.unseal_keys_b64[1]' vault-init.json)
KEY3=$(jq -r '.unseal_keys_b64[2]' vault-init.json)
echo "Unsealing vault-0..."
kubectl -n vault exec vault-0 -- vault operator unseal "$KEY1"
kubectl -n vault exec vault-0 -- vault operator unseal "$KEY2"
kubectl -n vault exec vault-0 -- vault operator unseal "$KEY3"
echo ""
echo "Vault status:"
kubectl -n vault exec vault-0 -- vault status
Run:
chmod +x scripts/03-unseal-vault.sh
./scripts/03-unseal-vault.sh
Check status:
kubectl -n vault exec vault-0 -- vault status
Expected:
Initialized true
Sealed false
8. Login to Vault
Export the root token locally:
export VAULT_ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')
Login inside the Vault pod:
kubectl -n vault exec -it vault-0 -- sh
Inside the pod:
export VAULT_ADDR=http://127.0.0.1:8200
vault login
Paste the root token.
Or run directly:
kubectl -n vault exec vault-0 -- vault login $VAULT_ROOT_TOKEN
9. Enable Vault UI Locally
Port-forward Vault UI:
kubectl -n vault port-forward svc/vault-ui 8200:8200
Open:
http://localhost:8200
Login with the root token.
10. Enable KV Secrets Engine and Create Secret
Create the secret script:
vi scripts/06-create-secret.sh
Add:
#!/bin/bash
set -e
ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')
kubectl -n vault exec vault-0 -- sh -c "
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=$ROOT_TOKEN
vault secrets enable -path=secret kv-v2 || true
# it enables the KV (Key-Value) version 2 secrets engine and makes it accessible at the path /secret
vault secrets list
vault kv put secret/myapp/config \
DB_HOST=postgres.default.svc.cluster.local \
DB_PORT=5432 \
DB_USER=myapp_user \
DB_PASSWORD=super-secret-password \
JWT_SECRET=my-jwt-secret
vault kv get secret/myapp/config
"
Run:
chmod +x scripts/06-create-secret.sh
./scripts/06-create-secret.sh
11. Enable Kubernetes Auth
Create the Kubernetes auth script:
vi scripts/04-enable-k8s-auth.sh
Add:
#!/bin/bash
set -e
ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')
kubectl -n vault exec vault-0 -- sh -c "
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=$ROOT_TOKEN
vault auth enable kubernetes || true
# environment variable automatically injected into every pod by Kubernetes
# kubectl -n vault exec vault-0 -- env | grep KUBERNETES
vault write auth/kubernetes/config \
kubernetes_host=https://\$KUBERNETES_PORT_443_TCP_ADDR:443
# When pods try to authenticate using the Kubernetes auth method, here's the address of the Kubernetes API server you should use to verify their identity
"
Run:
chmod +x scripts/04-enable-k8s-auth.sh
./scripts/04-enable-k8s-auth.sh
12. Create Vault Policy and Kubernetes Role
Create the policy and role script:
vi scripts/05-create-policy-and-role.sh
Add:
#!/bin/bash
set -e
ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')
kubectl -n vault exec vault-0 -- sh -c "
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=$ROOT_TOKEN
vault policy write myapp-policy - <<EOF_POLICY
path \"secret/data/myapp/config\" {
capabilities = [\"read\"]
}
EOF_POLICY
# myapp-policy grants permission to read the secret at secret/data/myapp/config
vault policy list
# Output: default, myapp-policy, root
vault policy read myapp-policy
# Shows the policy content back
vault write auth/kubernetes/role/myapp-role \
bound_service_account_names=myapp-sa \
bound_service_account_namespaces=myapp \
policies=myapp-policy \
ttl=24h
vault read auth/kubernetes/role/myapp-role
# Displays the configuration of the role you just created
"
Run:
chmod +x scripts/05-create-policy-and-role.sh
./scripts/05-create-policy-and-role.sh
Meaning:
Only pods using service account myapp-sa in namespace myapp can read secret/myapp/config.
13. Create Example Application Namespace
Create namespace manifest:
vi k8s/app/namespace.yaml
Add:
apiVersion: v1
kind: Namespace
metadata:
name: myapp
Create service account manifest:
vi k8s/app/service-account.yaml
Add:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-sa
namespace: myapp
14. Example Application Using Vault Agent Injector
Create deployment manifest:
vi k8s/app/deployment.yaml
Add:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
vault.hashicorp.com/agent-inject: "true" #inject Vault Agent container into this pod
vault.hashicorp.com/role: "myapp-role"
vault.hashicorp.com/agent-inject-secret-config.txt: "secret/data/myapp/config"
# Tells the Vault Agent: "Fetch the secret at secret/data/myapp/config, and write the result to the file /vault/secrets/config.txt inside the pod."
# The injector uses a naming convention: the suffix after agent-inject-template- becomes the filename created in /vault/secrets/
vault.hashicorp.com/agent-inject-template-config.txt: |
{{- with secret "secret/data/myapp/config" -}}
DB_HOST={{ .Data.data.DB_HOST }}
DB_PORT={{ .Data.data.DB_PORT }}
DB_USER={{ .Data.data.DB_USER }}
DB_PASSWORD={{ .Data.data.DB_PASSWORD }}
JWT_SECRET={{ .Data.data.JWT_SECRET }}
{{- end }}
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: busybox:1.36
command:
- sh
- -c
- |
echo "Starting app..."
echo "Reading Vault secret file:"
cat /vault/secrets/config.txt
sleep 3600
15. Deploy Example App
Create deploy script:
vi scripts/07-deploy-app.sh
Add:
#!/bin/bash
set -e
kubectl apply -f k8s/app/namespace.yaml
kubectl apply -f k8s/app/service-account.yaml
kubectl apply -f k8s/app/deployment.yaml
kubectl -n myapp get pods
Run:
chmod +x scripts/07-deploy-app.sh
./scripts/07-deploy-app.sh
Check pod:
kubectl -n myapp get pods
Expected:
myapp-xxxxx 2/2 Running
Why 2/2?
Because Vault Agent sidecar was injected.
Check app logs:
kubectl -n myapp logs deploy/myapp -c myapp
Expected:
Starting app...
Reading Vault secret file:
DB_HOST=postgres.default.svc.cluster.local
DB_PORT=5432
DB_USER=myapp_user
DB_PASSWORD=super-secret-password
JWT_SECRET=my-jwt-secret
Check injected containers:
kubectl -n myapp describe pod <pod-name>
You should see:
vault-agent-init
vault-agent
myapp
16. Test
Create another service account:
kubectl -n myapp create serviceaccount wrong-sa
Change deployment:
serviceAccountName: wrong-sa
Apply:
kubectl apply -f k8s/app/deployment.yaml
Vault Agent should fail to authenticate because the Vault role only allows:
service account: myapp-sa
namespace: myapp
kubectl get pod -n myapp
Top comments (0)