Production Secret Management with Vault
Overview
HashiCorp Vault provides secure, centralized secret management for the K3s cluster. This setup uses Vault in High Availability (HA) mode with Raft storage backend and the Vault Secrets Operator to automatically sync secrets from Vault into Kubernetes.
How It Works
- Vault acts as a centralized, secure database for secrets
- The Vault Secrets Operator runs in the cluster and watches for
VaultSecretresources - When a
VaultSecretis applied, the operator securely authenticates to Vault, fetches the specified data, and creates a regular KubernetesSecret - Applications use the Kubernetes
Secretas normal
This keeps actual secrets safe in Vault, while your Git repository only contains non-sensitive instructions for how to retrieve them.
Part 1: Install Vault in HA Mode
For production environments, Vault is installed in High Availability mode using its integrated Raft storage backend.
Add HashiCorp Helm Repository
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Install Vault in HA Mode
helm install vault hashicorp/vault \
--namespace vault \
--create-namespace \
--set "server.ha.enabled=true" \
--set "server.ha.raft.enabled=true"
This creates a Vault cluster with:
- Multiple Vault pods for high availability
- Raft storage backend for data persistence
- Automatic leader election
Verify Installation
kubectl get pods -n vault
Wait for all Vault pods to be in Running state.
Part 2: Initialize and Unseal Vault
A production Vault starts "sealed" for security. You must initialize it to get the master keys and then unseal it.
Initialize Vault
- Exec into the first Vault pod:
kubectl exec -it -n vault vault-0 -- /bin/sh
- Initialize Vault:
vault operator init
This command outputs:
- 5 Unseal Keys - You need 3 of these to unseal Vault
- 1 Initial Root Token - Used for initial authentication
⚠️ CRITICAL: Save all of this information securely (password manager, secure storage). This is the only time you'll see the unseal keys and root token in plain text.
Unseal Vault
Vault requires 3 out of 5 unseal keys to become operational. Run the unseal command three times, each time with a different key:
vault operator unseal <UNSEAL_KEY_1>
vault operator unseal <UNSEAL_KEY_2>
vault operator unseal <UNSEAL_KEY_3>
After the third key is entered, Vault will be unsealed. You can verify with:
vault status
You should see Sealed: false.
Type exit to leave the pod shell.
Unseal Additional Vault Pods
If you have multiple Vault pods (HA mode), you need to unseal each one:
# Unseal vault-1
kubectl exec -it -n vault vault-1 -- vault operator unseal <UNSEAL_KEY_1>
kubectl exec -it -n vault vault-1 -- vault operator unseal <UNSEAL_KEY_2>
kubectl exec -it -n vault vault-1 -- vault operator unseal <UNSEAL_KEY_3>
# Unseal vault-2 (if exists)
kubectl exec -it -n vault vault-2 -- vault operator unseal <UNSEAL_KEY_1>
kubectl exec -it -n vault vault-2 -- vault operator unseal <UNSEAL_KEY_2>
kubectl exec -it -n vault vault-2 -- vault operator unseal <UNSEAL_KEY_3>
Part 3: Install the Vault Secrets Operator
The Vault Secrets Operator is the bridge between your cluster and Vault, automatically syncing secrets.
Install the Operator
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
--namespace vault-secrets-operator \
--create-namespace
Verify Installation
kubectl get pods -n vault-secrets-operator
Wait for the operator pod to be in Running state.
Part 4: Configure Vault for Kubernetes Authentication
Configure Vault to trust your Kubernetes cluster and allow the operator to fetch secrets.
Connect to Vault and Log In
kubectl exec -it -n vault vault-0 -- /bin/sh
vault login <YOUR_INITIAL_ROOT_TOKEN>
Enable Secrets Engine and Kubernetes Auth
# Enable the KVv2 secrets engine at the path "secret/"
vault secrets enable -path=secret kv-v2
# Enable the Kubernetes auth method
vault auth enable kubernetes
# Configure the auth method with cluster details
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Create Policies
Policies define what secrets can be accessed. Create a policy for Longhorn R2 secrets:
vault policy write longhorn-r2-policy - <<EOF
path "secret/data/longhorn-r2" {
capabilities = ["read"]
}
EOF
For ArgoCD secrets:
vault policy write argocd-secrets-reader - <<EOF
path "secret/data/argocd/*" {
capabilities = ["read"]
}
EOF
Create Authentication Roles
Roles link policies to Kubernetes service accounts. Create a role for Longhorn:
vault write auth/kubernetes/role/longhorn-role \
bound_service_account_names=default \
bound_service_account_namespaces=longhorn-system \
policies=longhorn-r2-policy \
ttl=24h
Create a role for ArgoCD:
vault write auth/kubernetes/role/argocd \
bound_service_account_names=argocd-repo-server \
bound_service_account_namespaces=argocd \
policies=argocd-secrets-reader \
ttl=24h
Type exit to leave the pod shell.
Part 5: Store Secrets in Vault
Store Secrets via CLI
Connect to Vault pod and store your secrets:
kubectl exec -it -n vault vault-0 -- /bin/sh
vault login <YOUR_ROOT_TOKEN>
Store Longhorn R2 credentials:
vault kv put secret/longhorn-r2 \
AWS_ACCESS_KEY_ID="YOUR_R2_ACCESS_KEY_ID" \
AWS_SECRET_ACCESS_KEY="YOUR_R2_SECRET_ACCESS_KEY" \
AWS_ENDPOINTS="https://<YOUR_ACCOUNT_ID>.r2.cloudflarestorage.com"
Type exit to leave the pod shell.
Store Secrets via Web UI
- Port-forward to Vault UI:
kubectl port-forward -n vault svc/vault 8200:8200
-
Open browser: Navigate to
http://127.0.0.1:8200 -
Log in: Choose Token method and paste your root token
-
Navigate to Secrets: Go to
secret/→longhorn-r2 -
Create secret: Click "Create secret" and add your key-value pairs
Part 6: Sync Secrets with Vault Secrets Operator
The Vault Secrets Operator uses VaultSecret resources to sync secrets from
Vault into Kubernetes.
VaultConnection Resource
First, create a connection to Vault (k3s-vault/vault-connection.yaml):
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: vault-connection
namespace: longhorn-system
spec:
address: http://vault.vault:8200
skipTLSVerify: true
Apply it:
kubectl apply -f k3s-vault/vault-connection.yaml
VaultAuth Resource
Create the authentication configuration (k3s-vault/vault-auth.yaml):
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth-kubernetes
namespace: longhorn-system
spec:
vaultConnectionRef: vault-connection
method: kubernetes
mount: kubernetes
kubernetes:
role: longhorn-role
serviceAccount: default
Apply it:
kubectl apply -f k3s-vault/vault-auth.yaml
VaultSecret Resource
Create the VaultSecret resource (k3s-vault/longhorn-r2-vault-secret.yaml):
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultSecret
metadata:
name: longhorn-r2-secret-from-vault
namespace: longhorn-system
spec:
vault:
address: http://vault.vault:8200
auth:
kubernetes:
role: 'longhorn-role'
mountPath: 'kubernetes'
target:
name: r2-longhorn-secret
template:
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: '{{ .AWS_ACCESS_KEY_ID }}'
AWS_SECRET_ACCESS_KEY: '{{ .AWS_SECRET_ACCESS_KEY }}'
AWS_ENDPOINTS: '{{ .AWS_ENDPOINTS }}'
source:
kv:
path: 'secret/longhorn-r2'
version: '2'
Apply it:
kubectl apply -f k3s-vault/longhorn-r2-vault-secret.yaml
Verify Secret Creation
Check that the operator created the Kubernetes secret:
kubectl get secret r2-longhorn-secret -n longhorn-system
kubectl get vaultsecret -n longhorn-system
The operator should show Status: Valid and the Kubernetes secret should exist.
Accessing the Vault Web UI
Vault has a powerful built-in web UI perfect for day-to-day operations.
Port-Forward to Vault
kubectl port-forward -n vault svc/vault 8200:8200
Leave this terminal running.
Open the UI
Navigate to http://127.0.0.1:8200 in your browser.
Log In
- Choose Token as the login method
- Paste your Initial Root Token (saved during initialization)
- Click Sign in
You can now:
- Browse and edit secrets visually
- Create new policies
- Manage authentication roles
- View audit logs
- Monitor Vault health
Best Practices
Secret Rotation
Regularly rotate secrets stored in Vault:
-
Update secret in Vault:
kubectl exec -it -n vault vault-0 -- /bin/sh
vault login <TOKEN>
vault kv put secret/longhorn-r2 \
AWS_ACCESS_KEY_ID="NEW_KEY" \
AWS_SECRET_ACCESS_KEY="NEW_SECRET" -
Vault Secrets Operator automatically syncs the updated secret to Kubernetes
Policy Management
- Principle of Least Privilege: Only grant read access to specific paths
- Separate Policies: Create separate policies for different applications
- Regular Audits: Review policies periodically
Token Management
- Avoid Root Token: Create admin tokens with limited scope for daily use
- Token Rotation: Regularly rotate tokens
- Short TTLs: Use short TTLs for service account tokens (e.g., 24h)
Namespace Isolation
Create separate VaultAuth resources for different namespaces to ensure proper isolation:
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: myapp-auth
namespace: myapp
spec:
vaultConnectionRef: vault-connection
method: kubernetes
mount: kubernetes
kubernetes:
role: myapp-role
serviceAccount: myapp-sa
Troubleshooting
Vault is Sealed
If Vault becomes sealed (e.g., after a restart), unseal it:
kubectl exec -it -n vault vault-0 -- /bin/sh
vault operator unseal <UNSEAL_KEY_1>
vault operator unseal <UNSEAL_KEY_2>
vault operator unseal <UNSEAL_KEY_3>
VaultSecret Not Syncing
-
Check VaultSecret status:
kubectl describe vaultsecret <name> -n <namespace> -
Check operator logs:
kubectl logs -n vault-secrets-operator deployment/vault-secrets-operator -
Verify Vault connection:
kubectl get vaultconnection -n <namespace> -
Check authentication:
kubectl get vaultauth -n <namespace>
Authentication Failures
-
Verify role exists:
kubectl exec -it -n vault vault-0 -- vault read auth/kubernetes/role/<role-name> -
Check service account:
kubectl get serviceaccount -n <namespace> -
Verify policy:
kubectl exec -it -n vault vault-0 -- vault policy read <policy-name>
Secret Not Found
-
Verify secret exists in Vault:
kubectl exec -it -n vault vault-0 -- vault kv get secret/longhorn-r2 -
Check path in VaultSecret:
kubectl get vaultsecret <name> -n <namespace> -o yaml
Verification Commands
Check Vault Status
kubectl exec -it -n vault vault-0 -- vault status
List Secrets in Vault
kubectl exec -it -n vault vault-0 -- vault kv list secret/
Check Vault Secrets Operator
kubectl get pods -n vault-secrets-operator
kubectl get vaultsecrets -A
Verify Synced Secrets
kubectl get secrets -n longhorn-system | grep r2
References
- Vault configuration:
K3S/k3s-vault/ - Vault documentation: https://developer.hashicorp.com/vault/docs
- Vault Secrets Operator: https://secrets-store-csi-driver.sigs.k8s.io/