This is part of the Vault + Kubernetes Integration Guide. Return to the main guide for the full architecture overview.
The Vault Secrets Operator (VSO) is HashiCorp’s latest and most Kubernetes-native approach to secrets management. It uses Custom Resource Definitions (CRDs) to declaratively define what secrets to sync — making it a perfect fit for GitOps workflows.
Why VSO Over Agent Injector?
| Feature | Agent Injector | VSO |
|---|---|---|
| Architecture | Sidecar per pod | Single controller |
| Resource overhead | High (per-pod) | Minimal (controller only) |
| Configuration | Annotations (imperative) | CRDs (declarative) |
| GitOps friendly | ⚠️ Limited | ✅ Fully declarative |
| Auto rollout restart | ❌ Manual | ✅ Built-in |
| Dynamic secrets | ✅ Via sidecar | ✅ Via operator reconciliation |
| Secrets in etcd | ❌ tmpfs only | ✅ Synced to K8s Secret |
Installation
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
--namespace vault-secrets-operator-system \
--create-namespace \
--set "defaultVaultConnection.enabled=true" \
--set "defaultVaultConnection.address=https://vault.corp.com:8200"
Core CRDs
VSO uses a three-layer CRD model:
VaultConnection ──► Where is Vault?
│
VaultAuth ──► How to authenticate?
│
VaultStaticSecret ──► What KV secrets to sync?
VaultDynamicSecret──► What dynamic creds to generate?
VaultPKISecret ──► What certificates to issue?
Step-by-Step Setup
Step 1: VaultConnection
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: vault-connection
namespace: webapp-ns
spec:
address: https://vault.corp.com:8200
skipTLSVerify: false
# For custom CA:
# caCertSecretRef: vault-ca-cert
Step 2: VaultAuth
Using Kubernetes Auth:
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth-k8s
namespace: webapp-ns
spec:
vaultConnectionRef: vault-connection
method: kubernetes
mount: kubernetes
kubernetes:
role: webapp-role
serviceAccount: webapp-sa
audiences:
- vault
Using AppRole Auth:
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth-approle
namespace: webapp-ns
spec:
vaultConnectionRef: vault-connection
method: appRole
mount: approle
appRole:
roleId: "your-role-id"
secretRef:
name: vault-approle-creds
key: secret-id
Step 3: VaultStaticSecret (KV Secrets)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: webapp-config
namespace: webapp-ns
spec:
type: kv-v2
mount: secret
path: webapp/config
refreshAfter: 60s
vaultAuthRef: vault-auth-k8s
destination:
name: webapp-k8s-secret
create: true
overwrite: true
labels:
app: webapp
type: Opaque
# Auto-restart pods when secret changes
rolloutRestartTargets:
- kind: Deployment
name: webapp
What happens:
- VSO authenticates with Vault using the referenced
VaultAuth - Reads the secret from
secret/data/webapp/config - Creates/updates a Kubernetes Secret named
webapp-k8s-secret - Re-checks every 60 seconds
- If the secret changes, updates the K8s Secret and restarts the
webappDeployment
Step 4: VaultDynamicSecret (Database Creds)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: webapp-db-creds
namespace: webapp-ns
spec:
mount: database
path: creds/webapp-readonly
vaultAuthRef: vault-auth-k8s
destination:
name: webapp-db-secret
create: true
overwrite: true
# Renew before 2/3 of TTL expires
renewalPercent: 67
rolloutRestartTargets:
- kind: Deployment
name: webapp
VSO handles the entire lease lifecycle — creation, renewal, and rotation.
Step 5: VaultPKISecret (TLS Certificates)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: webapp-tls
namespace: webapp-ns
spec:
mount: pki
role: webapp-cert
vaultAuthRef: vault-auth-k8s
commonName: webapp.corp.com
altNames:
- webapp.webapp-ns.svc
- webapp.webapp-ns.svc.cluster.local
ttl: 720h
destination:
name: webapp-tls-secret
create: true
type: kubernetes.io/tls
rolloutRestartTargets:
- kind: Deployment
name: webapp
Consuming Synced Secrets
Since VSO creates standard Kubernetes Secrets, your pods consume them the usual way:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: webapp-ns
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
serviceAccountName: webapp-sa
containers:
- name: webapp
image: webapp:latest
envFrom:
- secretRef:
name: webapp-k8s-secret
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: webapp-db-secret
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: webapp-db-secret
key: password
volumeMounts:
- name: tls
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls
secret:
secretName: webapp-tls-secret
Secret Data Transformation
VSO can transform secrets before writing them to the K8s Secret:
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: webapp-config
namespace: webapp-ns
spec:
type: kv-v2
mount: secret
path: webapp/config
vaultAuthRef: vault-auth-k8s
destination:
name: webapp-k8s-secret
create: true
transformation:
excludeRaw: true
excludes:
- internal_key
templates:
DATABASE_URL:
text: |
postgresql://{{ get .Secrets "db_user" }}:{{ get .Secrets "db_pass" }}@{{ get .Secrets "db_host" }}:5432/appdb
REDIS_URL:
text: |
redis://:{{ get .Secrets "redis_pass" }}@redis:6379/0
Monitoring
VSO exposes Prometheus metrics. Key metrics to watch:
# Scrape config for Prometheus
- job_name: vault-secrets-operator
kubernetes_sd_configs:
- role: pod
namespaces:
names: ["vault-secrets-operator-system"]
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_control_plane]
regex: controller-manager
action: keep
Key metrics:
vso_secret_sync_total— Total sync operationsvso_secret_sync_error_total— Failed syncsvso_secret_rotation_total— Secret rotations performed
Security Consideration
VSO writes secrets to etcd (as standard K8s Secrets). If your threat model requires secrets to never touch etcd, use the Agent Injector or CSI Provider instead. If using VSO, ensure:
- etcd encryption at rest is enabled
- RBAC restricts Secret read access
- Audit logging tracks Secret access
See also: ArgoCD Vault Plugin Guide for GitOps secret injection during ArgoCD sync.