Set up HashiCorp Vault with Kubernetes to dynamically inject secrets into pods using the Vault Secrets Operator. This tutorial covers authentication configuration, operator deployment, and automated secret injection with annotations.
Prerequisites
- Running Kubernetes cluster with kubectl access
- Administrative access to install Helm charts
- Basic understanding of Kubernetes service accounts and RBAC
- Network connectivity between Vault and Kubernetes pods
What this solves
Managing secrets in Kubernetes clusters becomes complex when you need dynamic secret generation, rotation, and fine-grained access control. HashiCorp Vault with the Vault Secrets Operator provides centralized secret management that automatically injects secrets into pods without storing them in etcd or requiring manual secret updates.
Prerequisites and vault operator installation
Install HashiCorp Vault
First install Vault on your system to configure the initial setup and policies.
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install -y vault
Install kubectl and Helm
Install Kubernetes tools needed to deploy and manage the Vault operator.
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Start Vault in development mode
For this tutorial, start Vault in dev mode to focus on the Kubernetes integration. In production, use a proper Vault cluster.
vault server -dev -dev-root-token-id=root -dev-listen-address=0.0.0.0:8200 &
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'
echo 'export VAULT_ADDR="http://127.0.0.1:8200"' >> ~/.bashrc
echo 'export VAULT_TOKEN="root"' >> ~/.bashrc
Enable Vault secrets engines
Enable the key-value secrets engine and database secrets engine for dynamic secret generation.
vault secrets enable -path=secret kv-v2
vault secrets enable -path=database database
Create example secrets
Create some test secrets that will be injected into Kubernetes pods.
vault kv put secret/myapp username="appuser" password="supersecret123"
vault kv put secret/database host="db.example.com" port="5432" database="myapp"
Configure Vault authentication with Kubernetes
Enable Kubernetes authentication
Configure Vault to accept authentication from Kubernetes service accounts.
vault auth enable kubernetes
Get Kubernetes cluster information
Extract the information Vault needs to communicate with your Kubernetes cluster.
export VAULT_SA_NAME=$(kubectl get sa vault -o jsonpath="{.secrets[*]['name']}" -n default)
export SA_JWT_TOKEN=$(kubectl get secret $VAULT_SA_NAME -o jsonpath="{.data.token}" -n default | base64 --decode; echo)
export SA_CA_CRT=$(kubectl get secret $VAULT_SA_NAME -o jsonpath="{.data['ca\.crt']}" -n default | base64 --decode; echo)
export K8S_HOST=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.server}')
Create Vault service account in Kubernetes
Create a service account that Vault will use to verify other service accounts.
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault
namespace: default
kubectl apply -f vault-sa.yaml
Configure Kubernetes authentication in Vault
Tell Vault how to connect to Kubernetes and verify service account tokens.
vault write auth/kubernetes/config \
token_reviewer_jwt="$SA_JWT_TOKEN" \
kubernetes_host="$K8S_HOST" \
kubernetes_ca_cert="$SA_CA_CRT" \
issuer="https://kubernetes.default.svc.cluster.local"
Create Vault policies
Define which secrets each application can access through specific policies.
path "secret/data/myapp" {
capabilities = ["read"]
}
path "secret/data/database" {
capabilities = ["read"]
}
path "database/creds/myapp-role" {
capabilities = ["read"]
}
vault policy write myapp-policy myapp-policy.hcl
Create Kubernetes authentication role
Bind Kubernetes service accounts to Vault policies through authentication roles.
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=default \
policies=myapp-policy \
ttl=24h
Deploy Vault secrets operator and CRDs
Add HashiCorp Helm repository
Add the official HashiCorp Helm repository to install the Vault Secrets Operator.
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Create namespace for Vault operator
Create a dedicated namespace for the Vault Secrets Operator components.
kubectl create namespace vault-secrets-operator-system
Install Vault Secrets Operator
Deploy the operator using Helm with custom values for your environment.
controller:
manager:
image:
repository: hashicorp/vault-secrets-operator
tag: 0.4.3
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
defaultVaultConnection:
enabled: true
address: "http://HOST_IP:8200"
skipTLSVerify: true
Update values with your host IP
Replace HOST_IP with your actual machine IP address so pods can reach Vault.
export HOST_IP=$(ip route get 1 | awk '{print $7}' | head -1)
sed -i "s/HOST_IP/$HOST_IP/g" vso-values.yaml
Deploy the operator
Install the Vault Secrets Operator with your custom configuration.
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
-f vso-values.yaml \
--version 0.4.3
Verify operator deployment
Check that all operator components are running correctly.
kubectl get pods -n vault-secrets-operator-system
kubectl logs -n vault-secrets-operator-system deployment/vault-secrets-operator-controller-manager
Create VaultConnection resource
Configure how the operator connects to your Vault instance.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
namespace: default
name: vault-connection
spec:
address: "http://HOST_IP:8200"
skipTLSVerify: true
sed "s/HOST_IP/$HOST_IP/g" vault-connection.yaml | kubectl apply -f -
Create VaultAuth resource
Configure authentication between the operator and Vault using Kubernetes service accounts.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
namespace: default
name: vault-auth
spec:
vaultConnectionRef: vault-connection
method: kubernetes
mount: kubernetes
kubernetes:
role: myapp
serviceAccount: myapp
kubectl apply -f vault-auth.yaml
Implement dynamic secrets injection with annotations
Create application service account
Create the service account that your application pods will use to authenticate with Vault.
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: default
kubectl apply -f myapp-sa.yaml
Create VaultStaticSecret for KV secrets
Define which static secrets should be synced from Vault into Kubernetes secrets.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
namespace: default
name: myapp-secret
spec:
vaultAuthRef: vault-auth
mount: secret
type: kv-v2
path: myapp
destination:
name: myapp-secret
create: true
refreshAfter: 30s
kubectl apply -f vault-static-secret.yaml
Create VaultDynamicSecret for database credentials
Configure dynamic database credentials that are generated on-demand by Vault.
#!/bin/bash
First configure a database connection in Vault
vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/myapp?sslmode=disable" \
allowed_roles="myapp-role" \
username="postgres" \
password="rootpassword"
Create a role for generating dynamic credentials
vault write database/roles/myapp-role \
db_name=postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"\
default_ttl="1h" \
max_ttl="24h"
chmod +x database-config.sh
Run this only if you have a PostgreSQL database configured
./database-config.sh
Create example application deployment
Deploy a test application that uses the secrets injected by the Vault Secrets Operator.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: myapp
containers:
- name: myapp
image: nginx:latest
env:
- name: APP_USERNAME
valueFrom:
secretKeyRef:
name: myapp-secret
key: username
- name: APP_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secret
key: password
- name: DB_HOST
valueFrom:
secretKeyRef:
name: myapp-secret
key: host
optional: true
volumeMounts:
- name: secret-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secret-volume
secret:
secretName: myapp-secret
kubectl apply -f myapp-deployment.yaml
Create application with init container pattern
Use an init container to fetch secrets before the main application starts, useful for applications that need secrets at startup.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-init
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: myapp-init
template:
metadata:
labels:
app: myapp-init
spec:
serviceAccountName: myapp
initContainers:
- name: vault-init
image: hashicorp/vault:1.15.2
command: ['/bin/sh']
args:
- -c
- |
export VAULT_ADDR=http://HOST_IP:8200
export VAULT_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
vault auth -method=kubernetes role=myapp
vault kv get -field=username secret/myapp > /shared/username
vault kv get -field=password secret/myapp > /shared/password
echo "Secrets fetched successfully"
volumeMounts:
- name: shared-data
mountPath: /shared
containers:
- name: myapp
image: nginx:latest
command: ['/bin/sh']
args:
- -c
- |
echo "Username: $(cat /shared/username)"
echo "Password: $(cat /shared/password)"
nginx -g 'daemon off;'
volumeMounts:
- name: shared-data
mountPath: /shared
volumes:
- name: shared-data
emptyDir: {}
sed "s/HOST_IP/$HOST_IP/g" myapp-init-deployment.yaml | kubectl apply -f -
Verify your setup
Check Vault Secrets Operator status
Verify that the operator is successfully syncing secrets from Vault.
kubectl get vaultstaticsecrets -n default
kubectl describe vaultstaticsecret myapp-secret -n default
Verify Kubernetes secrets creation
Check that secrets have been created in Kubernetes with the expected data.
kubectl get secrets myapp-secret -o yaml
kubectl get secret myapp-secret -o jsonpath='{.data.username}' | base64 -d
Test application secret access
Verify that your application pods can access the injected secrets.
kubectl exec deployment/myapp -- env | grep APP_
kubectl exec deployment/myapp -- cat /etc/secrets/username
kubectl exec deployment/myapp -- cat /etc/secrets/password
Monitor operator logs
Check operator logs to ensure secrets are being synced without errors.
kubectl logs -n vault-secrets-operator-system deployment/vault-secrets-operator-controller-manager -f
Test secret rotation
Update a secret in Vault and verify it gets synchronized to Kubernetes.
vault kv put secret/myapp username="newuser" password="newsecret456"
Wait 30 seconds for refresh
sleep 30
kubectl get secret myapp-secret -o jsonpath='{.data.username}' | base64 -d
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| VaultStaticSecret shows permission denied | Service account lacks proper Vault role binding | Check vault write auth/kubernetes/role/myapp matches service account name |
| Secrets not appearing in pods | VaultConnection cannot reach Vault | Verify HOST_IP is accessible from pods: kubectl run test --image=curlimages/curl --rm -it -- curl http://HOST_IP:8200/v1/sys/health |
| Operator pods failing to start | CRDs not properly installed | Reinstall: helm uninstall vault-secrets-operator -n vault-secrets-operator-system && helm install ... |
| Authentication failures in logs | Kubernetes auth not configured | Reconfigure: vault write auth/kubernetes/config with correct cluster info |
| Secrets not refreshing | refreshAfter too long or not set | Set shorter refresh: refreshAfter: 30s in VaultStaticSecret spec |
Next steps
- Learn more advanced Vault and Kubernetes integration patterns
- Implement proper RBAC for service accounts
- Set up production Vault cluster with high availability
- Secure pod-to-pod communication with network policies
- Compare with Sealed Secrets approach for GitOps workflows
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
VAULT_VERSION="1.15.4"
VAULT_TOKEN="${VAULT_TOKEN:-root}"
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
# Usage
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Install HashiCorp Vault with Kubernetes integration"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --dev-mode Install in development mode (default)"
echo " --vault-addr Vault address (default: $VAULT_ADDR)"
echo " --vault-token Vault token (default: $VAULT_TOKEN)"
exit 1
}
# Parse arguments
DEV_MODE=true
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) usage ;;
--dev-mode) DEV_MODE=true ;;
--vault-addr) VAULT_ADDR="$2"; shift ;;
--vault-token) VAULT_TOKEN="$2"; shift ;;
*) echo -e "${RED}Unknown option: $1${NC}"; usage ;;
esac
shift
done
# Cleanup function
cleanup() {
echo -e "${YELLOW}Cleaning up on error...${NC}"
pkill -f "vault server" || true
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
echo -e "${GREEN}[1/10] Checking prerequisites...${NC}"
if [[ $EUID -ne 0 ]] && ! sudo -n true 2>/dev/null; then
echo -e "${RED}This script requires sudo privileges${NC}"
exit 1
fi
if ! command -v curl &> /dev/null; then
echo -e "${RED}curl is required but not installed${NC}"
exit 1
fi
}
# Detect OS
detect_os() {
echo -e "${GREEN}[2/10] Detecting operating system...${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
echo "Detected OS: $PRETTY_NAME (Package manager: $PKG_MGR)"
else
echo -e "${RED}/etc/os-release not found${NC}"
exit 1
fi
}
# Install HashiCorp Vault
install_vault() {
echo -e "${GREEN}[3/10] Installing HashiCorp Vault...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo $PKG_UPDATE
sudo $PKG_INSTALL vault
else
sudo $PKG_INSTALL dnf-plugins-core
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo $PKG_INSTALL vault
fi
vault --version
}
# Install kubectl
install_kubectl() {
echo -e "${GREEN}[4/10] Installing kubectl...${NC}"
KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod 755 kubectl
sudo mv kubectl /usr/local/bin/
kubectl version --client
}
# Install Helm
install_helm() {
echo -e "${GREEN}[5/10] Installing Helm...${NC}"
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 755 get_helm.sh
./get_helm.sh
rm -f get_helm.sh
helm version
}
# Start Vault in dev mode
start_vault_dev() {
echo -e "${GREEN}[6/10] Starting Vault in development mode...${NC}"
if pgrep -f "vault server" > /dev/null; then
echo -e "${YELLOW}Vault server already running${NC}"
return
fi
vault server -dev -dev-root-token-id="$VAULT_TOKEN" -dev-listen-address=0.0.0.0:8200 &
sleep 5
export VAULT_ADDR="$VAULT_ADDR"
export VAULT_TOKEN="$VAULT_TOKEN"
# Add to current user's bashrc
if [[ -n "${SUDO_USER:-}" ]]; then
USER_HOME=$(eval echo ~${SUDO_USER})
echo "export VAULT_ADDR=\"$VAULT_ADDR\"" >> "$USER_HOME/.bashrc"
echo "export VAULT_TOKEN=\"$VAULT_TOKEN\"" >> "$USER_HOME/.bashrc"
chown $SUDO_USER:$SUDO_USER "$USER_HOME/.bashrc"
else
echo "export VAULT_ADDR=\"$VAULT_ADDR\"" >> ~/.bashrc
echo "export VAULT_TOKEN=\"$VAULT_TOKEN\"" >> ~/.bashrc
fi
echo -e "${YELLOW}Warning: Development mode stores data in memory. Never use in production!${NC}"
}
# Configure Vault secrets engines
configure_vault_secrets() {
echo -e "${GREEN}[7/10] Configuring Vault secrets engines...${NC}"
export VAULT_ADDR="$VAULT_ADDR"
export VAULT_TOKEN="$VAULT_TOKEN"
# Enable secrets engines
vault secrets enable -path=secret kv-v2
vault secrets enable -path=database database
# Create example secrets
vault kv put secret/myapp username="appuser" password="supersecret123"
vault kv put secret/database host="db.example.com" port="5432" database="myapp"
}
# Configure Kubernetes authentication
configure_k8s_auth() {
echo -e "${GREEN}[8/10] Configuring Kubernetes authentication...${NC}"
# Check if kubectl can connect to cluster
if ! kubectl cluster-info &>/dev/null; then
echo -e "${YELLOW}Warning: kubectl cannot connect to Kubernetes cluster${NC}"
echo "Please ensure you have a running Kubernetes cluster and proper kubeconfig"
return
fi
# Enable Kubernetes authentication
vault auth enable kubernetes || echo "Kubernetes auth already enabled"
# Create vault service account
cat > /tmp/vault-sa.yaml << 'EOF'
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault
namespace: default
EOF
kubectl apply -f /tmp/vault-sa.yaml
rm -f /tmp/vault-sa.yaml
# Configure Vault Kubernetes auth
K8S_HOST=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.server}')
K8S_CA_CERT=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}' | base64 -d)
vault write auth/kubernetes/config \
kubernetes_host="$K8S_HOST" \
kubernetes_ca_cert="$K8S_CA_CERT"
}
# Install Vault Secrets Operator
install_vault_operator() {
echo -e "${GREEN}[9/10] Installing Vault Secrets Operator...${NC}"
if ! kubectl cluster-info &>/dev/null; then
echo -e "${YELLOW}Skipping Vault Secrets Operator installation - no Kubernetes cluster available${NC}"
return
fi
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm upgrade --install vault-secrets-operator hashicorp/vault-secrets-operator \
--namespace vault-secrets-operator-system \
--create-namespace \
--wait
}
# Verify installation
verify_installation() {
echo -e "${GREEN}[10/10] Verifying installation...${NC}"
# Check Vault
if vault status &>/dev/null; then
echo -e "${GREEN}✓ Vault is running and accessible${NC}"
else
echo -e "${RED}✗ Vault is not accessible${NC}"
return 1
fi
# Check kubectl
if command -v kubectl &>/dev/null; then
echo -e "${GREEN}✓ kubectl is installed${NC}"
else
echo -e "${RED}✗ kubectl is not installed${NC}"
return 1
fi
# Check Helm
if command -v helm &>/dev/null; then
echo -e "${GREEN}✓ Helm is installed${NC}"
else
echo -e "${RED}✗ Helm is not installed${NC}"
return 1
fi
# Check Vault secrets
if vault kv get secret/myapp &>/dev/null; then
echo -e "${GREEN}✓ Vault secrets are configured${NC}"
else
echo -e "${RED}✗ Vault secrets are not accessible${NC}"
return 1
fi
echo -e "${GREEN}Installation completed successfully!${NC}"
echo ""
echo "Environment variables set:"
echo " VAULT_ADDR=$VAULT_ADDR"
echo " VAULT_TOKEN=$VAULT_TOKEN"
echo ""
echo "Next steps:"
echo "1. Source your bashrc: source ~/.bashrc"
echo "2. Create Vault policies for your applications"
echo "3. Deploy VaultAuth and VaultStaticSecret resources"
}
# Main execution
main() {
echo -e "${GREEN}HashiCorp Vault Kubernetes Integration Installer${NC}"
echo "================================================"
check_prerequisites
detect_os
install_vault
install_kubectl
install_helm
start_vault_dev
configure_vault_secrets
configure_k8s_auth
install_vault_operator
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh