Never manually unseal Vault again! This Kubernetes operator automatically initializes and unseals HashiCorp Vault instances using transit unseal, making Vault operations truly hands-free.
- 🚀 Automatic Initialization & Unsealing - No manual intervention required
- 🔧 Flexible Configuration - Direct values, ConfigMaps, Secrets with YAML path extraction
- 🔐 Security First - Recovery keys only in logs, never stored by default
- ⚡ Post-Unseal Setup - Auto-configures KV engine and External Secrets Operator
- 🔄 Integration Ready - Works with Reflector and Reloader out of the box
- Kubernetes 1.24+
- Transit Vault instance (can be external)
- Transit token with appropriate permissions
- Vault configured with transit seal (see Vault Configuration)
Important: This operator does NOT replace Vault's seal configuration. Your Vault instance must be configured to use transit unsealing in its configuration file.
Add the following to your Vault configuration (vault.hcl
):
# Transit auto-unseal configuration
seal "transit" {
address = "http://your-transit-vault:8200"
disable_renewal = "false"
key_name = "autounseal"
mount_path = "transit"
tls_skip_verify = "true" # Only for development
# The token can be injected via environment variable
token = "your-transit-token" # or use VAULT_SEAL_TRANSIT_TOKEN env var
}
The operator automates the unsealing process but requires Vault to be configured for transit unsealing. Without this configuration, Vault won't know how to decrypt its master key.
helm repo add fredericrous https://fredericrous.github.io/charts
helm repo update
helm install vault-transit-unseal-operator fredericrous/vault-transit-unseal-operator \
--namespace vault-transit-unseal-system \
--create-namespace
The operator needs a token to authenticate with your transit Vault:
kubectl create secret generic vault-transit-token \
-n vault \
--from-literal=token=<YOUR_TRANSIT_TOKEN>
apiVersion: vault.homelab.io/v1alpha1
kind: VaultTransitUnseal
metadata:
name: vault-main
namespace: vault
spec:
vaultPod:
selector:
app: vault
transitVault:
address: http://transit-vault:8200
secretRef:
name: vault-transit-token
postUnsealConfig:
enableKV: true
enableExternalSecretsOperator: true
That's it! The operator will now automatically initialize and unseal your Vault pods.
Here's how the Vault configuration and operator work together:
# 1. Vault ConfigMap with transit seal configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-config
namespace: vault
data:
vault.hcl: |
seal "transit" {
address = "http://transit-vault:8200"
key_name = "autounseal"
mount_path = "transit"
token = "TRANSIT_TOKEN_PLACEHOLDER" # Will be replaced by init container
}
# ... rest of Vault config
---
# 2. Vault StatefulSet that uses the config
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: vault
namespace: vault
spec:
template:
spec:
initContainers:
- name: config-templater
# Replace token placeholder with actual secret
command: ["sh", "-c", "sed -i 's/TRANSIT_TOKEN_PLACEHOLDER/'$TOKEN'/g' /vault/config/vault.hcl"]
env:
- name: TOKEN
valueFrom:
secretKeyRef:
name: vault-transit-token
key: token
containers:
- name: vault
image: hashicorp/vault:1.20.1
args: ["server", "-config=/vault/config/vault.hcl"]
---
# 3. VaultTransitUnseal CRD that manages unsealing
apiVersion: vault.homelab.io/v1alpha1
kind: VaultTransitUnseal
metadata:
name: vault-main
namespace: vault
spec:
vaultPod:
selector:
app: vault
transitVault:
address: http://transit-vault:8200 # Same as in vault.hcl
secretRef:
name: vault-transit-token # Same secret as StatefulSet
The operator handles the unsealing lifecycle, but Vault must be configured to use transit sealing.
Store transit vault addresses in ConfigMaps and even use nested YAML with dot notation:
# ConfigMap with structured YAML
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-config
namespace: vault
data:
config.yaml: |
environments:
production:
transit:
address: "https://vault.prod.example.com:8200"
---
# Extract nested values
apiVersion: vault.homelab.io/v1alpha1
kind: VaultTransitUnseal
metadata:
name: vault-main
namespace: vault
spec:
transitVault:
addressFrom:
configMapKeyRef:
name: vault-config
key: config.yaml.environments.production.transit.address
default: "http://fallback-vault:8200" # Fallback value
secretRef:
name: vault-transit-token
apiVersion: vault.homelab.io/v1alpha1
kind: VaultTransitUnseal
metadata:
name: vault-production
namespace: vault
spec:
vaultPod:
selector:
app.kubernetes.io/name: vault
transitVault:
addressFrom:
configMapKeyRef:
name: vault-config
key: transit.address
default: "https://backup-vault:8200"
secretRef:
name: vault-transit-token
keyName: autounseal-prod
mountPath: transit
initialization:
recoveryShares: 5
recoveryThreshold: 3
secretNames:
storeRecoveryKeys: false # Production: keys only in logs
adminTokenAnnotations:
# Cross-namespace secret sync
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "external-secrets,argocd"
# Automatic pod restarts on secret change
reloader.stakater.com/match: "true"
postUnsealConfig:
enableKV: true
enableExternalSecretsOperator: true
externalSecretsOperatorConfig:
kubernetesAuth:
roleName: "external-secrets"
serviceAccounts:
- name: external-secrets
namespace: external-secrets
- Watches for Vault pods in your cluster
- Detects uninitialized or sealed instances
- Initializes Vault with transit unseal configuration
- Stores admin tokens in Kubernetes secrets
- Configures KV engine and authentication post-initialization
- Monitors continuously and unseals automatically when needed
- Recovery Keys: Not stored by default - appear once in operator logs during initialization
- Production: Keep
storeRecoveryKeys: false
and capture keys from logs - Development: Set
storeRecoveryKeys: true
for convenience - Token Rotation: Implement regular transit token rotation
vault-admin-token
: Root token for Vault admin accessvault-keys
: Recovery keys (only ifstoreRecoveryKeys: true
)
Symptom: Operator logs show no Vault pods found
Solution: Check your pod selector matches your Vault deployment labels:
# Check Vault pod labels
kubectl get pods -n vault --show-labels
# Verify selector in VaultTransitUnseal
kubectl get vaulttransitunseal -n vault -o yaml
Symptom: "permission denied" errors in operator logs
Solution: Verify transit token permissions:
# Test token manually
export VAULT_ADDR=<transit-vault-address>
export VAULT_TOKEN=<your-transit-token>
vault write transit/keys/autounseal type=aes256-gcm96
Symptom: "key not found in ConfigMap" errors
Solution:
- Check if using dot notation for nested YAML
- Verify the ConfigMap exists and contains the expected structure
- Consider adding a default value
View operator logs:
kubectl logs -n vault-transit-unseal-system deployment/vault-transit-unseal-controller-manager -f
Enable debug logging via Helm:
controllerManager:
manager:
args:
- --zap-log-level=debug
For detailed configuration options:
kubectl explain vaulttransitunseal.spec
New releases automatically update the Helm chart:
# Tag and push a new version
git tag v0.2.0
git push origin v0.2.0
# Create a GitHub release → chart updates automatically
Contributions are welcome! Please feel free to submit a Pull Request.
To install from source:
git clone https://github.com/fredericrous/vault-transit-unseal-operator.git
cd vault-transit-unseal-operator
helm install vault-transit-unseal-operator ./charts/vault-transit-unseal-operator \
--namespace vault-transit-unseal-system \
--create-namespace
For complete command-line flags documentation, see vault-transit-unseal-operator --help
.
MIT