5.6 KiB
Vault Secrets Operator (VSO) Reference
Overview
The platform uses HashiCorp Vault Secrets Operator (VSO) to sync secrets from Vault KV v2 to native Kubernetes Secrets. This replaces the previous SealedSecrets workflow.
Key benefit: Secret values can be rotated via Vault UI/CLI without a git commit. Only new VaultStaticSecret CRDs need to be committed.
Architecture
Vault (KV v2) VSO K8s Secret
kv/{namespace}/{name} --> VaultStaticSecret CRD --> Secret in namespace
(polls every 30s)
- Vault: Standalone instance in
vaultnamespace, KV v2 atkv/ - VSO: Deployed in
vault-secrets-operator-systemnamespace via ArgoCD - Auth: Kubernetes auth method — each namespace has its own ServiceAccount + VaultAuth CRD
KV Path Convention
kv/{namespace}/{secret-name}
Examples:
kv/homepage/homepage-widget-credentialskv/argocd/forte-helm-repokv/gitea/gitea-smtp-secretkv/keycloak/keycloak-credentials
Vault Policy Structure
Each namespace gets a read-only policy:
# Policy: ns-{namespace}
path "kv/data/{namespace}/*" {
capabilities = ["read"]
}
path "kv/metadata/{namespace}/*" {
capabilities = ["read", "list"]
}
Kubernetes Auth Roles
Each namespace has a bound ServiceAccount:
Role: ns-{namespace}
bound_service_account_names: vault-auth-{namespace}
bound_service_account_namespaces: {namespace}
policies: ns-{namespace}
audience: vault
ttl: 1h
CRD Reference
VaultAuth
Per-namespace auth binding. One per namespace.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: {namespace}
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: ns-{namespace}
serviceAccount: vault-auth-{namespace}
audiences:
- vault
Each VaultAuth requires a corresponding ServiceAccount:
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth-{namespace}
namespace: {namespace}
VaultStaticSecret
One per secret. Syncs a Vault KV path to a K8s Secret.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: {secret-name}
namespace: {namespace}
spec:
type: kv-v2
mount: kv
path: {namespace}/{secret-name}
destination:
name: {secret-name} # K8s Secret name (must match what apps expect)
create: true
type: Opaque # Optional, defaults to Opaque
labels: # Optional, for secrets that need labels
some-label: "value"
refreshAfter: 30s
vaultAuthRef: vault-auth
Special Labels
Some secrets require specific labels for correct operation:
| Secret | Label | Purpose |
|---|---|---|
renovate-env |
allowedToBeCloned: "true" |
Kyverno secret-cloner policy |
gitea-smtp-secret |
allowedToBeCloned: "true" |
Kyverno secret-cloner policy |
forte-helm-repo |
argocd.argoproj.io/secret-type: repository |
ArgoCD repository recognition |
forte10x-repo-creds |
argocd.argoproj.io/secret-type: repository |
ArgoCD repository recognition |
mcp10x-repo-creds |
argocd.argoproj.io/secret-type: repository |
ArgoCD repository recognition |
These are set in destination.labels of the VaultStaticSecret CRD.
Namespaces & Secrets Map
| Namespace | Secrets |
|---|---|
homepage |
homepage-widget-credentials |
renovate |
renovate-env |
gitea |
gitea-credentials, gitea-backup-s3, gitea-smtp-secret, gitea-runner-token |
keycloak |
keycloak-credentials, microsoft-idp-credentials (overlay) |
argocd |
forte-helm-repo, forte10x-repo-creds, mcp10x-repo-creds, argocd-notifications-secret |
mcp10x |
app-credentials |
ts-mcp |
ts-mcp-secrets |
argocd-mcp |
auth-oidc, argocd-mcp-credentials |
dot-ai |
dot-ai-secrets |
music-man |
musicman-credentials |
Common Operations
Add a new secret
-
Write to Vault:
vault kv put kv/{namespace}/{secret-name} key1=val1 key2=val2 -
Create VaultStaticSecret YAML (see template above)
-
Add to kustomization.yaml in the appropriate directory
-
Commit and push — ArgoCD syncs the CRD, VSO creates the K8s Secret
Rotate a secret value
No git commit needed:
vault kv put kv/{namespace}/{secret-name} key1=new-val1 key2=new-val2
VSO picks up changes within 30 seconds.
Check sync status
# VaultAuth status
kubectl get vaultauth -n {namespace}
# VaultStaticSecret status
kubectl get vaultstaticsecret -n {namespace}
# Verify K8s Secret exists with correct keys
kubectl get secret {name} -n {namespace} -o jsonpath='{.data}' | jq
Troubleshooting
- VaultAuth not authenticating: Check ServiceAccount exists, Vault role matches SA name/namespace
- VaultStaticSecret not syncing: Check
kubectl describe vaultstaticsecret {name} -n {ns}for events - Secret missing keys: Verify Vault KV path has all expected keys:
vault kv get kv/{ns}/{name} - Permission denied: Verify Vault policy allows read on
kv/data/{ns}/*
File Locations
| Type | Location |
|---|---|
| VSO ArgoCD Application | infra/base/vault-secrets-operator/ |
| VSO Helm values | infra/values/base/vault-secrets-operator-values.yaml |
| Vault policies script | scripts/vault-setup-policies.sh |
| Seed script | scripts/seed-vault-from-cluster.sh |
| VaultAuth + VaultStaticSecret | Alongside ArgoCD Application in each component directory |
Setup Scripts
# Create all Vault policies and auth roles
./scripts/vault-setup-policies.sh
# Seed Vault KV from existing K8s Secrets
./scripts/seed-vault-from-cluster.sh