Running Your Own Personal HashiCorp Vault on Local Kubernetes: A Security Engineer's Guide

May 31, 2025 min

Introduction

Running HashiCorp Vault locally on Kubernetes is an excellent way to learn Vault’s capabilities, test credential management strategies, and develop secure applications without the complexity of a production deployment. This guide walks you through setting up a fully functional Vault instance on your local machine using Kubernetes, complete with auto-unseal, secret engines, and policy management.

Whether you’re evaluating Vault for your organization, developing security tools, or just want a secure local secret store, this setup provides a production-like environment for experimentation and development.

Prerequisites and Environment Setup

Required Tools

Before we begin, ensure you have the following tools installed:

# Check if tools are installed
docker --version          # Docker Desktop or Docker Engine
kubectl version --client  # Kubernetes CLI
helm version             # Helm 3.x
vault --version          # Vault CLI

# Install missing tools
# macOS
brew install kubectl helm vault
brew install --cask docker

# Linux
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
curl https://get.helm.sh/helm-v3.12.0-linux-amd64.tar.gz | tar xz
wget https://releases.hashicorp.com/vault/1.15.0/vault_1.15.0_linux_amd64.zip

# Windows (using Chocolatey)
choco install kubernetes-cli helm vault docker-desktop

Kubernetes Cluster Options

Choose one of these local Kubernetes options:

Option 1: Docker Desktop (Recommended for beginners)

# Enable Kubernetes in Docker Desktop
# Settings → Kubernetes → Enable Kubernetes
# Wait for cluster to be ready
kubectl cluster-info

Option 2: kind (Kubernetes in Docker)

# Install kind
brew install kind  # macOS
# or
GO111MODULE="on" go install sigs.k8s.io/[email protected]

# Create cluster
kind create cluster --name vault-cluster --config kind-config.yaml

Option 3: minikube

# Install minikube
brew install minikube  # macOS

# Start cluster with sufficient resources
minikube start --cpus=4 --memory=8192 --driver=docker

Cluster Configuration

For kind, use this configuration for better Vault performance:

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 8200
    protocol: TCP
  extraMounts:
  - hostPath: ./vault-data
    containerPath: /vault-data

Basic Vault Installation

# Add HashiCorp Helm repository
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

# Create namespace
kubectl create namespace vault

# Install Vault in dev mode (quick start)
helm install vault hashicorp/vault \
  --namespace vault \
  --set "server.dev.enabled=true" \
  --set "injector.enabled=false" \
  --set "ui.enabled=true" \
  --set "ui.serviceType=NodePort" \
  --set "ui.serviceNodePort=30000"

# Wait for pod to be ready
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault -n vault --timeout=120s

# Check status
kubectl get all -n vault

Method 2: Manual Kubernetes Manifests

For more control, use custom manifests:

# vault-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: vault
---
# vault-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: vault
  namespace: vault
spec:
  type: NodePort
  ports:
    - name: vault
      port: 8200
      targetPort: 8200
      nodePort: 30000
    - name: vault-internal
      port: 8201
      targetPort: 8201
  selector:
    app: vault
---
# vault-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vault
  namespace: vault
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vault
  template:
    metadata:
      labels:
        app: vault
    spec:
      containers:
      - name: vault
        image: hashicorp/vault:1.15.0
        ports:
        - containerPort: 8200
          name: vault
        - containerPort: 8201
          name: vault-internal
        env:
        - name: VAULT_DEV_ROOT_TOKEN_ID
          value: "root-token"
        - name: VAULT_DEV_LISTEN_ADDRESS
          value: "0.0.0.0:8200"
        - name: VAULT_ADDR
          value: "http://127.0.0.1:8200"
        volumeMounts:
        - name: vault-data
          mountPath: /vault/data
        securityContext:
          capabilities:
            add:
              - IPC_LOCK
      volumes:
      - name: vault-data
        emptyDir: {}

Apply the manifests:

kubectl apply -f vault-namespace.yaml
kubectl apply -f vault-service.yaml
kubectl apply -f vault-deployment.yaml

Accessing Vault

# Port forward for CLI access
kubectl port-forward -n vault svc/vault 8200:8200 &

# Export Vault address
export VAULT_ADDR='http://127.0.0.1:8200'

# For dev mode, use the root token
export VAULT_TOKEN='root-token'

# Verify connection
vault status

# Access UI at http://localhost:8200/ui

Production-Like Setup with Raft Storage

For a more realistic setup, let’s configure Vault with Raft storage and auto-unseal:

Step 1: Create ConfigMap for Vault Configuration

# vault-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: vault-config
  namespace: vault
data:
  vault.hcl: |
    ui = true
    
    listener "tcp" {
      address = "[::]:8200"
      cluster_address = "[::]:8201"
      tls_disable = 1
    }
    
    storage "raft" {
      path = "/vault/data"
      node_id = "vault-0"
    }
    
    seal "transit" {
      address = "http://vault-transit:8200"
      disable_renewal = "false"
      key_name = "autounseal"
      mount_path = "transit/"
      tls_skip_verify = "true"
    }
    
    service_registration "kubernetes" {
      namespace = "vault"
      pod_name = "vault-0"
    }
    
    api_addr = "http://vault-0.vault-internal:8200"
    cluster_addr = "https://vault-0.vault-internal:8201"

Step 2: Set Up Transit Vault for Auto-Unseal

# transit-vault.yaml
apiVersion: v1
kind: Service
metadata:
  name: vault-transit
  namespace: vault
spec:
  selector:
    app: vault-transit
  ports:
    - port: 8200
      targetPort: 8200
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vault-transit
  namespace: vault
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vault-transit
  template:
    metadata:
      labels:
        app: vault-transit
    spec:
      containers:
      - name: vault
        image: hashicorp/vault:1.15.0
        env:
        - name: VAULT_DEV_ROOT_TOKEN_ID
          value: "transit-root-token"
        - name: VAULT_DEV_LISTEN_ADDRESS
          value: "0.0.0.0:8200"
        ports:
        - containerPort: 8200
        command: ["vault", "server", "-dev", "-dev-root-token-id=transit-root-token"]

Step 3: StatefulSet for Vault with Raft

# vault-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
  name: vault-internal
  namespace: vault
spec:
  clusterIP: None
  selector:
    app: vault
  ports:
    - name: vault
      port: 8200
    - name: vault-internal
      port: 8201
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: vault
  namespace: vault
spec:
  serviceName: vault-internal
  replicas: 1
  selector:
    matchLabels:
      app: vault
  template:
    metadata:
      labels:
        app: vault
    spec:
      containers:
      - name: vault
        image: hashicorp/vault:1.15.0
        ports:
        - containerPort: 8200
          name: vault
        - containerPort: 8201
          name: vault-internal
        env:
        - name: VAULT_ADDR
          value: "http://127.0.0.1:8200"
        - name: VAULT_API_ADDR
          value: "http://vault-0.vault-internal:8200"
        - name: VAULT_CLUSTER_ADDR
          value: "https://vault-0.vault-internal:8201"
        - name: HOSTNAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        volumeMounts:
        - name: vault-data
          mountPath: /vault/data
        - name: vault-config
          mountPath: /vault/config
        securityContext:
          capabilities:
            add:
              - IPC_LOCK
        command: ["vault", "server", "-config=/vault/config/vault.hcl"]
      volumes:
      - name: vault-config
        configMap:
          name: vault-config
  volumeClaimTemplates:
  - metadata:
      name: vault-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi

Step 4: Initialize and Unseal

# Apply all configurations
kubectl apply -f transit-vault.yaml
kubectl apply -f vault-config.yaml
kubectl apply -f vault-statefulset.yaml

# Wait for pods
kubectl wait --for=condition=ready pod -l app=vault-transit -n vault --timeout=120s
kubectl wait --for=condition=ready pod vault-0 -n vault --timeout=120s

# Configure transit vault for auto-unseal
kubectl exec -n vault deployment/vault-transit -- vault login transit-root-token
kubectl exec -n vault deployment/vault-transit -- vault secrets enable transit
kubectl exec -n vault deployment/vault-transit -- vault write -f transit/keys/autounseal

# Initialize main vault
kubectl exec -n vault vault-0 -- vault operator init -recovery-shares=5 -recovery-threshold=3

# Save the root token and recovery keys securely!

Configuring Vault for Development

Enable Essential Secret Engines

# Key-Value secrets engine
vault secrets enable -version=2 kv
vault secrets enable -path=secret kv-v2

# AWS secrets engine
vault secrets enable aws
vault write aws/config/root \
  access_key=$AWS_ACCESS_KEY_ID \
  secret_key=$AWS_SECRET_ACCESS_KEY \
  region=us-east-1

# Database secrets engine
vault secrets enable database

# PKI secrets engine
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki

# Transit for encryption as a service
vault secrets enable transit

Configure Authentication Methods

# Kubernetes auth
vault auth enable kubernetes
vault write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc:443" \
  kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
  token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token

# AppRole auth
vault auth enable approle
vault policy write my-app - <<EOF
path "secret/data/my-app/*" {
  capabilities = ["read"]
}
EOF

vault write auth/approle/role/my-app \
  token_policies="my-app" \
  token_ttl=1h \
  token_max_ttl=4h

# Userpass for local testing
vault auth enable userpass
vault write auth/userpass/users/testuser \
  password=testpassword \
  policies=default

Create Development Policies

# Developer policy
vault policy write developer - <<EOF
# Read and write to secret/data/dev
path "secret/data/dev/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

# List secret/metadata
path "secret/metadata/*" {
  capabilities = ["list"]
}

# Create and manage AWS credentials
path "aws/creds/developer" {
  capabilities = ["read"]
}

# Encrypt/decrypt with transit
path "transit/encrypt/app-key" {
  capabilities = ["update"]
}

path "transit/decrypt/app-key" {
  capabilities = ["update"]
}
EOF

# Security engineer policy
vault policy write security-engineer - <<EOF
# Full access to secret engine
path "secret/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

# Manage AWS roles
path "aws/roles/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

# Manage database connections
path "database/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

# Audit log access
path "sys/audit" {
  capabilities = ["read", "list"]
}

# Policy management
path "sys/policies/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}
EOF

Integration Examples

Python Application with Vault

# vault_client.py
import hvac
import os
from kubernetes import client, config

class VaultClient:
    def __init__(self, vault_addr='http://localhost:8200'):
        self.vault_addr = vault_addr
        self.client = None
        self._authenticate()
    
    def _authenticate(self):
        """Authenticate based on environment"""
        # Try Kubernetes auth first
        if os.path.exists('/var/run/secrets/kubernetes.io/serviceaccount/token'):
            self._k8s_auth()
        # Fall back to token auth
        elif os.environ.get('VAULT_TOKEN'):
            self.client = hvac.Client(
                url=self.vault_addr,
                token=os.environ['VAULT_TOKEN']
            )
        else:
            raise Exception("No authentication method available")
    
    def _k8s_auth(self):
        """Authenticate using Kubernetes service account"""
        with open('/var/run/secrets/kubernetes.io/serviceaccount/token', 'r') as f:
            jwt = f.read()
        
        self.client = hvac.Client(url=self.vault_addr)
        self.client.auth.kubernetes.login(
            role='my-app',
            jwt=jwt
        )
    
    def get_secret(self, path):
        """Retrieve secret from KV v2"""
        response = self.client.secrets.kv.v2.read_secret_version(
            path=path,
            mount_point='secret'
        )
        return response['data']['data']
    
    def get_aws_creds(self, role='developer'):
        """Get temporary AWS credentials"""
        response = self.client.read(f'aws/creds/{role}')
        return {
            'access_key': response['data']['access_key'],
            'secret_key': response['data']['secret_key'],
            'session_token': response['data']['security_token']
        }
    
    def encrypt(self, plaintext, key_name='app-key'):
        """Encrypt data using Transit engine"""
        response = self.client.write(
            f'transit/encrypt/{key_name}',
            plaintext=plaintext
        )
        return response['data']['ciphertext']
    
    def decrypt(self, ciphertext, key_name='app-key'):
        """Decrypt data using Transit engine"""
        response = self.client.write(
            f'transit/decrypt/{key_name}',
            ciphertext=ciphertext
        )
        return response['data']['plaintext']

# Example usage
if __name__ == '__main__':
    vault = VaultClient()
    
    # Store a secret
    vault.client.secrets.kv.v2.create_or_update_secret(
        path='my-app/config',
        secret={'api_key': 'secret-value'},
        mount_point='secret'
    )
    
    # Retrieve the secret
    config = vault.get_secret('my-app/config')
    print(f"API Key: {config['api_key']}")
    
    # Get AWS credentials
    aws_creds = vault.get_aws_creds()
    print(f"AWS Access Key: {aws_creds['access_key']}")
    
    # Encrypt sensitive data
    encrypted = vault.encrypt('sensitive-data')
    print(f"Encrypted: {encrypted}")
    
    # Decrypt it back
    decrypted = vault.decrypt(encrypted)
    print(f"Decrypted: {decrypted}")

Kubernetes Pod with Vault Integration

# app-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-app
  namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "my-app"
        vault.hashicorp.com/agent-inject-secret-config: "secret/data/my-app/config"
        vault.hashicorp.com/agent-inject-template-config: |
          {{ with secret "secret/data/my-app/config" -}}
          export API_KEY="{{ .Data.data.api_key }}"
          export DB_PASSWORD="{{ .Data.data.db_password }}"
          {{- end }}
    spec:
      serviceAccountName: my-app
      containers:
      - name: app
        image: my-app:latest
        command: ['sh', '-c']
        args: ['source /vault/secrets/config && exec my-app']
        env:
        - name: VAULT_ADDR
          value: "http://vault.vault.svc:8200"

Shell Script Integration

#!/bin/bash
# vault-wrapper.sh - Wrapper script for using Vault credentials

# Setup
VAULT_ADDR="http://localhost:8200"
VAULT_TOKEN=${VAULT_TOKEN:-$(cat ~/.vault-token)}

# Function to get AWS credentials
get_aws_creds() {
    local role="${1:-developer}"
    vault read -format=json aws/creds/$role | jq -r '.data | 
        "export AWS_ACCESS_KEY_ID=\(.access_key)\n
         export AWS_SECRET_ACCESS_KEY=\(.secret_key)\n
         export AWS_SESSION_TOKEN=\(.security_token)"'
}

# Function to get secret
get_secret() {
    local path="$1"
    vault kv get -format=json secret/$path | jq -r '.data.data'
}

# Function to encrypt file
encrypt_file() {
    local file="$1"
    local key="${2:-app-key}"
    
    base64 < "$file" | vault write -format=json transit/encrypt/$key plaintext=- | \
        jq -r '.data.ciphertext' > "$file.enc"
}

# Function to decrypt file
decrypt_file() {
    local file="$1"
    local key="${2:-app-key}"
    
    vault write -format=json transit/decrypt/$key ciphertext=@"$file" | \
        jq -r '.data.plaintext' | base64 -d
}

# Example usage
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    # Get AWS credentials and export them
    eval $(get_aws_creds developer)
    
    # Get database password
    DB_PASSWORD=$(get_secret my-app/config | jq -r '.db_password')
    
    # Encrypt a file
    encrypt_file sensitive-data.txt
    
    # Run command with credentials
    aws s3 ls
fi

Backup and Restore

Automated Backup Script

#!/bin/bash
# vault-backup.sh

BACKUP_DIR="/tmp/vault-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/vault-backup-$TIMESTAMP.snap"

# Create backup directory
mkdir -p $BACKUP_DIR

# Take snapshot
kubectl exec -n vault vault-0 -- vault operator raft snapshot save /tmp/backup.snap
kubectl cp vault/vault-0:/tmp/backup.snap $BACKUP_FILE

# Encrypt backup with GPG
gpg --symmetric --cipher-algo AES256 $BACKUP_FILE

# Upload to S3 (optional)
aws s3 cp $BACKUP_FILE.gpg s3://my-vault-backups/

# Clean up old backups (keep last 7 days)
find $BACKUP_DIR -name "vault-backup-*.snap*" -mtime +7 -delete

echo "Backup completed: $BACKUP_FILE.gpg"

Restore Process

#!/bin/bash
# vault-restore.sh

BACKUP_FILE="$1"

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup-file>"
    exit 1
fi

# Decrypt if needed
if [[ $BACKUP_FILE == *.gpg ]]; then
    gpg --decrypt $BACKUP_FILE > /tmp/restore.snap
    BACKUP_FILE="/tmp/restore.snap"
fi

# Copy to pod
kubectl cp $BACKUP_FILE vault/vault-0:/tmp/restore.snap

# Restore snapshot
kubectl exec -n vault vault-0 -- vault operator raft snapshot restore /tmp/restore.snap

echo "Restore completed from: $BACKUP_FILE"

Monitoring and Observability

Prometheus Metrics

# vault-prometheus.yaml
apiVersion: v1
kind: Service
metadata:
  name: vault-metrics
  namespace: vault
  labels:
    app: vault
spec:
  selector:
    app: vault
  ports:
    - name: metrics
      port: 8200
      targetPort: 8200
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: vault
  namespace: vault
spec:
  selector:
    matchLabels:
      app: vault
  endpoints:
  - port: metrics
    path: /v1/sys/metrics
    params:
      format: ['prometheus']

Enable Telemetry in Vault

# Add to vault config
telemetry {
  prometheus_retention_time = "30s"
  disable_hostname = true
}

Grafana Dashboard

{
  "dashboard": {
    "title": "Vault Metrics",
    "panels": [
      {
        "title": "Seal Status",
        "targets": [
          {
            "expr": "vault_core_unsealed"
          }
        ]
      },
      {
        "title": "Active Requests",
        "targets": [
          {
            "expr": "vault_route_request_duration_seconds_count"
          }
        ]
      },
      {
        "title": "Token Count",
        "targets": [
          {
            "expr": "vault_token_count"
          }
        ]
      }
    ]
  }
}

Security Hardening

Network Policies

# vault-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: vault-network-policy
  namespace: vault
spec:
  podSelector:
    matchLabels:
      app: vault
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: default
    - namespaceSelector:
        matchLabels:
          name: vault
    ports:
    - protocol: TCP
      port: 8200
    - protocol: TCP
      port: 8201
  egress:
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 443
    - protocol: TCP
      port: 8200

TLS Configuration

# Generate self-signed certificates
openssl req -x509 -newkey rsa:4096 -keyout vault-key.pem -out vault-cert.pem -days 365 -nodes \
  -subj "/CN=vault.vault.svc.cluster.local" \
  -addext "subjectAltName=DNS:vault,DNS:vault.vault.svc,DNS:vault.vault.svc.cluster.local,DNS:localhost,IP:127.0.0.1"

# Create Kubernetes secret
kubectl create secret tls vault-tls \
  --cert=vault-cert.pem \
  --key=vault-key.pem \
  -n vault

# Update Vault config for TLS
listener "tcp" {
  address = "[::]:8200"
  cluster_address = "[::]:8201"
  tls_cert_file = "/vault/tls/tls.crt"
  tls_key_file = "/vault/tls/tls.key"
}

RBAC Configuration

# vault-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: vault-k8s-role
rules:
- apiGroups: [""]
  resources: ["serviceaccounts", "serviceaccounts/token"]
  verbs: ["get", "list", "watch", "create"]
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["clusterrolebindings"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: vault-k8s-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: vault-k8s-role
subjects:
- kind: ServiceAccount
  name: vault
  namespace: vault

Troubleshooting Guide

Common Issues

Issue: Vault is sealed after restart

~jared gore