Set up HashiCorp Vault Agent Injector to automatically inject secrets into Kubernetes pods without storing sensitive data in container images or configuration files. This production-grade approach replaces hardcoded secrets with dynamic, secure credential management.
Prerequisites
- Running Kubernetes cluster
- kubectl installed and configured
- Helm 3 installed
- Basic understanding of Kubernetes concepts
What this solves
Managing secrets in Kubernetes clusters creates security risks when credentials are stored in configuration files, environment variables, or container images. This tutorial shows you how to integrate HashiCorp Vault with Kubernetes to automatically inject secrets into pods at runtime, eliminating hardcoded credentials and providing centralized secret rotation.
Step-by-step configuration
Install Vault Agent Injector
Add the HashiCorp Helm repository and install Vault Agent Injector in your Kubernetes cluster.
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault \
--set="injector.enabled=true" \
--set="server.dev.enabled=true" \
--set="server.image.tag=1.15.2" \
--namespace=vault-system \
--create-namespace
Configure Vault server connection
Wait for the Vault pod to be ready and configure the connection to your Vault server.
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault --namespace=vault-system --timeout=300s
kubectl exec -it vault-0 -n vault-system -- /bin/sh
vault auth enable kubernetes
vault secrets enable -path=secret kv-v2
Create Kubernetes authentication method
Configure Vault to authenticate requests from Kubernetes service accounts using the cluster's JWT tokens.
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Create test secrets in Vault
Store example database credentials that will be injected into your application pods.
vault kv put secret/myapp/db \
username="dbuser" \
password="supersecret123" \
host="db.example.com" \
port="5432"
vault kv put secret/myapp/api \
token="api-token-xyz789" \
endpoint="https://api.example.com"
exit
Create Vault policy for application access
Define a policy that allows your application to read specific secret paths.
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "secret/metadata/myapp/*" {
capabilities = ["list"]
}
kubectl cp /tmp/myapp-policy.hcl vault-0:/tmp/myapp-policy.hcl -n vault-system
kubectl exec -it vault-0 -n vault-system -- vault policy write myapp /tmp/myapp-policy.hcl
Create Kubernetes service account and RBAC
Set up the service account that your application pods will use for Vault authentication.
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-auth-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-auth
namespace: default
kubectl apply -f /tmp/vault-service-account.yaml
Configure Kubernetes auth role in Vault
Create a Vault role that maps the Kubernetes service account to the application policy.
kubectl exec -it vault-0 -n vault-system -- vault write auth/kubernetes/role/myapp \
bound_service_account_names=vault-auth \
bound_service_account_namespaces=default \
policies=myapp \
ttl=24h
Create secret injection templates
Define how Vault Agent will format and inject secrets into your pod filesystem.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-database: "secret/data/myapp/db"
vault.hashicorp.com/agent-inject-secret-api: "secret/data/myapp/api"
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "secret/data/myapp/db" -}}
DATABASE_URL="postgresql://{{ .Data.data.username }}:{{ .Data.data.password }}@{{ .Data.data.host }}:{{ .Data.data.port }}/myapp"
{{- end -}}
vault.hashicorp.com/agent-inject-template-api: |
{{- with secret "secret/data/myapp/api" -}}
API_TOKEN="{{ .Data.data.token }}"
API_ENDPOINT="{{ .Data.data.endpoint }}"
{{- end -}}
spec:
serviceAccountName: vault-auth
containers:
- name: app
image: nginx:alpine
command: ["/bin/sh"]
args: ["-c", "while true; do echo 'App running...'; sleep 30; done"]
env:
- name: VAULT_SECRETS_PATH
value: "/vault/secrets"
Deploy application with secret injection
Deploy the application and verify that Vault Agent automatically injects secrets into the pod.
kubectl apply -f /tmp/app-deployment.yaml
kubectl wait --for=condition=ready pod -l app=myapp --timeout=300s
Configure secret rotation
Set up automatic secret rotation by configuring Vault Agent to refresh secrets periodically.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-rotation
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: myapp-rotation
template:
metadata:
labels:
app: myapp-rotation
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-pre-populate-only: "false"
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-cache-use-auto-auth-token: "true"
vault.hashicorp.com/secret-volume-path: "/vault/secrets"
vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/db"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/myapp/db" -}}
{
"database": {
"username": "{{ .Data.data.username }}",
"password": "{{ .Data.data.password }}",
"host": "{{ .Data.data.host }}",
"port": {{ .Data.data.port }}
}
}
{{- end -}}
spec:
serviceAccountName: vault-auth
containers:
- name: app
image: nginx:alpine
command: ["/bin/sh"]
args: ["-c", "while true; do cat /vault/secrets/config; sleep 60; done"]
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
readOnly: true
kubectl apply -f /tmp/app-deployment-rotation.yaml
Verify your setup
Check that secrets are properly injected and accessible within your application pods.
kubectl get pods -l app=myapp
kubectl logs -l app=myapp
POD_NAME=$(kubectl get pods -l app=myapp -o jsonpath='{.items[0].metadata.name}')
kubectl exec $POD_NAME -- ls -la /vault/secrets/
kubectl exec $POD_NAME -- cat /vault/secrets/database
kubectl exec $POD_NAME -- cat /vault/secrets/api
kubectl exec -it vault-0 -n vault-system -- vault kv get secret/myapp/db
Advanced configuration
Configure production Vault server
For production use, deploy Vault in HA mode with proper TLS certificates and persistent storage.
global:
enabled: true
tlsDisable: false
injector:
enabled: true
replicas: 3
logLevel: "info"
agentImage:
repository: hashicorp/vault
tag: 1.15.2
server:
image:
repository: hashicorp/vault
tag: 1.15.2
ha:
enabled: true
replicas: 3
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
tls_disable = 0
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/vault/userconfig/vault-server-tls/tls.crt"
tls_key_file = "/vault/userconfig/vault-server-tls/tls.key"
}
storage "raft" {
path = "/vault/data"
}
service_registration "kubernetes" {}
helm upgrade vault hashicorp/vault \
--namespace=vault-system \
--values=/tmp/vault-production-values.yaml
Set up secret versioning and rollback
Configure Vault to maintain secret versions for rollback capabilities.
kubectl exec -it vault-0 -n vault-system -- vault kv metadata put -max-versions=5 secret/myapp/db
kubectl exec -it vault-0 -n vault-system -- vault kv put secret/myapp/db \
username="dbuser" \
password="newsecret456" \
host="db.example.com" \
port="5432"
kubectl exec -it vault-0 -n vault-system -- vault kv get -version=1 secret/myapp/db
Configure dynamic database secrets
Set up Vault to generate database credentials dynamically with automatic rotation.
kubectl exec -it vault-0 -n vault-system -- vault secrets enable database
kubectl exec -it vault-0 -n vault-system -- vault write database/config/postgresql \
plugin_name="postgresql-database-plugin" \
connection_url="postgresql://{{username}}:{{password}}@postgres.example.com:5432/myapp?sslmode=require" \
allowed_roles="myapp-role" \
username="vault-admin" \
password="admin-password"
kubectl exec -it vault-0 -n vault-system -- vault write database/roles/myapp-role \
db_name="postgresql" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Security best practices
Enable Vault audit logging
Configure comprehensive audit logging for security compliance and monitoring.
kubectl exec -it vault-0 -n vault-system -- vault audit enable file file_path=/vault/logs/vault-audit.log
kubectl exec -it vault-0 -n vault-system -- vault write sys/config/auditing/request-headers/X-Forwarded-For hmac=false
Configure namespace isolation
Set up Vault namespaces to isolate secrets between different applications and environments.
kubectl exec -it vault-0 -n vault-system -- vault namespace create production
kubectl exec -it vault-0 -n vault-system -- vault namespace create staging
Implement secret access monitoring
Set up monitoring and alerting for secret access patterns using Vault metrics.
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-exporter-config
namespace: vault-system
data:
config.yml: |
vault:
addr: "https://vault.vault-system.svc.cluster.local:8200"
token: "your-monitoring-token"
metrics:
- vault_core_unsealed
- vault_runtime_alloc_bytes
- vault_auth_method_login_total
kubectl apply -f /tmp/vault-monitoring.yaml
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Pod fails to start with init container errors | Service account lacks proper RBAC permissions | Verify ClusterRoleBinding with kubectl get clusterrolebinding vault-auth-binding |
| Secrets not appearing in pod filesystem | Incorrect Vault role configuration or annotations | Check role binding with kubectl exec vault-0 -n vault-system -- vault read auth/kubernetes/role/myapp |
| Authentication failures in Vault logs | JWT token validation issues | Reconfigure auth method with vault write auth/kubernetes/config using correct cluster CA |
| Secret templates render as empty | Policy doesn't allow access to secret path | Update policy with vault policy write myapp /path/to/policy.hcl |
| Vault Agent sidecar consuming high CPU | Aggressive secret renewal configuration | Adjust vault.hashicorp.com/agent-cache-enable and renewal intervals |
Next steps
- Configure Kubernetes RBAC with service accounts and role bindings for enhanced cluster security
- Implement Kubernetes network policies for pod-to-pod security and traffic isolation
- Configure Vault dynamic secrets for databases with PostgreSQL and MySQL integration
- Set up Kubernetes monitoring with Prometheus Operator and custom metrics
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Default values
VAULT_VERSION="1.15.2"
NAMESPACE="vault-system"
APP_NAMESPACE="default"
# Usage message
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -v, --vault-version VERSION Vault version (default: $VAULT_VERSION)"
echo " -n, --namespace NAMESPACE Vault namespace (default: $NAMESPACE)"
echo " -a, --app-namespace NAMESPACE Application namespace (default: $APP_NAMESPACE)"
echo " -h, --help Show this help message"
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-v|--vault-version)
VAULT_VERSION="$2"
shift 2
;;
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-a|--app-namespace)
APP_NAMESPACE="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
# Error handling and cleanup
cleanup() {
echo -e "${RED}Error occurred. Cleaning up...${NC}"
rm -f /tmp/myapp-policy.hcl /tmp/vault-service-account.yaml /tmp/myapp-deployment.yaml
}
trap cleanup ERR
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
;;
*)
echo -e "${RED}Unsupported distro: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
# Check if running as root or with sudo
check_privileges() {
if [[ $EUID -eq 0 ]]; then
SUDO_CMD=""
elif command -v sudo >/dev/null 2>&1; then
SUDO_CMD="sudo"
# Test sudo access
if ! sudo -n true 2>/dev/null; then
echo -e "${YELLOW}This script requires sudo privileges${NC}"
fi
else
echo -e "${RED}This script requires root privileges or sudo${NC}"
exit 1
fi
}
echo -e "${GREEN}[1/10] Checking prerequisites...${NC}"
check_privileges
echo -e "${GREEN}[2/10] Updating package repositories...${NC}"
$SUDO_CMD $PKG_UPDATE
echo -e "${GREEN}[3/10] Installing required packages...${NC}"
if command -v curl >/dev/null 2>&1; then
echo "curl already installed"
else
$SUDO_CMD $PKG_INSTALL curl
fi
# Check if kubectl is installed
if ! command -v kubectl >/dev/null 2>&1; then
echo -e "${RED}kubectl is required but not installed. Please install kubectl first.${NC}"
exit 1
fi
# Check if helm is installed
if ! command -v helm >/dev/null 2>&1; then
echo -e "${YELLOW}Installing Helm...${NC}"
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
rm -f get_helm.sh
fi
echo -e "${GREEN}[4/10] Verifying Kubernetes cluster access...${NC}"
if ! kubectl cluster-info >/dev/null 2>&1; then
echo -e "${RED}Cannot connect to Kubernetes cluster. Please check your kubeconfig.${NC}"
exit 1
fi
echo -e "${GREEN}[5/10] Adding HashiCorp Helm repository...${NC}"
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
echo -e "${GREEN}[6/10] Installing Vault with Agent Injector...${NC}"
helm install vault hashicorp/vault \
--set="injector.enabled=true" \
--set="server.dev.enabled=true" \
--set="server.image.tag=$VAULT_VERSION" \
--namespace="$NAMESPACE" \
--create-namespace
echo -e "${GREEN}[7/10] Waiting for Vault pod to be ready...${NC}"
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault --namespace="$NAMESPACE" --timeout=300s
echo -e "${GREEN}[8/10] Configuring Vault authentication and secrets...${NC}"
# Enable Kubernetes auth and KV secrets engine
kubectl exec -it vault-0 -n "$NAMESPACE" -- vault auth enable kubernetes
kubectl exec -it vault-0 -n "$NAMESPACE" -- vault secrets enable -path=secret kv-v2
# Configure Kubernetes auth method
kubectl exec -it vault-0 -n "$NAMESPACE" -- sh -c '
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
'
# Create test secrets
kubectl exec -it vault-0 -n "$NAMESPACE" -- vault kv put secret/myapp/db \
username="dbuser" \
password="supersecret123" \
host="db.example.com" \
port="5432"
kubectl exec -it vault-0 -n "$NAMESPACE" -- vault kv put secret/myapp/api \
token="api-token-xyz789" \
endpoint="https://api.example.com"
echo -e "${GREEN}[9/10] Creating Vault policy and Kubernetes resources...${NC}"
# Create Vault policy
cat > /tmp/myapp-policy.hcl << 'EOF'
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "secret/metadata/myapp/*" {
capabilities = ["list"]
}
EOF
kubectl cp /tmp/myapp-policy.hcl vault-0:/tmp/myapp-policy.hcl -n "$NAMESPACE"
kubectl exec -it vault-0 -n "$NAMESPACE" -- vault policy write myapp /tmp/myapp-policy.hcl
# Create service account and RBAC
cat > /tmp/vault-service-account.yaml << EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth
namespace: $APP_NAMESPACE
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-auth-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-auth
namespace: $APP_NAMESPACE
EOF
kubectl apply -f /tmp/vault-service-account.yaml
# Configure Kubernetes auth role in Vault
kubectl exec -it vault-0 -n "$NAMESPACE" -- vault write auth/kubernetes/role/myapp \
bound_service_account_names=vault-auth \
bound_service_account_namespaces="$APP_NAMESPACE" \
policies=myapp \
ttl=24h
# Create example deployment with secret injection
cat > /tmp/myapp-deployment.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: $APP_NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-database: "secret/data/myapp/db"
vault.hashicorp.com/agent-inject-secret-api: "secret/data/myapp/api"
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "secret/data/myapp/db" -}}
DATABASE_URL="postgresql://{{ .Data.data.username }}:{{ .Data.data.password }}@{{ .Data.data.host }}:{{ .Data.data.port }}/myapp"
{{- end }}
vault.hashicorp.com/agent-inject-template-api: |
{{- with secret "secret/data/myapp/api" -}}
API_TOKEN="{{ .Data.data.token }}"
API_ENDPOINT="{{ .Data.data.endpoint }}"
{{- end }}
spec:
serviceAccountName: vault-auth
containers:
- name: myapp
image: nginx:latest
ports:
- containerPort: 80
EOF
kubectl apply -f /tmp/myapp-deployment.yaml
echo -e "${GREEN}[10/10] Verifying installation...${NC}"
# Wait for deployment to be ready
kubectl wait --for=condition=available deployment/myapp --namespace="$APP_NAMESPACE" --timeout=300s
# Check if secrets were injected
echo -e "${YELLOW}Checking secret injection...${NC}"
POD_NAME=$(kubectl get pods -n "$APP_NAMESPACE" -l app=myapp -o jsonpath='{.items[0].metadata.name}')
if kubectl exec -n "$APP_NAMESPACE" "$POD_NAME" -- ls /vault/secrets/ | grep -q "database\|api"; then
echo -e "${GREEN}✓ Secrets successfully injected into pod${NC}"
else
echo -e "${RED}✗ Secret injection failed${NC}"
exit 1
fi
# Cleanup temporary files
rm -f /tmp/myapp-policy.hcl /tmp/vault-service-account.yaml /tmp/myapp-deployment.yaml
echo -e "${GREEN}Installation completed successfully!${NC}"
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Check injected secrets: kubectl exec -n $APP_NAMESPACE $POD_NAME -- cat /vault/secrets/database"
echo "2. View Vault UI (if enabled): kubectl port-forward -n $NAMESPACE vault-0 8200:8200"
echo "3. Customize the deployment template for your application needs"
Review the script before running. Execute with: bash install.sh