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:

  1. VSO authenticates with Vault using the referenced VaultAuth
  2. Reads the secret from secret/data/webapp/config
  3. Creates/updates a Kubernetes Secret named webapp-k8s-secret
  4. Re-checks every 60 seconds
  5. If the secret changes, updates the K8s Secret and restarts the webapp Deployment

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 operations
  • vso_secret_sync_error_total — Failed syncs
  • vso_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.