Running Your Own Personal HashiCorp Vault on Local Kubernetes: A Security Engineer's Guide
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
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)
Create cluster with optimized configuration:
First, let’s set up a reusable kind configuration in your home directory that you can use for any Vault-related projects:
# Create kind config directory in home
mkdir -p ~/.config/kind
# Create a reusable Vault-optimized configuration
cat > ~/.config/kind/vault-cluster.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: vault-cluster
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 8200
protocol: TCP
extraMounts:
- hostPath: ~/.config/vault-data
containerPath: /vault-data
EOF
# Create the data directory in your home
mkdir -p ~/.config/vault-data
# Create the cluster using your personal configuration
kind create cluster --config ~/.config/kind/vault-cluster.yaml
# Verify cluster is ready
kubectl cluster-info --context kind-vault-cluster
kubectl get nodes
Benefits of this approach:
- Reusable: Use the same config across multiple projects
- Personal: Stored in your home directory, not tied to specific projects
- Persistent: The
~/vault-datadirectory persists across cluster recreations - Organized: Keep all your kind configs in
~/.config/kind/
Configuration Management:
# List your kind configurations
ls ~/.config/kind/
# Create different configs for different purposes
cat > ~/.config/kind/multi-node-vault.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: vault-multi-node
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 8200
protocol: TCP
- role: worker
- role: worker
EOF
# Use specific configurations as needed
kind create cluster --config ~/.config/kind/vault-cluster.yaml
kind create cluster --config ~/.config/kind/multi-node-vault.yaml
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
Method 1: Using Helm (Recommended)
# 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"
Cloud Backup Solutions
For a personal Vault, backing up to the cloud provides durability and disaster recovery capabilities. Here are comprehensive solutions for different cloud providers:
AWS S3 Backup Strategy
Prerequisites Setup
# Create dedicated S3 bucket for Vault backups
aws s3 mb s3://your-vault-backups-$(uuidgen | tr '[:upper:]' '[:lower:]' | head -c 8)
# Enable versioning for backup protection
aws s3api put-bucket-versioning \
--bucket your-vault-backups \
--versioning-configuration Status=Enabled
# Configure server-side encryption
aws s3api put-bucket-encryption \
--bucket your-vault-backups \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/vault-backup-key"
}
}
]
}'
# Set lifecycle policy to manage costs
aws s3api put-bucket-lifecycle-configuration \
--bucket your-vault-backups \
--lifecycle-configuration '{
"Rules": [
{
"ID": "VaultBackupLifecycle",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
},
{
"Days": 365,
"StorageClass": "DEEP_ARCHIVE"
}
],
"Expiration": {
"Days": 2555
}
}
]
}'
Enhanced Backup Script for AWS
#!/bin/bash
# vault-backup-aws.sh - Comprehensive AWS backup solution
set -euo pipefail
# Configuration
BACKUP_BUCKET="your-vault-backups"
BACKUP_PREFIX="vault-backups"
KMS_KEY_ID="alias/vault-backup-key"
BACKUP_DIR="/tmp/vault-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)
RETENTION_DAYS=90
# Logging
LOG_FILE="/var/log/vault-backup.log"
exec 1> >(tee -a "$LOG_FILE")
exec 2>&1
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Check prerequisites
check_prereqs() {
log "Checking prerequisites..."
# Check if kubectl can connect to cluster
if ! kubectl cluster-info &>/dev/null; then
log "ERROR: Cannot connect to Kubernetes cluster"
exit 1
fi
# Check if Vault pod is running
if ! kubectl get pod -n vault vault-0 &>/dev/null; then
log "ERROR: Vault pod vault-0 not found"
exit 1
fi
# Check AWS credentials
if ! aws sts get-caller-identity &>/dev/null; then
log "ERROR: AWS credentials not configured"
exit 1
fi
# Check if bucket exists
if ! aws s3api head-bucket --bucket "$BACKUP_BUCKET" &>/dev/null; then
log "ERROR: Backup bucket $BACKUP_BUCKET not accessible"
exit 1
fi
}
# Create snapshot
create_snapshot() {
log "Creating Vault snapshot..."
mkdir -p "$BACKUP_DIR"
# Check Vault status first
local vault_status
vault_status=$(kubectl exec -n vault vault-0 -- vault status -format=json 2>/dev/null || echo '{}')
if [[ $(echo "$vault_status" | jq -r '.sealed // true') == "true" ]]; then
log "WARNING: Vault is sealed, snapshot may be incomplete"
fi
# Create snapshot with retry logic
local retry_count=0
local max_retries=3
while [ $retry_count -lt $max_retries ]; do
if kubectl exec -n vault vault-0 -- vault operator raft snapshot save /tmp/backup.snap; then
break
fi
retry_count=$((retry_count + 1))
log "Snapshot creation failed, attempt $retry_count/$max_retries"
sleep 10
done
if [ $retry_count -eq $max_retries ]; then
log "ERROR: Failed to create snapshot after $max_retries attempts"
exit 1
fi
# Copy snapshot from pod
kubectl cp vault/vault-0:/tmp/backup.snap "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap"
# Verify snapshot file
if [[ ! -f "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap" ]] || [[ ! -s "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap" ]]; then
log "ERROR: Snapshot file is missing or empty"
exit 1
fi
log "Snapshot created successfully: vault-snapshot-$TIMESTAMP.snap"
}
# Backup configuration
backup_config() {
log "Backing up Vault configuration..."
# Export Vault configuration
kubectl get configmap -n vault vault-config -o yaml > "$BACKUP_DIR/vault-config-$TIMESTAMP.yaml"
kubectl get secret -n vault vault-tls -o yaml > "$BACKUP_DIR/vault-tls-$TIMESTAMP.yaml" 2>/dev/null || true
kubectl get statefulset -n vault vault -o yaml > "$BACKUP_DIR/vault-statefulset-$TIMESTAMP.yaml"
# Create manifest archive
tar -czf "$BACKUP_DIR/vault-manifests-$TIMESTAMP.tar.gz" -C "$BACKUP_DIR" \
vault-config-$TIMESTAMP.yaml \
vault-tls-$TIMESTAMP.yaml \
vault-statefulset-$TIMESTAMP.yaml 2>/dev/null || true
log "Configuration backup completed"
}
# Create metadata
create_metadata() {
log "Creating backup metadata..."
cat > "$BACKUP_DIR/metadata-$TIMESTAMP.json" << EOF
{
"timestamp": "$TIMESTAMP",
"hostname": "$HOSTNAME",
"vault_version": "$(kubectl get pod -n vault vault-0 -o jsonpath='{.spec.containers[0].image}' | cut -d: -f2)",
"kubernetes_version": "$(kubectl version --short --client | grep 'Client Version' | cut -d: -f2 | tr -d ' ')",
"backup_type": "full",
"files": [
"vault-snapshot-$TIMESTAMP.snap",
"vault-manifests-$TIMESTAMP.tar.gz"
],
"checksum": "$(sha256sum "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap" | cut -d' ' -f1)"
}
EOF
}
# Encrypt and upload to S3
upload_to_s3() {
log "Encrypting and uploading to S3..."
# Encrypt snapshot
gpg --batch --yes --symmetric --cipher-algo AES256 \
--passphrase-file <(echo "$GPG_PASSPHRASE") \
"$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap"
# Upload files to S3 with server-side encryption
aws s3 cp "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap.gpg" \
"s3://$BACKUP_BUCKET/$BACKUP_PREFIX/snapshots/" \
--server-side-encryption aws:kms \
--ssekms-key-id "$KMS_KEY_ID" \
--metadata "hostname=$HOSTNAME,timestamp=$TIMESTAMP,type=snapshot"
aws s3 cp "$BACKUP_DIR/vault-manifests-$TIMESTAMP.tar.gz" \
"s3://$BACKUP_BUCKET/$BACKUP_PREFIX/manifests/" \
--server-side-encryption aws:kms \
--ssekms-key-id "$KMS_KEY_ID"
aws s3 cp "$BACKUP_DIR/metadata-$TIMESTAMP.json" \
"s3://$BACKUP_BUCKET/$BACKUP_PREFIX/metadata/" \
--server-side-encryption aws:kms \
--ssekms-key-id "$KMS_KEY_ID"
log "Upload completed successfully"
}
# Cleanup old backups
cleanup_old_backups() {
log "Cleaning up old backups..."
# Local cleanup
find "$BACKUP_DIR" -name "vault-*" -mtime +7 -delete 2>/dev/null || true
# S3 cleanup (older than retention period)
local cutoff_date
cutoff_date=$(date -d "$RETENTION_DAYS days ago" '+%Y%m%d')
aws s3api list-objects-v2 \
--bucket "$BACKUP_BUCKET" \
--prefix "$BACKUP_PREFIX/" \
--query "Contents[?LastModified<='${cutoff_date}T00:00:00.000Z'].Key" \
--output text | while read -r key; do
if [[ -n "$key" ]]; then
aws s3 rm "s3://$BACKUP_BUCKET/$key"
log "Deleted old backup: $key"
fi
done
}
# Verify backup integrity
verify_backup() {
log "Verifying backup integrity..."
# Download and verify the uploaded file
aws s3 cp "s3://$BACKUP_BUCKET/$BACKUP_PREFIX/snapshots/vault-snapshot-$TIMESTAMP.snap.gpg" \
"/tmp/verify-$TIMESTAMP.snap.gpg"
# Compare checksums
local original_checksum
local uploaded_checksum
original_checksum=$(sha256sum "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap.gpg" | cut -d' ' -f1)
uploaded_checksum=$(sha256sum "/tmp/verify-$TIMESTAMP.snap.gpg" | cut -d' ' -f1)
if [[ "$original_checksum" == "$uploaded_checksum" ]]; then
log "Backup integrity verified successfully"
rm "/tmp/verify-$TIMESTAMP.snap.gpg"
else
log "ERROR: Backup integrity check failed"
exit 1
fi
}
# Send notification
send_notification() {
local status=$1
if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Vault backup $status for $HOSTNAME at $TIMESTAMP\"}" \
"$SLACK_WEBHOOK_URL" 2>/dev/null || true
fi
if [[ -n "${EMAIL_RECIPIENT:-}" ]]; then
echo "Vault backup $status for $HOSTNAME at $TIMESTAMP" | \
mail -s "Vault Backup $status" "$EMAIL_RECIPIENT" 2>/dev/null || true
fi
}
# Main execution
main() {
log "Starting Vault backup process..."
# Check for required GPG passphrase
if [[ -z "${GPG_PASSPHRASE:-}" ]]; then
log "ERROR: GPG_PASSPHRASE environment variable not set"
exit 1
fi
trap 'send_notification "FAILED"; exit 1' ERR
check_prereqs
create_snapshot
backup_config
create_metadata
upload_to_s3
verify_backup
cleanup_old_backups
send_notification "COMPLETED"
log "Vault backup process completed successfully"
}
main "$@"
Google Cloud Storage Backup
#!/bin/bash
# vault-backup-gcp.sh - Google Cloud Storage backup solution
set -euo pipefail
# Configuration
GCS_BUCKET="your-vault-backups"
PROJECT_ID="your-project-id"
KMS_KEY="projects/$PROJECT_ID/locations/global/keyRings/vault-backup/cryptoKeys/vault-backup-key"
BACKUP_PREFIX="vault-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Setup GCS bucket
setup_gcs() {
# Create bucket with uniform bucket-level access
gsutil mb -p "$PROJECT_ID" -l US gs://"$GCS_BUCKET" 2>/dev/null || true
# Enable versioning
gsutil versioning set on gs://"$GCS_BUCKET"
# Set lifecycle policy
cat > lifecycle.json << 'EOF'
{
"rule": [
{
"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
"condition": {"age": 30}
},
{
"action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
"condition": {"age": 90}
},
{
"action": {"type": "SetStorageClass", "storageClass": "ARCHIVE"},
"condition": {"age": 365}
},
{
"action": {"type": "Delete"},
"condition": {"age": 2555}
}
]
}
EOF
gsutil lifecycle set lifecycle.json gs://"$GCS_BUCKET"
rm lifecycle.json
}
# Upload with customer-managed encryption
upload_to_gcs() {
log "Uploading to Google Cloud Storage..."
# Upload snapshot with CMEK
gsutil -o "GSUtil:encryption_key=$KMS_KEY" cp \
"$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap.gpg" \
"gs://$GCS_BUCKET/$BACKUP_PREFIX/snapshots/"
# Upload manifests
gsutil -o "GSUtil:encryption_key=$KMS_KEY" cp \
"$BACKUP_DIR/vault-manifests-$TIMESTAMP.tar.gz" \
"gs://$GCS_BUCKET/$BACKUP_PREFIX/manifests/"
# Upload metadata
gsutil -o "GSUtil:encryption_key=$KMS_KEY" cp \
"$BACKUP_DIR/metadata-$TIMESTAMP.json" \
"gs://$GCS_BUCKET/$BACKUP_PREFIX/metadata/"
log "GCS upload completed"
}
# Include the main backup logic from AWS script...
Azure Blob Storage Backup
#!/bin/bash
# vault-backup-azure.sh - Azure Blob Storage backup solution
set -euo pipefail
# Configuration
STORAGE_ACCOUNT="yourvaultbackups"
CONTAINER_NAME="vault-backups"
RESOURCE_GROUP="vault-backup-rg"
KEY_VAULT_NAME="vault-backup-kv"
ENCRYPTION_KEY="vault-backup-key"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Setup Azure Blob Storage
setup_azure() {
# Create resource group
az group create --name "$RESOURCE_GROUP" --location eastus 2>/dev/null || true
# Create storage account with encryption
az storage account create \
--name "$STORAGE_ACCOUNT" \
--resource-group "$RESOURCE_GROUP" \
--location eastus \
--sku Standard_LRS \
--encryption-services blob \
--encryption-key-source Microsoft.Keyvault \
--encryption-key-vault "https://$KEY_VAULT_NAME.vault.azure.net/" \
--encryption-key-name "$ENCRYPTION_KEY" \
2>/dev/null || true
# Create container
az storage container create \
--name "$CONTAINER_NAME" \
--account-name "$STORAGE_ACCOUNT" \
--auth-mode login
# Set lifecycle policy
cat > lifecycle-policy.json << 'EOF'
{
"rules": [
{
"name": "VaultBackupLifecycle",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["vault-backups/"]
},
"actions": {
"baseBlob": {
"tierToCool": {
"daysAfterModificationGreaterThan": 30
},
"tierToArchive": {
"daysAfterModificationGreaterThan": 90
},
"delete": {
"daysAfterModificationGreaterThan": 2555
}
}
}
}
}
]
}
EOF
az storage account management-policy create \
--account-name "$STORAGE_ACCOUNT" \
--policy @lifecycle-policy.json \
--resource-group "$RESOURCE_GROUP"
rm lifecycle-policy.json
}
# Upload to Azure Blob Storage
upload_to_azure() {
log "Uploading to Azure Blob Storage..."
# Upload files with metadata
az storage blob upload \
--account-name "$STORAGE_ACCOUNT" \
--container-name "$CONTAINER_NAME" \
--name "snapshots/vault-snapshot-$TIMESTAMP.snap.gpg" \
--file "$BACKUP_DIR/vault-snapshot-$TIMESTAMP.snap.gpg" \
--metadata "hostname=$HOSTNAME" "timestamp=$TIMESTAMP" "type=snapshot" \
--auth-mode login
az storage blob upload \
--account-name "$STORAGE_ACCOUNT" \
--container-name "$CONTAINER_NAME" \
--name "manifests/vault-manifests-$TIMESTAMP.tar.gz" \
--file "$BACKUP_DIR/vault-manifests-$TIMESTAMP.tar.gz" \
--auth-mode login
az storage blob upload \
--account-name "$STORAGE_ACCOUNT" \
--container-name "$CONTAINER_NAME" \
--name "metadata/metadata-$TIMESTAMP.json" \
--file "$BACKUP_DIR/metadata-$TIMESTAMP.json" \
--auth-mode login
log "Azure upload completed"
}
# Include the main backup logic from AWS script...
Multi-Cloud Backup Strategy
#!/bin/bash
# vault-backup-multicloud.sh - Backup to multiple cloud providers
# Configuration for multi-cloud redundancy
BACKUP_PROVIDERS=("aws" "gcp" "azure")
BACKUP_DIR="/tmp/vault-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Provider-specific configurations
declare -A PROVIDER_CONFIGS
PROVIDER_CONFIGS[aws]="bucket=your-vault-backups,region=us-east-1"
PROVIDER_CONFIGS[gcp]="bucket=your-vault-backups,project=your-project-id"
PROVIDER_CONFIGS[azure]="account=yourvaultbackups,container=vault-backups"
backup_to_provider() {
local provider=$1
log "Starting backup to $provider..."
case $provider in
"aws")
source vault-backup-aws.sh
upload_to_s3
;;
"gcp")
source vault-backup-gcp.sh
upload_to_gcs
;;
"azure")
source vault-backup-azure.sh
upload_to_azure
;;
esac
log "Backup to $provider completed"
}
# Execute parallel backups
main() {
log "Starting multi-cloud backup process..."
# Create snapshot once
create_snapshot
backup_config
create_metadata
# Backup to all providers in parallel
for provider in "${BACKUP_PROVIDERS[@]}"; do
backup_to_provider "$provider" &
done
# Wait for all uploads to complete
wait
log "Multi-cloud backup process completed"
}
Automated Scheduling with Cron
# Add to crontab for automated backups
# Edit with: crontab -e
# Daily backup at 2 AM
0 2 * * * /home/user/scripts/vault-backup-aws.sh >> /var/log/vault-backup.log 2>&1
# Weekly full backup to all clouds (Sunday 3 AM)
0 3 * * 0 /home/user/scripts/vault-backup-multicloud.sh >> /var/log/vault-backup.log 2>&1
# Hourly snapshot during business hours
0 9-17 * * 1-5 /home/user/scripts/vault-snapshot-only.sh >> /var/log/vault-backup.log 2>&1
Kubernetes CronJob for Automated Backups
# vault-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-backup
namespace: vault
spec:
schedule: "0 2 * * *" # Daily at 2 AM
timeZone: "America/New_York"
jobTemplate:
spec:
template:
spec:
serviceAccountName: vault-backup
containers:
- name: backup
image: vault-backup:latest
env:
- name: AWS_REGION
value: "us-east-1"
- name: BACKUP_BUCKET
value: "your-vault-backups"
- name: GPG_PASSPHRASE
valueFrom:
secretKeyRef:
name: backup-secrets
key: gpg-passphrase
- name: SLACK_WEBHOOK_URL
valueFrom:
secretKeyRef:
name: backup-secrets
key: slack-webhook
volumeMounts:
- name: backup-script
mountPath: /scripts
- name: aws-credentials
mountPath: /root/.aws
command: ["/scripts/vault-backup-aws.sh"]
volumes:
- name: backup-script
configMap:
name: backup-scripts
defaultMode: 0755
- name: aws-credentials
secret:
secretName: aws-credentials
restartPolicy: OnFailure
backoffLimit: 3
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
Cloud Restore Procedures
AWS S3 Restore
#!/bin/bash
# vault-restore-aws.sh
BACKUP_BUCKET="your-vault-backups"
RESTORE_TIMESTAMP="$1"
if [[ -z "$RESTORE_TIMESTAMP" ]]; then
echo "Usage: $0 <timestamp>"
echo "Available backups:"
aws s3 ls "s3://$BACKUP_BUCKET/vault-backups/snapshots/" | grep ".snap.gpg"
exit 1
fi
# Download and decrypt
mkdir -p /tmp/restore
aws s3 cp "s3://$BACKUP_BUCKET/vault-backups/snapshots/vault-snapshot-$RESTORE_TIMESTAMP.snap.gpg" \
"/tmp/restore/"
# Decrypt
gpg --batch --yes --decrypt \
--passphrase-file <(echo "$GPG_PASSPHRASE") \
"/tmp/restore/vault-snapshot-$RESTORE_TIMESTAMP.snap.gpg" > \
"/tmp/restore/vault-snapshot-$RESTORE_TIMESTAMP.snap"
# Verify integrity
aws s3 cp "s3://$BACKUP_BUCKET/vault-backups/metadata/metadata-$RESTORE_TIMESTAMP.json" \
"/tmp/restore/"
expected_checksum=$(jq -r '.checksum' "/tmp/restore/metadata-$RESTORE_TIMESTAMP.json")
actual_checksum=$(sha256sum "/tmp/restore/vault-snapshot-$RESTORE_TIMESTAMP.snap" | cut -d' ' -f1)
if [[ "$expected_checksum" != "$actual_checksum" ]]; then
echo "ERROR: Checksum mismatch!"
exit 1
fi
# Restore to Vault
kubectl cp "/tmp/restore/vault-snapshot-$RESTORE_TIMESTAMP.snap" \
vault/vault-0:/tmp/restore.snap
kubectl exec -n vault vault-0 -- vault operator raft snapshot restore /tmp/restore.snap
echo "Restore completed successfully"
Disaster Recovery Runbook
# Vault Disaster Recovery Runbook
## Emergency Contacts
- Security Team: [email protected]
- On-call Engineer: +1-555-ONCALL
## Recovery Scenarios
### Scenario 1: Complete Cluster Loss
1. **Assessment** (5 minutes)
- Verify Kubernetes cluster status
- Check cloud provider status page
- Confirm backup availability
2. **Environment Recreation** (15 minutes)
```bash
# Recreate kind cluster
kind create cluster --config ~/.config/kind/vault-cluster.yaml
# Restore Vault manifests
aws s3 cp s3://your-vault-backups/vault-backups/manifests/vault-manifests-<timestamp>.tar.gz .
tar -xzf vault-manifests-<timestamp>.tar.gz
kubectl apply -f vault-statefulset-<timestamp>.yaml
-
Data Restoration (10 minutes)
# Use latest backup ./vault-restore-aws.sh $(aws s3 ls s3://your-vault-backups/vault-backups/snapshots/ | tail -1 | awk '{print $4}' | sed 's/vault-snapshot-//;s/.snap.gpg//') -
Verification (10 minutes)
- Test Vault accessibility
- Verify secret engines
- Confirm authentication methods
Scenario 2: Data Corruption
- Stop all write operations
- Identify last good backup
- Restore from point-in-time backup
- Verify data integrity
Scenario 3: Ransomware/Security Incident
- Isolate affected systems
- Use offline backup copies
- Rebuild from clean state
- Implement additional security measures
Recovery Testing
- Monthly: Test backup restoration process
- Quarterly: Full disaster recovery simulation
- Annually: Cross-region recovery test
### Security Considerations for Cloud Backups
#### Encryption at Rest and in Transit
```bash
# Generate strong encryption key for backups
openssl rand -base64 32 > ~/.config/vault-backup-key
# Use strong GPG encryption
export GPG_PASSPHRASE=$(openssl rand -base64 32)
# Encrypt before upload
gpg --batch --yes --symmetric --cipher-algo AES256 \
--passphrase-file <(echo "$GPG_PASSPHRASE") \
vault-snapshot.snap
Access Control and Monitoring
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VaultBackupAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/VaultBackupRole"
},
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::your-vault-backups/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption": "aws:kms"
},
"IpAddress": {
"aws:SourceIp": ["YOUR_HOME_IP/32", "YOUR_VPN_CIDR"]
}
}
}
]
}
Backup Verification and Testing
#!/bin/bash
# verify-backup-integrity.sh
# Monthly backup verification job
verify_monthly_backups() {
local current_month=$(date +%Y%m)
# List all backups for current month
aws s3api list-objects-v2 \
--bucket "$BACKUP_BUCKET" \
--prefix "vault-backups/snapshots/vault-snapshot-${current_month}" \
--query 'Contents[].Key' \
--output text | while read -r backup_key; do
local timestamp=$(basename "$backup_key" | sed 's/vault-snapshot-//;s/.snap.gpg//')
# Download and verify each backup
download_and_verify_backup "$timestamp"
# Test restore in isolated environment
test_restore_process "$timestamp"
done
}
test_restore_process() {
local timestamp=$1
# Create temporary test environment
kind create cluster --name vault-test-restore
# Restore backup to test cluster
./vault-restore-aws.sh "$timestamp"
# Verify Vault functionality
kubectl exec -n vault vault-0 -- vault status
kubectl exec -n vault vault-0 -- vault kv list secret/ || true
# Cleanup
kind delete cluster --name vault-test-restore
log "Backup verification completed for $timestamp"
}
This comprehensive cloud backup solution provides:
- Multi-cloud redundancy for disaster recovery
- Strong encryption at rest and in transit
- Automated scheduling with Kubernetes CronJobs
- Integrity verification and monitoring
- Detailed disaster recovery procedures
- Security best practices for cloud storage
Choose the cloud provider that best fits your needs, or implement the multi-cloud strategy for maximum redundancy. The automated scripts handle encryption, upload, verification, and cleanup, ensuring your personal Vault data is safely backed up to the cloud.
~jared gore