DEV Community

Cover image for vault project
Omar Ahmed
Omar Ahmed

Posted on Edited on

vault project

Read this first

Vault on Kubernetes

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
Enter fullscreen mode Exit fullscreen mode

Final structure:

vault/
├── helm/
│   └── vault/
│       └── values.yaml
├── k8s/
│   └── app/
│       ├── namespace.yaml
│       ├── service-account.yaml
│       └── deployment.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add HashiCorp Helm Repository

helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Enter fullscreen mode Exit fullscreen mode

Check the chart:

helm search repo hashicorp/vault
Enter fullscreen mode Exit fullscreen mode

3. Create Vault Namespace

kubectl create namespace vault
Enter fullscreen mode Exit fullscreen mode

4. Vault Helm Values

Create the values file:

cat > helm/vault/values.yaml <<'YAML'
server:
  dev:
    enabled: false
  standalone:
    enabled: true
    config: |
      ui = true
      listener "tcp" {
        address         = "[::]:8200"
        cluster_address = "[::]:8201"
        tls_disable     = 1
      }
      storage "raft" {
        path = "/vault/data"
      }
  dataStorage:
    enabled: true
    size: 1Gi

ui:
  enabled: true
  serviceType: ClusterIP
YAML
Enter fullscreen mode Exit fullscreen mode

5. Install Vault with Helm


helm upgrade --install vault hashicorp/vault \
  --namespace vault \
  -f helm/vault/values.yaml

kubectl -n vault get pods
kubectl -n vault get svc
Enter fullscreen mode Exit fullscreen mode

Expected:

vault-0                         0/1     Running
vault-agent-injector-xxxxx      1/1     Running
Enter fullscreen mode Exit fullscreen mode

Vault pods will show 0/1 because Vault is not initialized and unsealed yet.


6. Initialize Vault

kubectl -n vault exec vault-0 -- vault operator init \
  -key-shares=5 \
  -key-threshold=3 \
  -format=json > vault-init.json

# Initializes a fresh Vault server for the first time, 

cat vault-init.json
Enter fullscreen mode Exit fullscreen mode

You will get:

{
  "unseal_keys_b64": [
    "...",
    "...",
    "...",
    "...",
    "..."
  ],
  "root_token": "..."
}
Enter fullscreen mode Exit fullscreen mode

7. Unseal Vault

#!/bin/bash

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"

kubectl -n vault exec vault-0 -- vault status
Enter fullscreen mode Exit fullscreen mode

Expected:

Initialized     true
Sealed          false
Enter fullscreen mode Exit fullscreen mode

8. Login to Vault

Export the root token locally:

VAULT_ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')
echo $VAULT_ROOT_TOKEN
kubectl -n vault exec vault-0 -- vault login "$VAULT_ROOT_TOKEN"
# Authenticates you to Vault using a token
Enter fullscreen mode Exit fullscreen mode

9. Enable Vault UI Locally

Port-forward Vault UI:

kubectl -n vault port-forward svc/vault-ui 8200:8200
Enter fullscreen mode Exit fullscreen mode

Open:

http://localhost:8200
Enter fullscreen mode Exit fullscreen mode

Login with the root token.


10. Enable KV Secrets Engine and Create Secret


ROOT_TOKEN=$(cat vault-init.json | jq -r '.root_token')

kubectl -n vault exec vault-0 -- sh -c "
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

echo '##Secrets List'
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

echo '##Get Secrets'
vault kv get secret/myapp/config
"
Enter fullscreen mode Exit fullscreen mode

11. Authenticate with Kubernetes

kubectl create ns myapps

# JWT Token
kubectl create sa vault-reviewer -n myapps
TOKEN_REVIEW_JWT=$(kubectl -n myapps create token vault-reviewer --duration=24h)

cat > vault-reviewer-binding.yaml <<'YAML'
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: vault-reviewer-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:auth-delegator
subjects:
- kind: ServiceAccount
  name: vault-reviewer
  namespace: myapps
YAML

kubectl apply -f vault-reviewer-binding.yaml

# Get Kubernetes API Info
KUBE_HOST=$(kubectl config view --raw -o=jsonpath='{.clusters[0].cluster.server}')

# Kubernetes CA Cert
KUBE_CA_CERT=$(kubectl config view --raw \
  -o=jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d)

kubectl exec -n vault -it vault-0 -- vault auth enable kubernetes

kubectl exec -n vault -it vault-0 -- vault write auth/kubernetes/config \
  token_reviewer_jwt="$TOKEN_REVIEW_JWT" \
  kubernetes_host="$KUBE_HOST" \
  kubernetes_ca_cert="$KUBE_CA_CERT"
Enter fullscreen mode Exit fullscreen mode

12. Create Vault Policy and Kubernetes Role

Create the policy and role script:

kubectl create ns myapp
kubectl create sa myapp -n myapp

kubectl -n vault exec vault-0 -- sh -c "
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

echo '## List Policies'
vault policy list

echo '## Read Specific Policy'
vault policy read myapp-policy
# Shows the policy content back

vault write auth/kubernetes/role/myapp-role \
  bound_service_account_names=myapp \
  bound_service_account_namespaces=myapp \
  policies=myapp-policy \
  ttl=24h

echo '## Read Role'
vault read auth/kubernetes/role/myapp-role
# Displays the configuration of the role you just created
"
Enter fullscreen mode Exit fullscreen mode

Meaning:

Only pods using service account myapp-sa in namespace myapp can read secret/myapp/config.
Enter fullscreen mode Exit fullscreen mode

13. Example Application Using Vault Agent Injector

Create deployment manifest:

touch k8s/app/deployment.yaml
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

14. Deploy Example App

kubectl apply -f k8s/app/deployment.yaml
kubectl -n myapp get pods
Enter fullscreen mode Exit fullscreen mode

Expected:

NAME                     READY   STATUS     RESTARTS   AGE
myapp-59b648f545-t97kb   0/2     Init:0/1   0          2m44s

## Then 
myapp-59b648f545-tf48g   2/2     Running           0          13s
Enter fullscreen mode Exit fullscreen mode

Why 2/2?

Because Vault Agent sidecar was injected.

Check app logs:

kubectl -n myapp logs deploy/myapp -c myapp
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check injected containers:

kubectl -n myapp describe pod <pod-name>
Enter fullscreen mode Exit fullscreen mode

You should see:

vault-agent-init   (init container)
vault-agent        (sidecar container)
myapp
Enter fullscreen mode Exit fullscreen mode

🧩 vault-agent-init (Init Container)

Fetch the secret ONCE before your app starts

  1. Pod starts
  2. vault-agent-init runs FIRST
  3. Authenticates to Vault (using ServiceAccount JWT)
  4. Reads secret from Vault
  5. Writes file → /vault/secrets/config.txt
  6. Exits

🧩 vault-agent (Sidecar Container)

Keep the secret UPDATED while the app is running

  1. Runs alongside your app
  2. Authenticates to Vault
  3. Watches the secret
  4. If secret changes → rewrites file
  5. Renews Vault token automatically

15. Test

Create another service account:

kubectl -n myapp create serviceaccount wrong-sa
Enter fullscreen mode Exit fullscreen mode

Change deployment:

serviceAccountName: wrong-sa
Enter fullscreen mode Exit fullscreen mode

Apply:

kubectl apply -f k8s/app/deployment.yaml
Enter fullscreen mode Exit fullscreen mode

Vault Agent should fail to authenticate because the Vault role only allows:

service account: myapp-sa
namespace: myapp
Enter fullscreen mode Exit fullscreen mode
kubectl get pod -n myapp
Enter fullscreen mode Exit fullscreen mode

Top comments (0)