Vault and Secrets Management#
Audience: Operations Administrators, Security Professionals
Prerequisites: Understanding of secrets management
Outcome: Understand how Kleidia uses Vault for secret storage
Overview#
Kleidia uses OpenBao as the central secrets management system. All sensitive data is stored in OpenBao, not in the database.
Vault Architecture#
Deployment#
- Location: Kubernetes cluster (StatefulSet)
- Mode: Production mode with persistent storage
- Storage: File-based storage (can use Raft for HA)
- Auto-Unseal: Static key unsealing (no manual unseal)
Vault Components#
KV v2 Secrets Engine#
- Mount Path:
yubikeys/ - Purpose: Store YubiKey secrets (PINs, PUKs, management keys)
- Versioning: Enabled for secret versioning
- Encryption: AES-256-GCM encryption at rest
PKI Secrets Engine#
- Mount Path:
pki/ - Purpose: Certificate Authority for YubiKey certificates
- Root CA: 10-year self-signed certificate
- Roles: Configurable PKI roles for certificate signing
Secret Storage#
YubiKey Secrets#
Secrets stored at path: yubikeys/data/{serial}/secrets
Structure:
{
"pin": "123456",
"puk": "12345678",
"management_key": "010203040506070801020304050607080102030405060708"
}Access:
- Backend retrieves secrets via Vault API
- Secrets encrypted at rest by Vault
- Secrets encrypted in transit (RSA-OAEP) to agent
Application Secrets#
Application secrets stored in separate Vault paths:
- JWT signing keys (
secret/data/kleidia/jwt-secret) - Encryption keys (
secret/data/kleidia/encryption-key) - Database passwords (
secret/data/kleidia/database) - License data (
secret/data/kleidia/licenses/*)
Authentication Model#
Kleidia uses AppRole authentication with dedicated roles for each component, following the principle of least privilege.
AppRole Overview#
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Helm Charts │ │ Backend Service │ │ License Service │
│ │ │ │ │ │
│ helm-admin │ │ backend-openbao │ │ license-openbao │
│ AppRole │ │ AppRole │ │ AppRole │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ OpenBao (Vault) │
│ │
│ • Audit logging enabled │
│ • Least-privilege policies │
│ • Separation of concerns │
└─────────────────────────────────────────────────────────────────┘Backend Authentication#
The backend service authenticates using the backend-openbao AppRole:
- Role ID: Stored in Kubernetes secret
openbao-backend-approle - Secret ID: Stored in Kubernetes secret
openbao-backend-approle - Token TTL: 1 hour (auto-renewed)
- Token Max TTL: 4 hours
Permissions:
- Read/write YubiKey secrets
- PKI operations (sign, issue, revoke)
- Read application secrets (JWT, encryption, database)
- Cannot access license secrets
License Service Authentication#
The license service authenticates using the license-openbao AppRole:
- Role ID: Stored in Kubernetes secret
openbao-license-approle - Secret ID: Stored in Kubernetes secret
openbao-license-approle - Token TTL: 1 hour (auto-renewed)
- Token Max TTL: 4 hours
Permissions:
- Read/write license secrets
- Cannot access YubiKey secrets
- Cannot access backend application secrets
Helm Admin Authentication#
For Helm upgrades after initial installation, the helm-admin AppRole is used:
- Role ID: Stored in Kubernetes secret
openbao-helm-approle - Secret ID: Stored in Kubernetes secret
openbao-helm-approle - Token TTL: 1 hour
Permissions:
- Manage auth method configurations
- Update policies
- Configure PKI roles
- Cannot read any secrets
- Cannot create new secrets engines
Vault Policies#
Backend Policy (kleidia-backend)#
# PKI operations
path "pki/sign/*" {
capabilities = ["create", "read", "update"]
}
path "pki/issue/*" {
capabilities = ["create", "read", "update"]
}
path "pki/cert/ca" {
capabilities = ["read"]
}
path "pki/revoke" {
capabilities = ["update"]
}
# YubiKey secrets
path "yubikeys/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "yubikeys/metadata/*" {
capabilities = ["list", "read", "delete"]
}
# Application secrets (specific paths only)
path "secret/data/kleidia/jwt-secret" {
capabilities = ["create", "read", "update"]
}
path "secret/data/kleidia/encryption-key" {
capabilities = ["create", "read", "update"]
}
path "secret/data/kleidia/database" {
capabilities = ["create", "read", "update"]
}
# Explicit deny for license secrets
path "secret/data/kleidia/licenses/*" {
capabilities = ["deny"]
}License Service Policy (license-service)#
# License secrets only
path "secret/data/kleidia/licenses/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/kleidia/licenses/*" {
capabilities = ["list", "read", "delete"]
}
# Explicit deny for backend secrets
path "yubikeys/*" {
capabilities = ["deny"]
}
path "secret/data/kleidia/jwt-secret" {
capabilities = ["deny"]
}Secret Lifecycle#
Secret Creation#
- User registers YubiKey with PIN/PUK/management key
- Frontend sends secrets to backend
- Backend stores secrets in Vault:
yubikeys/data/{serial}/secrets - Vault encrypts secrets at rest
- Backend confirms storage
Secret Retrieval#
- User requests YubiKey operation
- Frontend requests secrets:
GET /api/yubikey/{serial}/secrets - Backend authenticates to Vault (AppRole)
- Backend retrieves secrets from Vault
- Backend encrypts secrets with agent public key (RSA-OAEP)
- Backend returns encrypted secrets to frontend
Secret Rotation#
- Manual Rotation: User updates secrets via web interface
- Versioning: OpenBao maintains secret versions for rollback
Vault Configuration#
OpenBao Initialization and Bootstrap#
During initial Helm deployment, OpenBao (Vault) is automatically initialized:
Automatic Initialization Process#
- OpenBao Pod Starts: StatefulSet creates the OpenBao pod
- Auto-Initialization: Helm hook job initializes OpenBao:
- Generates root token
- Generates 3 recovery keys (for static seal)
- Stores keys temporarily in Kubernetes secret
openbao-init-keys
- Configuration Applied:
- Enables KV v2 secrets engine at
yubikeys/ - Enables PKI secrets engine at
pki/ - Enables AppRole authentication
- Creates AppRoles for backend, license service, and Helm admin
- Creates policies with least-privilege access
- Enables audit logging
- Enables KV v2 secrets engine at
Bootstrap Keys Security Model#
Initialization Keys Generated:
- Root Token: Master administrative token (deleted after bootstrap)
- Recovery Keys (3): Used for emergency recovery operations
- Unseal Key: (Legacy compatibility, not used with static seal)
Key Storage Flow:
Temporary Storage (During Installation):
- Keys stored in Kubernetes secret:
openbao-init-keysin namespace - Secret contains:
root-token,recovery-key-1,recovery-key-2,recovery-key-3 - Only accessible to backend service account with specific RBAC permissions
- Keys stored in Kubernetes secret:
First Admin Login (Key Retrieval):
- Admin user logs in for first time
- Backend detects
openbao-init-keyssecret exists - Modal automatically displays keys to admin user
- Admin must copy and securely store keys
- Admin confirms keys are saved
Secure Deletion (Post-Confirmation):
- Backend deletes
openbao-init-keyssecret from Kubernetes - Keys no longer exist in cluster
- Action logged in audit trail
- Keys only exist in admin’s secure storage
- Backend deletes
Post-Deletion Operations:
- Helm upgrades use
helm-adminAppRole (stored inopenbao-helm-approle) - Backend uses
backend-openbaoAppRole (stored inopenbao-backend-approle) - License service uses
license-openbaoAppRole (stored inopenbao-license-approle) - No root token required for normal operations
- Helm upgrades use
Security Rationale:
- Keys must be displayed to admin for disaster recovery scenarios
- Keys should not remain in cluster indefinitely (reduces attack surface)
- Manual deletion ensures admin has secured the keys
- One-time display prevents repeated exposure
- AppRoles enable continued operation without root token
RBAC Permissions for Bootstrap Keys#
Backend service account has scoped permissions:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: backend-secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["openbao-init-keys"]
verbs: ["get", "delete"]Security Features:
- Scoped to specific secret name only
- Only
getanddeleteverbs (no create/update) - Namespace-scoped (not cluster-wide)
- Only backend pod can access
Post-Initialization Setup#
After bootstrap keys are handled, Vault is fully configured:
- Enable KV v2:
vault secrets enable -path=yubikeys kv-v2 - Enable PKI:
vault secrets enable pki - Generate Root CA: Create self-signed root certificate
- Create Policies: Define backend, license, and admin policies
- Configure AppRoles: Set up AppRole authentication for all components
- Enable Audit Logging: File-based audit device at
/openbao/audit/audit.log
Production Configuration#
For production deployments:
- Persistent Storage: Use persistent volumes
- Auto-Unseal: Configure static key unsealing
- High Availability: Use Raft storage backend (optional)
- Audit Logging: Enabled by default
Audit Logging#
All OpenBao operations are logged for security and compliance:
Audit Log Configuration#
- Device Type: File
- Path:
/openbao/audit/audit.log - Format: JSON (one entry per line)
Logged Operations#
- Authentication attempts (success/failure)
- Secret read/write/delete operations
- Policy changes
- Configuration changes
- Token creation/renewal/revocation
Viewing Audit Logs#
# Get OpenBao pod
VAULT_POD=$(kubectl get pods -l app.kubernetes.io/name=openbao -n kleidia -o jsonpath='{.items[0].metadata.name}')
# View recent logs
kubectl exec -it $VAULT_POD -n kleidia -- tail -100 /openbao/audit/audit.log
# Search for specific paths
kubectl exec -it $VAULT_POD -n kleidia -- grep "yubikeys/data" /openbao/audit/audit.logSecurity Considerations#
Secret Protection#
- Encryption at Rest: Vault encrypts all secrets
- Access Control: Policies restrict secret access per component
- Audit Logging: All secret access logged
- Versioning: Secret versions for rollback
Privilege Separation#
| Component | YubiKey Secrets | License Secrets | PKI Operations | Config Changes |
|---|---|---|---|---|
| Backend | ✅ Full | ❌ Denied | ✅ Full | ❌ No |
| License Service | ❌ Denied | ✅ Full | ❌ No | ❌ No |
| Helm Admin | ❌ No Access | ❌ No Access | ✅ Roles only | ✅ Limited |
Operational Security#
- Root Token: Deleted after initial bootstrap
- AppRole Secrets: Stored in Kubernetes secrets
- Policy Review: Regular policy review and updates
- Access Monitoring: Monitor secret access patterns via audit logs
Troubleshooting#
Common Issues#
- Vault Unsealed: Check Vault status, unseal if needed
- Policy Denied: Verify AppRole has correct policy attached
- Secret Not Found: Check secret path and serial number
- Authentication Failed: Verify AppRole credentials in Kubernetes secret
Checking AppRole Authentication#
# Get OpenBao pod
VAULT_POD=$(kubectl get pods -l app.kubernetes.io/name=openbao -n kleidia -o jsonpath='{.items[0].metadata.name}')
# Check AppRole is enabled
kubectl exec -it $VAULT_POD -n kleidia -- bao auth list | grep approle
# Check backend AppRole exists
kubectl exec -it $VAULT_POD -n kleidia -- bao read auth/approle/role/backend-openbao
# Check Kubernetes secrets exist
kubectl get secret openbao-backend-approle -n kleidia
kubectl get secret openbao-license-approle -n kleidia
kubectl get secret openbao-helm-approle -n kleidia