managing-secrets
Managing secrets (API keys, database credentials, certificates) with Vault, cloud providers, and Kubernetes. Use when storing sensitive data, rotating credentials, syncing secrets to Kubernetes, implementing dynamic secrets, or scanning code for leaked secrets.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install ancoleman-ai-design-components-secret-management
Repository
Skill path: skills/secret-management
Managing secrets (API keys, database credentials, certificates) with Vault, cloud providers, and Kubernetes. Use when storing sensitive data, rotating credentials, syncing secrets to Kubernetes, implementing dynamic secrets, or scanning code for leaked secrets.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Backend, DevOps, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: ancoleman.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install managing-secrets into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/ancoleman/ai-design-components before adding managing-secrets to shared team environments
- Use managing-secrets for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: managing-secrets
description: Managing secrets (API keys, database credentials, certificates) with Vault, cloud providers, and Kubernetes. Use when storing sensitive data, rotating credentials, syncing secrets to Kubernetes, implementing dynamic secrets, or scanning code for leaked secrets.
---
# Managing Secrets
Secure storage, rotation, and delivery of secrets (API keys, database credentials, TLS certificates) for applications and infrastructure.
## When to Use This Skill
Use when:
- Storing API keys, database credentials, or encryption keys
- Implementing secret rotation (manual or automatic)
- Syncing secrets from external stores to Kubernetes
- Setting up dynamic secrets (database, cloud providers)
- Scanning code for leaked secrets
- Implementing zero-knowledge patterns
- Meeting compliance requirements (SOC 2, ISO 27001, PCI DSS)
## Quick Decision Frameworks
### Framework 1: Choosing a Secret Store
| Scenario | Primary Choice | Alternative |
|----------|----------------|-------------|
| Kubernetes + Multi-Cloud | Vault + ESO | Cloud Secret Manager + ESO |
| Kubernetes + Single Cloud | Cloud Secret Manager + ESO | Vault + ESO |
| Serverless (AWS Lambda) | AWS Secrets Manager | AWS Parameter Store |
| Multi-Cloud Enterprise | HashiCorp Vault | Doppler (SaaS) |
| Small Team (<10 apps) | Doppler, Infisical | 1Password Secrets Automation |
| GitOps-Centric | SOPS (git-encrypted) | Sealed Secrets (K8s-only) |
**Decision Tree:**
- Kubernetes? → External Secrets Operator (ESO) with chosen backend
- Single cloud? → Cloud-native (AWS/GCP/Azure)
- Multi-cloud/on-prem? → HashiCorp Vault
- GitOps? → SOPS or Sealed Secrets
### Framework 2: Static vs. Dynamic Secrets
| Secret Type | Use Dynamic? | TTL | Solution |
|-------------|-------------|-----|----------|
| Database credentials | YES | 1 hour | Vault DB engine |
| Cloud IAM (AWS/GCP) | YES | 15 min | Vault cloud engine |
| SSH/RDP access | YES | 5 min | Vault SSH engine |
| TLS certificates | YES | 24 hours | Vault PKI / cert-manager |
| Third-party API keys | NO | Quarterly | Vault KV v2 (manual rotation) |
### Framework 3: Kubernetes Secret Delivery
| Method | Use Case | Rotation | Restart Required |
|--------|----------|----------|------------------|
| **External Secrets Operator** | Static secrets, periodic sync | Polling (1h) | Yes |
| **Secrets Store CSI Driver** | File-based, watch rotation | inotify | No |
| **Vault Secrets Operator** | Vault-specific, dynamic | Automatic renewal | Optional |
## HashiCorp Vault Fundamentals
### Core Components
- **Secrets Engines**: KV v2 (static), Database (dynamic), AWS, PKI, SSH
- **Auth Methods**: Kubernetes, JWT/OIDC, AppRole, LDAP
- **Policies**: HCL-based access control (least privilege)
- **Leases**: TTL for secrets, auto-renewal, auto-revocation
### Static Secrets (KV v2)
```bash
# Create secret
vault kv put secret/myapp/config api_key=sk_live_EXAMPLE
# Read secret
vault kv get secret/myapp/config
# List versions
vault kv metadata get secret/myapp/config
```
### Dynamic Database Credentials
```bash
# Configure PostgreSQL
vault write database/config/postgres \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb"
# Create role
vault write database/roles/app-role \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\"..." \
default_ttl="1h"
# Generate credentials
vault read database/creds/app-role
```
For detailed Vault architecture, see `references/vault-architecture.md`.
## Kubernetes Integration
### External Secrets Operator (ESO)
Syncs secrets from 30+ providers to Kubernetes Secrets.
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.example.com"
auth:
kubernetes:
role: "app-role"
```
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: secret/data/database/config
```
### Vault Secrets Operator (VSO)
Kubernetes-native Vault integration with automatic lease renewal.
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: postgres-creds
spec:
vaultAuthRef: vault-auth
mount: database
path: creds/app-role
renewalPercent: 67 # Renew at 67% of TTL
destination:
name: dynamic-db-creds
```
For ESO vs CSI vs VSO comparison, see `references/kubernetes-integration.md`.
## Secret Rotation Patterns
### Pattern 1: Versioned Static Secrets (Blue/Green)
1. Create new secret version in Vault
2. Update staging environment
3. Monitor for errors (24-48 hours)
4. Gradual production rollout (10% → 50% → 100%)
5. Revoke old secret (after 7 days)
### Pattern 2: Dynamic Database Credentials
Vault auto-generates credentials with short TTL:
- App fetches credentials from Vault
- Vault automatically renews lease (at 67% of TTL)
- On expiration, Vault revokes access
- On renewal failure, app requests new credentials
### Pattern 3: TLS Certificate Rotation
Using cert-manager + Vault PKI:
- cert-manager requests certificate from Vault
- Automatically renews before expiration (default: 67% of duration)
- Updates Kubernetes Secret on renewal
- Optional pod restart (via Reloader)
For detailed rotation workflows, see `references/rotation-patterns.md`.
## Multi-Language Integration
### Python (hvac)
```python
import hvac
client = hvac.Client(url='https://vault.example.com')
client.auth.kubernetes(role='app-role', jwt=jwt)
# Fetch dynamic credentials
response = client.secrets.database.generate_credentials(name='postgres-role')
username = response['data']['username']
password = response['data']['password']
```
### Go (Vault API)
```go
import vault "github.com/hashicorp/vault/api"
client, _ := vault.NewClient(vault.DefaultConfig())
k8sAuth, _ := auth.NewKubernetesAuth("app-role")
client.Auth().Login(context.Background(), k8sAuth)
secret, _ := client.Logical().Read("database/creds/postgres-role")
```
### TypeScript (node-vault)
```typescript
import vault from 'node-vault';
const client = vault({ endpoint: 'https://vault.example.com' });
await client.kubernetesLogin({ role: 'app-role', jwt });
const response = await client.read('database/creds/postgres-role');
```
For complete examples, see `examples/dynamic-db-credentials/`.
## Secret Scanning
### Pre-Commit Hooks (Gitleaks)
```bash
# Install Gitleaks
brew install gitleaks
# Run on staged files
gitleaks protect --staged --verbose
```
Pre-commit hook prevents secrets from being committed.
For setup, see `examples/secret-scanning/pre-commit`.
### CI/CD Integration
```yaml
# GitHub Actions
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
```
### Remediation Workflow
When a secret is leaked:
1. **Rotate immediately** (within 1 hour)
2. **Revoke at provider**
3. **Remove from Git history** (BFG Repo-Cleaner)
4. **Force push** (notify team)
5. **Audit access** (who had access during leak window)
6. **Document incident**
For detailed remediation, see `references/secret-scanning.md`.
## Zero-Knowledge Patterns
### Client-Side Encryption (E2EE)
User password → PBKDF2 → encryption key → encrypt secret → send to server
Server stores only encrypted blobs (cannot decrypt).
### Shamir's Secret Sharing
Split secret into N shares, require M to reconstruct (e.g., 3 of 5).
```bash
# Initialize Vault with Shamir shares
vault operator init -key-shares=5 -key-threshold=3
# Unseal requires 3 of 5 key shares
vault operator unseal <KEY_1>
vault operator unseal <KEY_2>
vault operator unseal <KEY_3>
```
For implementations, see `references/zero-knowledge.md`.
## Library Recommendations (2025)
### Secret Stores
| Library | Use Case | Trust Score |
|---------|----------|-------------|
| HashiCorp Vault | Enterprise, multi-cloud | High (73.3/100) |
| External Secrets Operator | Kubernetes integration | High (85.0/100) |
| AWS Secrets Manager | AWS workloads | High |
| GCP Secret Manager | GCP workloads | High |
| Azure Key Vault | Azure workloads | High |
### Secret Scanning
| Library | Use Case | Trust Score |
|---------|----------|-------------|
| Gitleaks | Pre-commit, CI/CD | High (89.9/100) |
| TruffleHog | Git history scanning | Medium |
### Client Libraries
| Language | Library | Version |
|----------|---------|---------|
| Python | `hvac` | 2.2.0+ |
| Go | `vault/api` | Latest |
| TypeScript | `node-vault` | 0.10.2+ |
| Rust | `vaultrs` | 0.7+ |
## Common Workflows
### Workflow 1: Vault + ESO on Kubernetes
1. Install Vault (Helm chart)
2. Initialize and unseal Vault
3. Enable Kubernetes auth
4. Install External Secrets Operator
5. Create SecretStore (Vault connection)
6. Create ExternalSecret (secret mapping)
For step-by-step guide, see `examples/vault-eso-setup/`.
### Workflow 2: Dynamic Database Credentials
1. Enable database secrets engine
2. Configure database connection
3. Create role with TTL
4. App fetches credentials
5. Vault auto-renews lease
For implementation, see `examples/dynamic-db-credentials/`.
### Workflow 3: Secret Scanning Remediation
1. Gitleaks detects secret
2. Block commit (pre-commit hook)
3. Developer removes secret
4. Developer stores in Vault
5. Developer references Vault path
For setup, see `examples/secret-scanning/`.
## Integration with Related Skills
- **auth-security**: OAuth client secrets, JWT signing keys
- **databases-***: Dynamic database credentials
- **deploying-applications**: Container registry credentials
- **observability**: Grafana/Datadog API keys
- **infrastructure-as-code**: Cloud provider credentials
## Security Best Practices
1. Never commit secrets to Git (use Gitleaks pre-commit hook)
2. Use dynamic secrets where possible
3. Rotate secrets regularly (quarterly for static, hourly for dynamic)
4. Implement least privilege (Vault policies, RBAC)
5. Enable audit logging
6. Encrypt at rest (Vault storage, etcd encryption)
7. Use short TTLs (< 24 hours for dynamic secrets)
8. Monitor failed access attempts
## Common Pitfalls
### Secrets in Environment Variables
Environment variables visible in process lists.
**Solution:** Use file-based secrets (Kubernetes volumes, CSI driver).
### Hardcoded Secrets in Manifests
Base64 is not encryption.
**Solution:** Use External Secrets Operator.
### No Secret Rotation
Stale credentials increase breach risk.
**Solution:** Use dynamic secrets or automate rotation.
### Root Token in Production
Unlimited permissions.
**Solution:** Use auth methods with least privilege policies.
## For Detailed Information, See
- `references/vault-architecture.md` - Vault internals, HA setup, policies
- `references/kubernetes-integration.md` - ESO, CSI driver, VSO comparison
- `references/rotation-patterns.md` - Detailed rotation workflows
- `references/secret-scanning.md` - Gitleaks, remediation procedures
- `references/zero-knowledge.md` - E2EE, Shamir's secret sharing
- `references/cloud-providers.md` - AWS, GCP, Azure secret managers
- `examples/vault-eso-setup/` - Complete Kubernetes setup
- `examples/dynamic-db-credentials/` - Multi-language examples
- `examples/secret-scanning/` - Pre-commit hooks, CI/CD
- `scripts/setup_vault.sh` - Automated Vault installation
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/vault-architecture.md
```markdown
# HashiCorp Vault Architecture
Comprehensive guide to Vault internals, high availability setup, policies, and production deployment patterns.
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [Secrets Engines](#secrets-engines)
3. [Authentication Methods](#authentication-methods)
4. [Policies and Access Control](#policies-and-access-control)
5. [Storage Backends](#storage-backends)
6. [High Availability Setup](#high-availability-setup)
7. [Audit Logging](#audit-logging)
8. [Production Deployment](#production-deployment)
9. [Common Pitfalls](#common-pitfalls)
## Architecture Overview
### Core Components
```
┌─────────────────────────────────────────────────────────┐
│ HashiCorp Vault Architecture │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ API Layer (HTTP/HTTPS) │ │
│ │ ├── RESTful API │ │
│ │ ├── CLI (vault command) │ │
│ │ └── UI (Web interface) │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Authentication Methods │ │
│ │ ├── Kubernetes (service accounts) │ │
│ │ ├── JWT/OIDC (external identity) │ │
│ │ ├── AppRole (machines, CI/CD) │ │
│ │ ├── LDAP/Active Directory │ │
│ │ └── Token (direct auth) │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Core (Policy Engine) │ │
│ │ ├── Request routing │ │
│ │ ├── Policy evaluation (HCL) │ │
│ │ ├── Lease management │ │
│ │ └── Token generation │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Secrets Engines │ │
│ │ ├── KV v2 (versioned key-value) │ │
│ │ ├── Database (dynamic credentials) │ │
│ │ ├── AWS (dynamic IAM) │ │
│ │ ├── PKI (TLS certificates) │ │
│ │ └── SSH (dynamic certificates) │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Barrier (Encryption Layer) │ │
│ │ ├── AES-256-GCM encryption │ │
│ │ ├── Unsealing mechanism │ │
│ │ └── Master key protection │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Storage Backend │ │
│ │ ├── Consul (HA, recommended) │ │
│ │ ├── etcd (Kubernetes native) │ │
│ │ ├── S3 (cost-effective, no HA) │ │
│ │ └── PostgreSQL (relational) │ │
│ └────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### Data Flow
1. **Request**: Client sends request (CLI, API, SDK)
2. **Authentication**: Vault validates identity (token, K8s SA, etc.)
3. **Authorization**: Policy engine evaluates permissions
4. **Secrets Engine**: Generates or retrieves secret
5. **Encryption**: Barrier encrypts data
6. **Storage**: Encrypted data written to backend
7. **Response**: Decrypted secret returned to client
## Secrets Engines
### KV v2 (Versioned Key-Value)
Static secrets with versioning and soft deletes.
**Enable:**
```bash
vault secrets enable -path=secret kv-v2
```
**Write Secret:**
```bash
vault kv put secret/myapp/config \
api_key=sk_live_123 \
database_url=postgresql://localhost/mydb
```
**Read Secret:**
```bash
vault kv get secret/myapp/config
vault kv get -version=2 secret/myapp/config # Specific version
```
**Versioning:**
```bash
vault kv metadata get secret/myapp/config # View all versions
vault kv undelete -versions=2 secret/myapp/config # Restore deleted version
vault kv destroy -versions=1 secret/myapp/config # Permanently delete
```
**Use Cases:**
- API keys (third-party services)
- OAuth client secrets
- Encryption keys (KEK)
- Configuration values
### Database Secrets Engine (Dynamic Credentials)
Auto-generates database credentials with TTL.
**Enable:**
```bash
vault secrets enable database
```
**Configure PostgreSQL:**
```bash
vault write database/config/postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="app-role,readonly-role" \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb?sslmode=require" \
username="vault-admin" \
password="vault-admin-password"
```
**Create Role:**
```bash
vault write database/roles/app-role \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
```
**Generate Credentials:**
```bash
vault read database/creds/app-role
# Output:
# Key Value
# --- -----
# lease_id database/creds/app-role/abc123
# lease_duration 1h
# username v-k8s-app-role-xyz789
# password A1b2C3d4E5f6
```
**Rotation:**
```bash
# Rotate root credentials (Vault admin password)
vault write -force database/rotate-root/postgres
```
**Supported Databases:**
- PostgreSQL, MySQL, MongoDB
- MSSQL, Oracle, Cassandra
- Elasticsearch, InfluxDB
- Redshift, Snowflake
### AWS Secrets Engine (Dynamic IAM)
Auto-generates AWS IAM credentials.
**Enable:**
```bash
vault secrets enable aws
```
**Configure:**
```bash
vault write aws/config/root \
access_key=AKIAIOSFODNN7EXAMPLE \
secret_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
region=us-east-1
```
**Create Role:**
```bash
vault write aws/roles/app-role \
credential_type=iam_user \
policy_document=-<<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
EOF
```
**Generate Credentials:**
```bash
vault read aws/creds/app-role
# Output:
# Key Value
# --- -----
# lease_id aws/creds/app-role/def456
# lease_duration 15m
# access_key AKIAI44QH8DHBEXAMPLE
# secret_key je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY
```
**TTL:** Default 15 minutes, auto-revoked on expiration.
### PKI Secrets Engine (TLS Certificates)
Issues TLS certificates with automatic renewal.
**Enable:**
```bash
vault secrets enable pki
vault secrets tune -max-lease-ttl=8760h pki
```
**Generate Root CA:**
```bash
vault write pki/root/generate/internal \
common_name=example.com \
ttl=8760h
```
**Configure URLs:**
```bash
vault write pki/config/urls \
issuing_certificates="https://vault.example.com/v1/pki/ca" \
crl_distribution_points="https://vault.example.com/v1/pki/crl"
```
**Create Role:**
```bash
vault write pki/roles/web-server \
allowed_domains=example.com \
allow_subdomains=true \
max_ttl=72h
```
**Issue Certificate:**
```bash
vault write pki/issue/web-server \
common_name=app.example.com \
ttl=24h
# Returns: certificate, issuing_ca, private_key
```
## Authentication Methods
### Kubernetes Auth
Authenticate using Kubernetes service account tokens.
**Enable:**
```bash
vault auth enable kubernetes
```
**Configure:**
```bash
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc" \
token_reviewer_jwt="<service-account-jwt>" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
```
**Create Role:**
```bash
vault write auth/kubernetes/role/app-role \
bound_service_account_names=app-sa \
bound_service_account_namespaces=production \
policies=app-policy \
ttl=1h
```
**Login (from pod):**
```bash
vault write auth/kubernetes/login \
role=app-role \
jwt=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
```
### JWT/OIDC Auth
Authenticate using external identity providers (Okta, Auth0, Google).
**Enable:**
```bash
vault auth enable oidc
```
**Configure:**
```bash
vault write auth/oidc/config \
oidc_discovery_url="https://accounts.google.com" \
oidc_client_id="<client-id>" \
oidc_client_secret="<client-secret>" \
default_role="default"
```
**Create Role:**
```bash
vault write auth/oidc/role/default \
bound_audiences="<client-id>" \
allowed_redirect_uris="https://vault.example.com/ui/vault/auth/oidc/oidc/callback" \
user_claim="email" \
policies=default
```
### AppRole Auth
Machine authentication for CI/CD, applications.
**Enable:**
```bash
vault auth enable approle
```
**Create Role:**
```bash
vault write auth/approle/role/ci-role \
secret_id_ttl=10m \
token_num_uses=10 \
token_ttl=20m \
token_max_ttl=30m \
secret_id_num_uses=40 \
policies=ci-policy
```
**Get RoleID:**
```bash
vault read auth/approle/role/ci-role/role-id
# role_id: 12345678-1234-1234-1234-123456789012
```
**Generate SecretID:**
```bash
vault write -f auth/approle/role/ci-role/secret-id
# secret_id: 87654321-4321-4321-4321-210987654321
```
**Login:**
```bash
vault write auth/approle/login \
role_id=12345678-1234-1234-1234-123456789012 \
secret_id=87654321-4321-4321-4321-210987654321
```
## Policies and Access Control
### Policy Syntax (HCL)
```hcl
# Read access to KV v2 secrets
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
# Write access to specific secret
path "secret/data/myapp/config" {
capabilities = ["create", "update", "read"]
}
# Dynamic database credentials
path "database/creds/app-role" {
capabilities = ["read"]
}
# Deny all other paths
path "*" {
capabilities = ["deny"]
}
```
**Capabilities:**
- `create`: Create new secrets
- `read`: Read existing secrets
- `update`: Update existing secrets
- `delete`: Delete secrets
- `list`: List secret paths
- `sudo`: Admin operations
- `deny`: Explicitly deny access
### Policy Templates
Dynamic policies using templates:
```hcl
# Policy for service accounts in specific namespace
path "secret/data/{{identity.entity.aliases.auth_kubernetes_abc123.metadata.service_account_namespace}}/*" {
capabilities = ["read"]
}
```
### Writing Policies
```bash
# Write policy from file
vault policy write app-policy app-policy.hcl
# Write policy inline
vault policy write readonly-policy - <<EOF
path "secret/data/*" {
capabilities = ["read", "list"]
}
EOF
# List policies
vault policy list
# Read policy
vault policy read app-policy
```
### Least Privilege Example
```hcl
# Application policy (minimal permissions)
path "secret/data/myapp/config" {
capabilities = ["read"]
}
path "database/creds/app-role" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
```
## Storage Backends
### Consul (Recommended for HA)
**Pros:**
- High availability
- Automatic failover
- Service discovery
- Health checking
**Cons:**
- Requires separate Consul cluster
- More complex setup
**Configuration:**
```hcl
storage "consul" {
address = "consul.example.com:8500"
path = "vault/"
token = "<consul-token>"
}
```
### etcd (Kubernetes Native)
**Pros:**
- Kubernetes native
- HA support
- Widely deployed
**Cons:**
- Performance at scale
- Requires etcd cluster
**Configuration:**
```hcl
storage "etcd" {
address = "https://etcd.example.com:2379"
etcd_api = "v3"
path = "vault/"
ha_enabled = "true"
}
```
### S3 (Cost-Effective)
**Pros:**
- Low cost
- Managed service
- Unlimited storage
**Cons:**
- No HA support
- Higher latency
**Configuration:**
```hcl
storage "s3" {
bucket = "vault-storage"
region = "us-east-1"
access_key = "AKIAIOSFODNN7EXAMPLE"
secret_key = "wJalrXUtnFEMI/K7MDENG"
}
```
### PostgreSQL
**Pros:**
- Relational database
- Familiar tooling
- HA with replication
**Cons:**
- Not officially recommended
- Performance tuning needed
**Configuration:**
```hcl
storage "postgresql" {
connection_url = "postgres://vault:password@postgres:5432/vault?sslmode=require"
ha_enabled = "true"
}
```
## High Availability Setup
### HA Architecture
```
┌────────────────────────────────────────────────┐
│ Vault HA Cluster (3 nodes) │
├────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────┐ │
│ │ vault-0 │ │ vault-1 │ │vault-│ │
│ │ (Active) │ │ (Standby) │ │ 2 │ │
│ │ │ │ │ │(Stand│ │
│ └──────┬───────┘ └──────┬───────┘ └──┬───┘ │
│ │ │ │ │
│ └─────────────────┼──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Storage Backend │ │
│ │ (Consul/etcd) │ │
│ └─────────────────────┘ │
│ │
└────────────────────────────────────────────────┘
```
**Key Concepts:**
- One active node (handles requests)
- Multiple standby nodes (ready for failover)
- Automatic leader election
- Storage backend coordinates HA
### Kubernetes Deployment (HA)
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: vault
spec:
serviceName: vault
replicas: 3
selector:
matchLabels:
app: vault
template:
metadata:
labels:
app: vault
spec:
containers:
- name: vault
image: hashicorp/vault:1.15
args:
- "server"
- "-config=/vault/config/vault.hcl"
ports:
- containerPort: 8200
name: api
- containerPort: 8201
name: cluster
volumeMounts:
- name: config
mountPath: /vault/config
- name: data
mountPath: /vault/data
volumes:
- name: config
configMap:
name: vault-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
```
**vault.hcl:**
```hcl
storage "raft" {
path = "/vault/data"
node_id = "vault-0"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0
tls_cert_file = "/vault/tls/tls.crt"
tls_key_file = "/vault/tls/tls.key"
}
api_addr = "https://vault-0.vault:8200"
cluster_addr = "https://vault-0.vault:8201"
ui = true
```
### Unsealing in HA
All nodes must be unsealed independently:
```bash
# Unseal each node (requires 3 of 5 key shares)
kubectl exec vault-0 -- vault operator unseal <KEY_SHARE_1>
kubectl exec vault-0 -- vault operator unseal <KEY_SHARE_2>
kubectl exec vault-0 -- vault operator unseal <KEY_SHARE_3>
kubectl exec vault-1 -- vault operator unseal <KEY_SHARE_1>
kubectl exec vault-1 -- vault operator unseal <KEY_SHARE_2>
kubectl exec vault-1 -- vault operator unseal <KEY_SHARE_3>
kubectl exec vault-2 -- vault operator unseal <KEY_SHARE_1>
kubectl exec vault-2 -- vault operator unseal <KEY_SHARE_2>
kubectl exec vault-2 -- vault operator unseal <KEY_SHARE_3>
```
**Auto-Unseal (Recommended for Production):**
Using cloud KMS (AWS, GCP, Azure):
```hcl
seal "awskms" {
region = "us-east-1"
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abc-def-ghi"
}
```
## Audit Logging
### Enable Audit Device
```bash
# File audit device
vault audit enable file file_path=/vault/logs/audit.log
# Syslog audit device
vault audit enable syslog tag="vault" facility="AUTH"
# Socket audit device
vault audit enable socket address="logstash.example.com:9090" socket_type="tcp"
```
### Audit Log Format
JSON format with request and response details:
```json
{
"time": "2025-12-03T10:15:30Z",
"type": "response",
"auth": {
"client_token": "hmac-sha256:abc123...",
"accessor": "hmac-sha256:def456...",
"display_name": "kubernetes-production-app-sa",
"policies": ["app-policy", "default"]
},
"request": {
"operation": "read",
"path": "database/creds/app-role",
"remote_address": "10.244.1.5"
},
"response": {
"secret": {
"lease_id": "database/creds/app-role/xyz789"
}
}
}
```
**Logged Information:**
- Authentication details
- Request path and operation
- Response (secrets are HMAC-hashed)
- Remote IP address
- Timestamp
## Production Deployment
### Checklist
- [ ] **TLS enabled** (never run Vault without TLS in production)
- [ ] **HA setup** (3+ nodes with Consul/etcd/Raft storage)
- [ ] **Auto-unseal** (AWS KMS, GCP KMS, Azure Key Vault)
- [ ] **Audit logging** (multiple devices for redundancy)
- [ ] **Backup strategy** (storage backend snapshots)
- [ ] **Monitoring** (Prometheus metrics, health checks)
- [ ] **Policies reviewed** (least privilege, no root tokens)
- [ ] **Secrets rotation** (quarterly for static, automatic for dynamic)
- [ ] **Disaster recovery** (tested restore procedure)
### Resource Requirements
**Development:**
- 1 CPU, 512 MB RAM
- Single node
- File storage backend
**Production (Small - <1000 secrets):**
- 2 CPU, 2 GB RAM per node
- 3 nodes (HA)
- Consul/etcd storage
**Production (Large - >10,000 secrets):**
- 4 CPU, 8 GB RAM per node
- 5+ nodes (HA)
- Dedicated storage cluster
- Load balancer
### Monitoring
**Prometheus Metrics:**
```yaml
# vault.hcl
telemetry {
prometheus_retention_time = "30s"
disable_hostname = true
}
```
**Key Metrics:**
- `vault_core_unsealed`: Vault seal status (1 = unsealed)
- `vault_core_active`: Active node (1 = active, 0 = standby)
- `vault_token_count`: Total active tokens
- `vault_secret_lease_creation`: Secret lease generation rate
- `vault_runtime_alloc_bytes`: Memory usage
## Common Pitfalls
### Pitfall 1: Running Without TLS
**Problem:** Secrets transmitted in plaintext.
**Solution:**
```hcl
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0 # NEVER set to 1 in production
tls_cert_file = "/vault/tls/tls.crt"
tls_key_file = "/vault/tls/tls.key"
}
```
### Pitfall 2: Using Root Token
**Problem:** Unlimited permissions, no audit trail.
**Solution:** Delete root token after initial setup, use auth methods.
```bash
# Revoke root token
vault token revoke <ROOT_TOKEN>
```
### Pitfall 3: No Backup Strategy
**Problem:** Data loss on storage backend failure.
**Solution:** Regular snapshots of storage backend.
```bash
# Consul snapshot
consul snapshot save backup.snap
# etcd snapshot
ETCDCTL_API=3 etcdctl snapshot save backup.db
```
### Pitfall 4: Unsealing Delays
**Problem:** Manual unsealing after restart delays recovery.
**Solution:** Use auto-unseal with cloud KMS.
### Pitfall 5: Single Audit Device
**Problem:** Audit log failure blocks all requests.
**Solution:** Configure multiple audit devices (file + syslog).
```bash
vault audit enable file file_path=/vault/logs/audit.log
vault audit enable syslog tag="vault"
```
### Pitfall 6: No Lease Renewal
**Problem:** Dynamic secrets expire, causing app failures.
**Solution:** Implement lease renewal in application code.
```python
import time
import threading
def renew_lease(client, lease_id, lease_duration):
renewal_time = lease_duration * 0.67 # Renew at 67% of TTL
time.sleep(renewal_time)
client.sys.renew_lease(lease_id)
```
### Pitfall 7: Hardcoded Policies in Code
**Problem:** Policy changes require code deployment.
**Solution:** Store policies as files, reference in Vault.
```bash
vault policy write app-policy /path/to/app-policy.hcl
```
```
### references/kubernetes-integration.md
```markdown
# Kubernetes Secret Integration
Comprehensive guide to integrating secret management with Kubernetes using External Secrets Operator, Secrets Store CSI Driver, and Vault Secrets Operator.
## Table of Contents
1. [Integration Approaches](#integration-approaches)
2. [External Secrets Operator (ESO)](#external-secrets-operator-eso)
3. [Secrets Store CSI Driver](#secrets-store-csi-driver)
4. [Vault Secrets Operator (VSO)](#vault-secrets-operator-vso)
5. [Comparison Matrix](#comparison-matrix)
6. [Best Practices](#best-practices)
## Integration Approaches
### Native Kubernetes Secrets (Baseline)
```yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # base64("admin")
password: cGFzc3dvcmQ= # base64("password")
```
**Problems:**
- Base64 is NOT encryption
- Secrets stored in etcd (encrypted at rest if enabled)
- Manual rotation required
- No audit trail
- Secrets in Git (if committed)
**Solution:** Use external secret stores + sync operators.
### External Secret Stores
```
┌─────────────────────────────────────────────────────┐
│ External Secret Store Integrations │
├─────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────┐ │
│ │ External Secrets Operator (ESO) │ │
│ │ ├── Syncs from 30+ providers │ │
│ │ ├── Creates Kubernetes Secrets │ │
│ │ └── Polling-based refresh │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Secrets Store CSI Driver │ │
│ │ ├── Mounts secrets as files │ │
│ │ ├── No Kubernetes Secret creation │ │
│ │ └── Watch-based refresh │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Vault Secrets Operator (VSO) │ │
│ │ ├── Vault-specific CRDs │ │
│ │ ├── Dynamic secret renewal │ │
│ │ └─ Kubernetes-native experience │ │
│ └────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
```
## External Secrets Operator (ESO)
### Installation
```bash
# Helm installation
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets-system \
--create-namespace
# Verify installation
kubectl get pods -n external-secrets-system
```
### Core Resources
**SecretStore** (Namespace-scoped)
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "app-role"
serviceAccountRef:
name: app-sa
```
**ClusterSecretStore** (Cluster-wide)
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-global
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "cluster-admin-role"
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets-system
```
**ExternalSecret**
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: production
spec:
refreshInterval: 1h # Poll every hour
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials # Kubernetes Secret name
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: secret/data/database/config
property: username
- secretKey: password
remoteRef:
key: secret/data/database/config
property: password
```
### Multi-Provider Examples
**AWS Secrets Manager**
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: app-sa
```
**GCP Secret Manager**
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: gcp-secrets
spec:
provider:
gcpsm:
projectID: "my-project-123"
auth:
workloadIdentity:
clusterLocation: us-central1
clusterName: production-cluster
serviceAccountRef:
name: app-sa
```
**Azure Key Vault**
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: azure-secrets
spec:
provider:
azurekv:
vaultUrl: "https://my-vault.vault.azure.net"
authType: WorkloadIdentity
serviceAccountRef:
name: app-sa
```
### Advanced Features
**Secret Templating**
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-config
spec:
secretStoreRef:
name: vault-backend
target:
name: app-config
template:
type: Opaque
data:
config.yaml: |
database:
url: postgresql://{{ .username }}:{{ .password }}@postgres:5432/mydb
api_key: {{ .api_key }}
data:
- secretKey: username
remoteRef:
key: secret/data/database/config
property: username
- secretKey: password
remoteRef:
key: secret/data/database/config
property: password
- secretKey: api_key
remoteRef:
key: secret/data/api/keys
property: stripe_key
```
**DataFrom (Fetch All Keys)**
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: all-secrets
spec:
secretStoreRef:
name: vault-backend
target:
name: all-secrets
dataFrom:
- extract:
key: secret/data/myapp/config # Fetch all keys from this path
```
**PushSecret (Sync TO External Store)**
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-credentials
spec:
refreshInterval: 10m
secretStoreRefs:
- name: vault-backend
kind: SecretStore
selector:
secret:
name: local-secret # Kubernetes Secret to push
data:
- match:
secretKey: username
remoteRef:
remoteKey: secret/data/synced/credentials
property: username
```
## Secrets Store CSI Driver
### Installation
```bash
# Install CSI driver
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system
# Install Vault provider
kubectl apply -f https://raw.githubusercontent.com/hashicorp/vault-csi-provider/main/deployment/vault-csi-provider.yaml
```
### SecretProviderClass (Vault)
```yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-database
namespace: production
spec:
provider: vault
parameters:
vaultAddress: "https://vault.example.com"
roleName: "app-role"
vaultSkipTLSVerify: "false"
objects: |
- objectName: "db-username"
secretPath: "secret/data/database/config"
secretKey: "username"
- objectName: "db-password"
secretPath: "secret/data/database/config"
secretKey: "password"
```
### Pod with CSI Volume
```yaml
apiVersion: v1
kind: Pod
metadata:
name: app
namespace: production
spec:
serviceAccountName: app-sa
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets"
readOnly: true
env:
- name: DB_USERNAME_FILE
value: /mnt/secrets/db-username
- name: DB_PASSWORD_FILE
value: /mnt/secrets/db-password
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "vault-database"
```
### Sync to Kubernetes Secret (Optional)
```yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-database
spec:
provider: vault
secretObjects:
- secretName: db-credentials
type: Opaque
data:
- objectName: db-username
key: username
- objectName: db-password
key: password
parameters:
vaultAddress: "https://vault.example.com"
roleName: "app-role"
objects: |
- objectName: "db-username"
secretPath: "secret/data/database/config"
secretKey: "username"
- objectName: "db-password"
secretPath: "secret/data/database/config"
secretKey: "password"
```
### Auto-Rotation Detection
Application code to watch for file changes:
```python
import time
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class SecretReloader(FileSystemEventHandler):
def on_modified(self, event):
if event.src_path == "/mnt/secrets/db-password":
print("Secret rotated, reloading database connection...")
reload_database_connection()
observer = Observer()
observer.schedule(SecretReloader(), "/mnt/secrets", recursive=False)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
```
## Vault Secrets Operator (VSO)
### Installation
```bash
# Helm installation
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
--namespace vault-secrets-operator-system \
--create-namespace
```
### VaultConnection
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: vault-connection
namespace: production
spec:
address: "https://vault.example.com:8200"
skipTLSVerify: false
caCertSecretRef: vault-ca-cert
```
### VaultAuth
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: vault-auth
namespace: production
spec:
vaultConnectionRef: vault-connection
method: kubernetes
mount: kubernetes
kubernetes:
role: app-role
serviceAccount: app-sa
```
### VaultStaticSecret (KV v2)
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: api-keys
namespace: production
spec:
vaultAuthRef: vault-auth
mount: secret
path: myapp/api-keys
type: kv-v2
refreshAfter: 1h
destination:
create: true
name: api-keys
```
### VaultDynamicSecret (Database)
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: postgres-creds
namespace: production
spec:
vaultAuthRef: vault-auth
mount: database
path: creds/postgres-role
renewalPercent: 67 # Renew at 67% of TTL
destination:
create: true
name: dynamic-db-creds
rolloutRestartTargets:
- kind: Deployment
name: app
```
**Automatic Renewal:**
- VSO renews lease at 67% of TTL
- On renewal failure, requests new credentials
- Optionally triggers pod restart (rolloutRestartTargets)
### VaultPKISecret (TLS Certificates)
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: app-tls
namespace: production
spec:
vaultAuthRef: vault-auth
mount: pki
role: web-server
commonName: app.example.com
altNames:
- www.app.example.com
ttl: 24h
destination:
create: true
name: app-tls-cert
rolloutRestartTargets:
- kind: Deployment
name: app
```
## Comparison Matrix
| Feature | ESO | CSI Driver | VSO |
|---------|-----|------------|-----|
| **Multi-Provider** | Yes (30+) | Yes (AWS, GCP, Azure, Vault) | No (Vault only) |
| **Secret Type** | Kubernetes Secret | Files (volume mount) | Kubernetes Secret |
| **Rotation** | Polling (refresh interval) | Watch (inotify) | Lease renewal (automatic) |
| **Dynamic Secrets** | Limited | Yes | Yes (native support) |
| **Pod Restart** | Manual | Manual | Automatic (optional) |
| **Complexity** | Low | Medium | Low (for Vault users) |
| **Performance** | Polling overhead | Low overhead | Low overhead |
| **Maturity** | High (CNCF) | High (Kubernetes SIG) | Medium (HashiCorp) |
### When to Use Each
**External Secrets Operator (ESO):**
- Multi-cloud environments
- Need to support multiple secret stores
- Static secrets with hourly refresh acceptable
- Team familiar with Kubernetes operators
**Secrets Store CSI Driver:**
- Need file-based secret delivery
- Automatic rotation without pod restart
- TLS certificates (frequent rotation)
- Applications that watch files for changes
**Vault Secrets Operator (VSO):**
- Vault-centric infrastructure
- Dynamic secrets (database, cloud)
- Automatic lease renewal required
- Prefer Kubernetes-native CRDs
## Best Practices
### 1. Namespace Isolation
Use namespace-scoped SecretStores:
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production # Isolated to production namespace
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret/production" # Namespace-specific path
auth:
kubernetes:
role: "production-app-role"
serviceAccountRef:
name: app-sa
```
### 2. Least Privilege Service Accounts
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-secret-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["db-credentials"] # Specific secret only
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-secret-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app-sa
roleRef:
kind: Role
name: app-secret-reader
apiGroup: rbac.authorization.k8s.io
```
### 3. Refresh Interval Tuning
```yaml
# Frequent rotation (TLS certs)
refreshInterval: 10m
# Moderate rotation (API keys)
refreshInterval: 1h
# Infrequent rotation (static configs)
refreshInterval: 6h
```
### 4. Monitoring and Alerting
**Prometheus Metrics (ESO):**
```yaml
- external_secrets_sync_calls_total
- external_secrets_sync_calls_error
- external_secrets_status_condition
```
**Alerts:**
```yaml
- alert: ExternalSecretSyncFailure
expr: external_secrets_sync_calls_error > 0
for: 5m
annotations:
summary: "ExternalSecret sync failing"
```
### 5. Encryption at Rest (etcd)
Enable etcd encryption for Kubernetes Secrets:
```yaml
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64_ENCODED_SECRET>
- identity: {}
```
```yaml
# kube-apiserver flag
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
```
### 6. Secret Versioning
Track secret versions for rollback:
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: versioned-secret
annotations:
external-secrets.io/secret-version: "v2"
spec:
secretStoreRef:
name: vault-backend
target:
name: app-config
data:
- secretKey: api_key
remoteRef:
key: secret/data/api-keys
version: 2 # Specific version
```
### 7. Testing Secret Rotation
```bash
# Update secret in Vault
vault kv put secret/myapp/config api_key=new_key_v2
# Wait for refresh interval
sleep 3600
# Verify Kubernetes Secret updated
kubectl get secret db-credentials -o jsonpath='{.data.api_key}' | base64 -d
# Check pod logs for reload
kubectl logs -f deployment/app
```
### 8. Disaster Recovery
Backup SecretStore configurations:
```bash
# Export all ExternalSecrets
kubectl get externalsecrets -A -o yaml > externalsecrets-backup.yaml
# Export all SecretStores
kubectl get secretstores -A -o yaml > secretstores-backup.yaml
# Restore
kubectl apply -f externalsecrets-backup.yaml
kubectl apply -f secretstores-backup.yaml
```
```
### references/rotation-patterns.md
```markdown
# Secret Rotation Patterns
Detailed workflows for rotating static secrets, dynamic credentials, and TLS certificates.
## Table of Contents
1. [Why Rotate Secrets](#why-rotate-secrets)
2. [Versioned Static Secrets](#versioned-static-secrets)
3. [Dynamic Database Credentials](#dynamic-database-credentials)
4. [TLS Certificate Rotation](#tls-certificate-rotation)
5. [Cloud Provider Credentials](#cloud-provider-credentials)
6. [Automation Scripts](#automation-scripts)
## Why Rotate Secrets
**Security Benefits:**
- Limits blast radius of compromised credentials
- Reduces window of unauthorized access
- Meets compliance requirements (SOC 2, ISO 27001, PCI DSS)
- Detects dormant credential usage
**Industry Standards:**
- **PCI DSS**: Rotate passwords every 90 days
- **SOC 2**: Document rotation policy and evidence
- **ISO 27001**: Regular credential review and rotation
- **NIST**: Recommend rotation on suspicion of compromise
## Versioned Static Secrets
### Pattern: Blue/Green Rotation
For third-party API keys with no auto-rotation support.
**Steps:**
1. **Create New Secret Version**
```bash
# Vault KV v2 (versioned)
vault kv put secret/api-keys/stripe \
key=sk_live_NEW_KEY_v2 \
created_at="2025-12-03T10:00:00Z" \
rotated_by="ops-team"
# Verify version created
vault kv metadata get secret/api-keys/stripe
# current_version: 2
```
2. **Update Staging Environment**
```bash
# Update Kubernetes Secret in staging
kubectl set env deployment/api-service -n staging \
STRIPE_API_KEY=sk_live_NEW_KEY_v2
# Or update ExternalSecret to fetch version 2
kubectl patch externalsecret stripe-key -n staging --type merge -p '
spec:
data:
- secretKey: key
remoteRef:
key: secret/data/api-keys/stripe
property: key
version: 2
'
```
3. **Monitor for Errors (24-48 Hours)**
```bash
# Check application logs
kubectl logs -f deployment/api-service -n staging | grep -i "stripe\|error"
# Monitor error rates
curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~\"5..\"}[5m])"
# Verify API calls succeed
curl https://api.stripe.com/v1/charges \
-u sk_live_NEW_KEY_v2: \
-d amount=100 \
-d currency=usd \
-d source=tok_test
```
4. **Gradual Production Rollout**
```bash
# Update 10% of pods
kubectl patch deployment api-service -n production -p '
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
'
kubectl set env deployment/api-service -n production STRIPE_API_KEY=sk_live_NEW_KEY_v2
# Wait 1 hour, check metrics
sleep 3600
kubectl top pods -n production -l app=api-service
# Update remaining pods
kubectl rollout status deployment/api-service -n production
```
5. **Revoke Old Secret (After 7 Days)**
```bash
# Revoke at provider
# Stripe: Dashboard → API Keys → Revoke old key
# Delete old version from Vault
vault kv metadata delete secret/api-keys/stripe -versions=1
# Document rotation
vault kv put secret/rotation-log/stripe-2025-12 \
old_key=sk_live_OLD_KEY_v1 \
new_key=sk_live_NEW_KEY_v2 \
rotated_at="2025-12-03T10:00:00Z" \
revoked_at="2025-12-10T10:00:00Z"
```
### Automation Script
```bash
#!/bin/bash
# rotate-static-secret.sh
SECRET_PATH=$1 # e.g., "secret/api-keys/stripe"
NEW_VALUE=$2
echo "Rotating secret: $SECRET_PATH"
# Write new version
vault kv put "$SECRET_PATH" \
key="$NEW_VALUE" \
created_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
rotated_by="$(whoami)"
# Get new version number
NEW_VERSION=$(vault kv metadata get -format=json "$SECRET_PATH" | jq -r '.current_version')
echo "New version: $NEW_VERSION"
# Update staging
kubectl set env deployment/api-service -n staging \
API_KEY="$NEW_VALUE"
echo "Secret rotated. Monitor staging for 24-48 hours before production rollout."
```
## Dynamic Database Credentials
### Pattern: Automatic Lease Renewal
Vault auto-generates credentials with short TTL (1 hour).
**Initial Setup:**
```bash
# 1. Enable database engine
vault secrets enable database
# 2. Configure PostgreSQL connection
vault write database/config/postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="app-role" \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/mydb?sslmode=require" \
username="vault-admin" \
password="vault-admin-password"
# 3. Create role with TTL
vault write database/roles/app-role \
db_name=postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
```
**Application Integration (Python):**
```python
import hvac
import time
import threading
from sqlalchemy import create_engine
from sqlalchemy.pool import NullPool
class VaultDatabaseClient:
def __init__(self, vault_url, role):
self.client = hvac.Client(url=vault_url)
self.role = role
self.lease_id = None
self.engine = None
# Authenticate
with open('/var/run/secrets/kubernetes.io/serviceaccount/token') as f:
jwt = f.read()
self.client.auth.kubernetes(role='app-role', jwt=jwt)
# Get initial credentials
self._refresh_credentials()
# Start renewal thread
threading.Thread(target=self._renewal_loop, daemon=True).start()
def _refresh_credentials(self):
response = self.client.secrets.database.generate_credentials(name=self.role)
username = response['data']['username']
password = response['data']['password']
self.lease_id = response['lease_id']
self.lease_duration = response['lease_duration']
# Create new database engine
if self.engine:
self.engine.dispose()
self.engine = create_engine(
f"postgresql://{username}:{password}@postgres:5432/mydb",
poolclass=NullPool # Don't pool connections (credentials rotate)
)
print(f"Credentials refreshed. Lease ID: {self.lease_id}, TTL: {self.lease_duration}s")
def _renewal_loop(self):
while True:
# Renew at 67% of lease duration
renewal_time = self.lease_duration * 0.67
time.sleep(renewal_time)
try:
# Attempt renewal
self.client.sys.renew_lease(self.lease_id)
print(f"Lease renewed: {self.lease_id}")
except Exception as e:
print(f"Renewal failed: {e}. Requesting new credentials...")
self._refresh_credentials()
def query(self, sql):
with self.engine.connect() as conn:
return conn.execute(sql).fetchall()
# Usage
db_client = VaultDatabaseClient('https://vault.example.com', 'app-role')
users = db_client.query("SELECT * FROM users")
```
**Kubernetes with VSO:**
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: postgres-creds
namespace: production
spec:
vaultAuthRef: vault-auth
mount: database
path: creds/app-role
renewalPercent: 67 # Renew at 67% of TTL
destination:
create: true
name: dynamic-db-creds
rolloutRestartTargets:
- kind: Deployment
name: app # Auto-restart on credential change
```
**Monitoring:**
```bash
# Check active leases
vault list sys/leases/lookup/database/creds/app-role
# View lease details
vault lease lookup database/creds/app-role/<LEASE_ID>
# Force revoke (emergency)
vault lease revoke database/creds/app-role/<LEASE_ID>
# Revoke all leases for role
vault lease revoke -prefix database/creds/app-role
```
## TLS Certificate Rotation
### Pattern: cert-manager + Vault PKI
Automatic certificate issuance and renewal.
**Vault PKI Setup:**
```bash
# 1. Enable PKI engine
vault secrets enable pki
vault secrets tune -max-lease-ttl=8760h pki
# 2. Generate root CA
vault write pki/root/generate/internal \
common_name=example.com \
ttl=8760h
# 3. Configure URLs
vault write pki/config/urls \
issuing_certificates="https://vault.example.com/v1/pki/ca" \
crl_distribution_points="https://vault.example.com/v1/pki/crl"
# 4. Create role
vault write pki/roles/web-server \
allowed_domains=example.com \
allow_subdomains=true \
max_ttl=72h \
key_type=rsa \
key_bits=2048
```
**cert-manager Integration:**
```yaml
# 1. Issuer (Vault-backed)
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: vault-issuer
namespace: production
spec:
vault:
server: https://vault.example.com
path: pki/sign/web-server
auth:
kubernetes:
role: cert-manager
mountPath: /v1/auth/kubernetes
secretRef:
name: cert-manager-vault-token
key: token
---
# 2. Certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-tls
namespace: production
spec:
secretName: app-tls-secret
duration: 24h
renewBefore: 8h # Renew 8 hours before expiration (67% of 24h)
issuerRef:
name: vault-issuer
dnsNames:
- app.example.com
- www.app.example.com
```
**Automatic Renewal:**
cert-manager automatically:
1. Monitors certificate expiration
2. Requests new certificate 8 hours before expiry
3. Updates Kubernetes Secret (app-tls-secret)
4. Triggers pod reload (via Reloader or similar)
**Pod Reload with Reloader:**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
annotations:
reloader.stakater.com/auto: "true" # Auto-reload on Secret change
spec:
template:
spec:
containers:
- name: app
volumeMounts:
- name: tls
mountPath: /etc/tls
volumes:
- name: tls
secret:
secretName: app-tls-secret
```
## Cloud Provider Credentials
### Pattern: AWS IAM with Vault
**Vault AWS Engine Setup:**
```bash
# 1. Enable AWS engine
vault secrets enable aws
# 2. Configure root credentials
vault write aws/config/root \
access_key=AKIAIOSFODNN7EXAMPLE \
secret_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
region=us-east-1
# 3. Create role with inline policy
vault write aws/roles/s3-access \
credential_type=iam_user \
policy_document=-<<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}
EOF
default_ttl=15m \
max_ttl=1h
```
**Application Integration:**
```python
import boto3
import hvac
import time
import threading
class VaultAWSClient:
def __init__(self, vault_url, aws_role):
self.client = hvac.Client(url=vault_url)
self.aws_role = aws_role
self.lease_id = None
# Authenticate with Vault
with open('/var/run/secrets/kubernetes.io/serviceaccount/token') as f:
jwt = f.read()
self.client.auth.kubernetes(role='app-role', jwt=jwt)
# Get initial AWS credentials
self._refresh_credentials()
# Start renewal thread
threading.Thread(target=self._renewal_loop, daemon=True).start()
def _refresh_credentials(self):
response = self.client.secrets.aws.generate_credentials(name=self.aws_role)
self.access_key = response['data']['access_key']
self.secret_key = response['data']['secret_key']
self.lease_id = response['lease_id']
self.lease_duration = response['lease_duration']
# Update boto3 session
self.session = boto3.Session(
aws_access_key_id=self.access_key,
aws_secret_access_key=self.secret_key
)
self.s3 = self.session.client('s3')
print(f"AWS credentials refreshed. Lease: {self.lease_id}, TTL: {self.lease_duration}s")
def _renewal_loop(self):
while True:
renewal_time = self.lease_duration * 0.67
time.sleep(renewal_time)
try:
self.client.sys.renew_lease(self.lease_id)
print(f"Lease renewed: {self.lease_id}")
except Exception as e:
print(f"Renewal failed. Requesting new credentials...")
self._refresh_credentials()
def upload_file(self, bucket, key, file_path):
self.s3.upload_file(file_path, bucket, key)
# Usage
aws_client = VaultAWSClient('https://vault.example.com', 's3-access')
aws_client.upload_file('my-bucket', 'data.csv', '/tmp/data.csv')
```
## Automation Scripts
### Comprehensive Rotation Script
```python
#!/usr/bin/env python3
"""
rotate_secrets.py
Automated secret rotation script for multiple secret types.
Usage:
python rotate_secrets.py --type static --path secret/api-keys/stripe --value sk_live_NEW
python rotate_secrets.py --type database --role app-role --ttl 1h
"""
import argparse
import hvac
import time
from datetime import datetime
class SecretRotator:
def __init__(self, vault_url, vault_token):
self.client = hvac.Client(url=vault_url, token=vault_token)
def rotate_static(self, path, new_value):
"""Rotate static secret with versioning."""
print(f"Rotating static secret: {path}")
# Write new version
self.client.secrets.kv.v2.create_or_update_secret(
path=path.replace('secret/data/', '').replace('secret/', ''),
secret={'key': new_value},
mount_point='secret'
)
# Get metadata
metadata = self.client.secrets.kv.v2.read_secret_metadata(
path=path.replace('secret/data/', '').replace('secret/', ''),
mount_point='secret'
)
version = metadata['data']['current_version']
print(f"✓ New version created: {version}")
print(f"✓ Previous version available for rollback")
return version
def rotate_database(self, role, ttl='1h'):
"""Generate new dynamic database credentials."""
print(f"Generating new database credentials for role: {role}")
response = self.client.secrets.database.generate_credentials(name=role)
print(f"✓ Username: {response['data']['username']}")
print(f"✓ Lease ID: {response['lease_id']}")
print(f"✓ TTL: {response['lease_duration']}s")
return response
def revoke_old_version(self, path, versions_to_keep=2):
"""Delete old versions of static secret."""
print(f"Cleaning up old versions: {path}")
metadata = self.client.secrets.kv.v2.read_secret_metadata(
path=path.replace('secret/data/', '').replace('secret/', ''),
mount_point='secret'
)
current_version = metadata['data']['current_version']
versions_to_delete = list(range(1, current_version - versions_to_keep + 1))
if versions_to_delete:
self.client.secrets.kv.v2.delete_secret_versions(
path=path.replace('secret/data/', '').replace('secret/', ''),
versions=versions_to_delete,
mount_point='secret'
)
print(f"✓ Deleted versions: {versions_to_delete}")
else:
print("✓ No old versions to delete")
def main():
parser = argparse.ArgumentParser(description='Rotate secrets in Vault')
parser.add_argument('--type', required=True, choices=['static', 'database'])
parser.add_argument('--path', help='Secret path (for static secrets)')
parser.add_argument('--value', help='New secret value (for static secrets)')
parser.add_argument('--role', help='Database role name (for dynamic secrets)')
parser.add_argument('--ttl', default='1h', help='TTL for dynamic secrets')
parser.add_argument('--vault-url', default='https://vault.example.com')
parser.add_argument('--vault-token', required=True)
args = parser.parse_args()
rotator = SecretRotator(args.vault_url, args.vault_token)
if args.type == 'static':
if not args.path or not args.value:
parser.error("--path and --value required for static secrets")
rotator.rotate_static(args.path, args.value)
# Wait 7 days before cleanup (manual trigger)
print("\nRun the following command in 7 days to clean up old versions:")
print(f" python rotate_secrets.py --type cleanup --path {args.path} --vault-token <TOKEN>")
elif args.type == 'database':
if not args.role:
parser.error("--role required for database secrets")
rotator.rotate_database(args.role, args.ttl)
if __name__ == '__main__':
main()
```
### Validation Script
```bash
#!/bin/bash
# validate_rotation.sh
set -e
SECRET_PATH=$1
EXPECTED_VERSION=$2
echo "Validating secret rotation: $SECRET_PATH"
# Check Vault
CURRENT_VERSION=$(vault kv metadata get -format=json "$SECRET_PATH" | jq -r '.current_version')
if [ "$CURRENT_VERSION" -eq "$EXPECTED_VERSION" ]; then
echo "✓ Vault version correct: $CURRENT_VERSION"
else
echo "✗ Vault version mismatch. Expected: $EXPECTED_VERSION, Got: $CURRENT_VERSION"
exit 1
fi
# Check Kubernetes Secret (if using ESO)
K8S_SECRET_NAME=$(echo "$SECRET_PATH" | sed 's/\//-/g')
K8S_VERSION=$(kubectl get secret "$K8S_SECRET_NAME" -o jsonpath='{.metadata.annotations.external-secrets\.io/secret-version}')
if [ "$K8S_VERSION" -eq "$EXPECTED_VERSION" ]; then
echo "✓ Kubernetes Secret synced: $K8S_VERSION"
else
echo "✗ Kubernetes Secret not synced. Expected: $EXPECTED_VERSION, Got: $K8S_VERSION"
exit 1
fi
echo "✓ Rotation validated successfully"
```
```
### references/secret-scanning.md
```markdown
# Secret Scanning and Remediation
Comprehensive guide to detecting, preventing, and remediating leaked secrets using Gitleaks and other tools.
## Table of Contents
1. [Why Scan for Secrets](#why-scan-for-secrets)
2. [Gitleaks](#gitleaks)
3. [Pre-Commit Hooks](#pre-commit-hooks)
4. [CI/CD Integration](#cicd-integration)
5. [Remediation Workflow](#remediation-workflow)
6. [Alternative Tools](#alternative-tools)
## Why Scan for Secrets
**The Problem:**
- 10M+ secrets exposed on GitHub in 2024 (GitGuardian)
- Average breach cost: $4.45M (IBM 2025)
- 63% of breaches from leaked credentials (Verizon DBIR)
- 95% preventable with secret scanning
**Common Leak Sources:**
- Git commits (hardcoded passwords, API keys)
- Environment files (.env, config.json)
- Configuration files (application.yml, settings.py)
- CI/CD logs (exposed secrets in build output)
- Docker images (secrets baked into layers)
## Gitleaks
High-performance secret scanner with customizable rules.
### Installation
```bash
# macOS
brew install gitleaks
# Linux
curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/scripts/install.sh | sh
# Docker
docker pull ghcr.io/gitleaks/gitleaks:latest
```
### Basic Usage
```bash
# Scan current repository
gitleaks detect --verbose
# Scan specific directory
gitleaks detect --source /path/to/repo --verbose
# Scan Git history
gitleaks detect --log-opts "--all" --verbose
# Protect mode (scan uncommitted changes)
gitleaks protect --staged --verbose
# Generate report
gitleaks detect --report-format json --report-path gitleaks-report.json
```
### Configuration (.gitleaks.toml)
```toml
title = "Gitleaks Configuration"
[extend]
useDefault = true
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(api[_-]?key|apikey)['"` ]*[:=]['"` ]*[a-zA-Z0-9]{20,}'''
tags = ["key", "API"]
[[rules]]
id = "aws-access-key"
description = "AWS Access Key"
regex = '''AKIA[0-9A-Z]{16}'''
tags = ["AWS", "key"]
[[rules]]
id = "private-key"
description = "Private Key"
regex = '''-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'''
tags = ["key", "private"]
[[rules]]
id = "jwt"
description = "JSON Web Token"
regex = '''eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'''
tags = ["JWT"]
[allowlist]
description = "Allowlist for test files and examples"
paths = [
'''.*/test/.*''',
'''.*/examples/.*''',
'''.*_test\.go''',
'''.*\.md'''
]
regexes = [
'''sk_test_''', # Stripe test keys
'''pk_test_''', # Stripe test keys
'''EXAMPLE_API_KEY''', # Placeholder examples
]
```
### Exit Codes
```bash
gitleaks detect
echo $?
# 0: No secrets found
# 1: Secrets detected
# 2: Error occurred
```
## Pre-Commit Hooks
### Install pre-commit
```bash
# Install pre-commit framework
pip install pre-commit
# Or via Homebrew
brew install pre-commit
```
### .pre-commit-config.yaml
```yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.1
hooks:
- id: gitleaks
name: Gitleaks
description: Detect secrets in staged files
entry: gitleaks protect --staged --redact
language: system
pass_filenames: false
```
### Manual Hook (.git/hooks/pre-commit)
```bash
#!/bin/bash
# .git/hooks/pre-commit
echo "Running Gitleaks secret scan..."
# Run Gitleaks on staged files
gitleaks protect --staged --verbose --redact
if [ $? -ne 0 ]; then
echo ""
echo "❌ Secret detected! Commit blocked."
echo "To fix:"
echo " 1. Remove the secret from your code"
echo " 2. Store it in Vault: vault kv put secret/myapp/config api_key=<YOUR_KEY>"
echo " 3. Reference it in code: vault.read('secret/data/myapp/config')"
echo ""
echo "To bypass (NOT recommended): git commit --no-verify"
exit 1
fi
echo "✅ No secrets detected. Proceeding with commit."
exit 0
```
**Install hook:**
```bash
chmod +x .git/hooks/pre-commit
```
### Husky (for Node.js projects)
```json
{
"husky": {
"hooks": {
"pre-commit": "gitleaks protect --staged --verbose"
}
}
}
```
```bash
npx husky install
npx husky add .husky/pre-commit "gitleaks protect --staged --verbose"
```
## CI/CD Integration
### GitHub Actions
```yaml
# .github/workflows/secret-scan.yml
name: Secret Scanning
on:
push:
branches: [main, develop]
pull_request:
jobs:
gitleaks:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for comprehensive scan
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # For commercial use
- name: Upload results (if secrets found)
if: failure()
uses: actions/upload-artifact@v4
with:
name: gitleaks-report
path: gitleaks-report.sarif
```
### GitLab CI
```yaml
# .gitlab-ci.yml
gitleaks:
stage: test
image: ghcr.io/gitleaks/gitleaks:latest
script:
- gitleaks detect --verbose --report-format json --report-path gitleaks-report.json
artifacts:
when: on_failure
paths:
- gitleaks-report.json
allow_failure: false
```
### Jenkins
```groovy
pipeline {
agent any
stages {
stage('Secret Scan') {
steps {
sh 'gitleaks detect --verbose --report-format json --report-path gitleaks-report.json'
}
}
}
post {
always {
archiveArtifacts artifacts: 'gitleaks-report.json', allowEmptyArchive: true
}
}
}
```
### CircleCI
```yaml
# .circleci/config.yml
version: 2.1
jobs:
gitleaks:
docker:
- image: ghcr.io/gitleaks/gitleaks:latest
steps:
- checkout
- run:
name: Run Gitleaks
command: gitleaks detect --verbose
- store_artifacts:
path: gitleaks-report.json
workflows:
version: 2
build:
jobs:
- gitleaks
```
## Remediation Workflow
### When a Secret is Leaked
**Immediate Actions (within 1 hour):**
1. **Rotate the Exposed Secret**
```bash
# PRIORITY 1: Create new secret in Vault
vault kv put secret/api-keys/stripe \
key=sk_live_NEW_KEY_AFTER_LEAK \
rotated_reason="Leaked to Git on 2025-12-03" \
previous_key=sk_live_OLD_KEY_LEAKED
```
2. **Revoke at Provider**
```bash
# Stripe: Dashboard → API Keys → Revoke
# AWS: IAM → Delete access key
# GitHub: Settings → Developer settings → Revoke token
```
3. **Update Applications**
```bash
# Update all environments immediately
kubectl set env deployment/api-service -n staging STRIPE_API_KEY=sk_live_NEW_KEY_AFTER_LEAK
kubectl set env deployment/api-service -n production STRIPE_API_KEY=sk_live_NEW_KEY_AFTER_LEAK
```
**Git History Cleanup (within 24 hours):**
4. **Remove from Git History (BFG Repo-Cleaner)**
```bash
# Install BFG
brew install bfg
# Clone fresh copy
git clone --mirror https://github.com/org/repo.git repo.git
cd repo.git
# Create file with secrets to remove
cat > secrets.txt <<EOF
sk_live_OLD_KEY_LEAKED
AKIA1234567890ABCDEF
-----BEGIN RSA PRIVATE KEY-----
EOF
# Remove secrets
bfg --replace-text secrets.txt .
# Clean up
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# Force push (CAUTION: Coordinate with team)
git push --force --all
git push --force --tags
```
**Alternative: filter-repo**
```bash
pip install git-filter-repo
# Remove specific file
git filter-repo --invert-paths --path config/secrets.json
# Remove specific pattern
echo "sk_live_" > secrets-pattern.txt
git filter-repo --replace-text secrets-pattern.txt
```
5. **Audit Access**
```bash
# Check Vault audit logs
vault audit list
# Read audit log
cat /vault/logs/audit.log | jq 'select(.request.path == "secret/data/api-keys/stripe")'
# GitHub: Who cloned during leak window?
# Check repository Insights → Traffic → Git clones
```
6. **Document Incident**
```markdown
# Incident Report: Secret Leak 2025-12-03
## Summary
- **Date**: 2025-12-03 10:15 UTC
- **Secret**: Stripe API key (sk_live_OLD_KEY_LEAKED)
- **Exposure**: Committed to main branch, pushed to GitHub
- **Discovered**: Gitleaks CI/CD scan
- **Resolved**: Secret rotated within 30 minutes
## Timeline
- 10:15: Secret committed (commit abc123)
- 10:20: CI/CD pipeline detected leak, blocked merge
- 10:25: Secret rotated in Vault
- 10:30: Old key revoked at Stripe
- 11:00: Git history rewritten (BFG)
- 11:15: Force push completed
## Root Cause
- Developer accidentally committed .env file
- Pre-commit hook not installed locally
## Prevention
- [ ] Enforce pre-commit hooks (CI check)
- [ ] Add .env to .gitignore (template)
- [ ] Developer training on secret management
- [ ] Mandatory .gitleaks.toml in all repos
## Impact
- No unauthorized API usage detected
- No customer data accessed
- Leak window: 15 minutes (before rotation)
```
### False Positive Handling
**Allowlist in .gitleaks.toml:**
```toml
[allowlist]
description = "Allowlist for test files and examples"
# Ignore test files
paths = [
'''.*/test/.*''',
'''.*/examples/.*''',
'''.*\.md''', # Documentation
]
# Ignore test keys and placeholders
regexes = [
'''sk_test_[a-zA-Z0-9]{24}''', # Stripe test keys
'''pk_test_[a-zA-Z0-9]{24}''',
'''EXAMPLE_[A-Z_]+''', # Placeholders
'''YOUR_API_KEY_HERE''',
]
# Ignore specific commits (emergency override)
commits = [
"abc123def456", # Migration commit with test data
]
```
**Inline Ignore:**
```python
# gitleaks:allow
api_key = "sk_test_THIS_IS_A_TEST_KEY_FOR_EXAMPLES"
```
## Alternative Tools
### TruffleHog
Deep Git history scanning with entropy detection.
```bash
# Installation
pip install truffleHog
# Scan repository
trufflehog git https://github.com/org/repo.git
# Scan since specific commit
trufflehog git https://github.com/org/repo.git --since-commit abc123
# JSON output
trufflehog git https://github.com/org/repo.git --json
```
### detect-secrets (Yelp)
Baseline-based scanning for Python projects.
```bash
# Installation
pip install detect-secrets
# Create baseline
detect-secrets scan > .secrets.baseline
# Audit baseline (interactive)
detect-secrets audit .secrets.baseline
# Scan for new secrets
detect-secrets scan --baseline .secrets.baseline
```
### git-secrets (AWS)
Prevents committing AWS credentials.
```bash
# Installation
brew install git-secrets
# Install hooks
git secrets --install
git secrets --register-aws
# Scan repository
git secrets --scan
```
### Comparison Matrix
| Tool | Performance | Accuracy | Custom Rules | CI/CD | License |
|------|-------------|----------|--------------|-------|---------|
| **Gitleaks** | Excellent | High | Yes (regex) | Easy | MIT |
| **TruffleHog** | Good | High (entropy) | Limited | Moderate | GPL-3.0 |
| **detect-secrets** | Moderate | Moderate | Yes (plugins) | Easy | Apache-2.0 |
| **git-secrets** | Good | Moderate | Yes (regex) | Easy | Apache-2.0 |
**Recommendation:** Use Gitleaks for most projects (fast, accurate, easy CI/CD integration).
## Best Practices
1. **Layer Defenses**
- Pre-commit hooks (local)
- CI/CD scanning (remote)
- Periodic full repository scans
2. **Rotate Immediately**
- Assume leaked = compromised
- Rotate within 1 hour
- Revoke at provider
3. **Educate Developers**
- Secret management training
- Code review checklist
- Security champions
4. **Monitor Audit Logs**
- Track secret access patterns
- Alert on unusual activity
- Regular audit reviews
5. **Automate Response**
- Auto-rotate on detection
- Auto-revoke leaked credentials
- Incident tickets
```
### references/zero-knowledge.md
```markdown
# Zero-Knowledge Secret Patterns
Client-side encryption and threshold cryptography for zero-knowledge secret management.
## Table of Contents
1. [Zero-Knowledge Principles](#zero-knowledge-principles)
2. [Client-Side Encryption (E2EE)](#client-side-encryption-e2ee)
3. [Shamir's Secret Sharing](#shamirs-secret-sharing)
4. [Use Cases](#use-cases)
## Zero-Knowledge Principles
**Definition:** Server never has access to decryption keys or plaintext secrets.
**Key Properties:**
- Encryption/decryption happens client-side only
- Server stores only encrypted blobs
- Master key derived from user password (never transmitted)
- Compromise of server doesn't expose secrets
**Use Cases:**
- Password managers (1Password, Bitwarden)
- Encrypted notes (Standard Notes)
- Secure messaging (Signal, WhatsApp)
- Vault root token recovery (Shamir shares)
## Client-Side Encryption (E2EE)
### Pattern: User Password → Encryption Key
```typescript
// Client-side encryption (browser)
import { pbkdf2, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
class ZeroKnowledgeVault {
// Derive encryption key from user password
static async deriveKey(
password: string,
salt: Buffer
): Promise<Buffer> {
return new Promise((resolve, reject) => {
// PBKDF2 with 100k iterations
pbkdf2(password, salt, 100000, 32, 'sha256', (err, key) => {
if (err) reject(err);
else resolve(key);
});
});
}
// Encrypt secret client-side
static async encrypt(
plaintext: string,
password: string,
salt: Buffer
): Promise<{ encrypted: string; iv: string; salt: string }> {
const key = await this.deriveKey(password, salt);
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
return {
encrypted: encrypted + ':' + authTag.toString('base64'),
iv: iv.toString('base64'),
salt: salt.toString('base64'),
};
}
// Decrypt secret client-side
static async decrypt(
encryptedData: { encrypted: string; iv: string; salt: string },
password: string
): Promise<string> {
const salt = Buffer.from(encryptedData.salt, 'base64');
const key = await this.deriveKey(password, salt);
const iv = Buffer.from(encryptedData.iv, 'base64');
const [ciphertext, authTagB64] = encryptedData.encrypted.split(':');
const authTag = Buffer.from(authTagB64, 'base64');
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertext, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage
async function example() {
const masterPassword = 'user-entered-password';
const salt = randomBytes(32);
// Encrypt secret (client-side)
const encrypted = await ZeroKnowledgeVault.encrypt(
'my-database-password',
masterPassword,
salt
);
// Send to server (server CANNOT decrypt)
await fetch('/api/secrets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(encrypted),
});
// Later: Decrypt secret (client-side)
const decrypted = await ZeroKnowledgeVault.decrypt(encrypted, masterPassword);
console.log(decrypted); // 'my-database-password'
}
```
### Server-Side (Zero-Knowledge)
```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Dict
import sqlite3
app = FastAPI()
class EncryptedSecret(BaseModel):
encrypted: str
iv: str
salt: str
# Server stores ONLY encrypted data (cannot decrypt)
@app.post("/api/secrets")
async def store_secret(secret: EncryptedSecret):
# Server has NO access to plaintext
conn = sqlite3.connect('secrets.db')
conn.execute('''
INSERT INTO secrets (encrypted_data, iv, salt, created_at)
VALUES (?, ?, ?, datetime('now'))
''', (secret.encrypted, secret.iv, secret.salt))
conn.commit()
conn.close()
return {"status": "stored", "message": "Secret encrypted and stored"}
@app.get("/api/secrets/{id}")
async def get_secret(id: int):
# Server returns encrypted blob (client decrypts)
conn = sqlite3.connect('secrets.db')
cursor = conn.execute(
'SELECT encrypted_data, iv, salt FROM secrets WHERE id = ?',
(id,)
)
row = cursor.fetchone()
conn.close()
if not row:
raise HTTPException(status_code=404, detail="Secret not found")
return {
"encrypted": row[0],
"iv": row[1],
"salt": row[2]
}
```
### Web Crypto API (Browser)
```javascript
// Modern browser-based encryption
class WebCryptoVault {
// Derive key from password
static async deriveKey(password, salt) {
const encoder = new TextEncoder();
const passwordKey = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256',
},
passwordKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Encrypt
static async encrypt(plaintext, password) {
const salt = crypto.getRandomValues(new Uint8Array(32));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await this.deriveKey(password, salt);
const encoder = new TextEncoder();
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
encoder.encode(plaintext)
);
return {
encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
iv: btoa(String.fromCharCode(...iv)),
salt: btoa(String.fromCharCode(...salt)),
};
}
// Decrypt
static async decrypt(encryptedData, password) {
const salt = Uint8Array.from(atob(encryptedData.salt), c => c.charCodeAt(0));
const iv = Uint8Array.from(atob(encryptedData.iv), c => c.charCodeAt(0));
const encrypted = Uint8Array.from(atob(encryptedData.encrypted), c => c.charCodeAt(0));
const key = await this.deriveKey(password, salt);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
encrypted
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}
}
// Usage
async function example() {
const encrypted = await WebCryptoVault.encrypt('my-secret', 'user-password');
console.log(encrypted); // { encrypted, iv, salt }
const decrypted = await WebCryptoVault.decrypt(encrypted, 'user-password');
console.log(decrypted); // 'my-secret'
}
```
## Shamir's Secret Sharing
### Pattern: Threshold Cryptography
Split a secret into N shares, require M shares to reconstruct.
**Use Cases:**
- Vault root token recovery (3 of 5 key holders)
- Multi-party approval workflows
- Distributed trust (no single point of failure)
### Python Implementation
```python
from secretsharing import PlaintextToHexSecretSharer
# Split secret into 5 shares (require 3 to reconstruct)
root_token = "your-secret-root-token-here"
shares = PlaintextToHexSecretSharer.split_secret(root_token, 3, 5)
print(f"Share 1 (CTO): {shares[0]}")
print(f"Share 2 (Security Lead): {shares[1]}")
print(f"Share 3 (DevOps Lead): {shares[2]}")
print(f"Share 4 (Compliance Officer): {shares[3]}")
print(f"Share 5 (Backup - secure storage): {shares[4]}")
# Reconstruct with any 3 of 5 shares
recovered_token = PlaintextToHexSecretSharer.recover_secret(shares[0:3])
assert recovered_token == root_token
# Only 2 shares? Cannot reconstruct
try:
PlaintextToHexSecretSharer.recover_secret(shares[0:2])
except Exception as e:
print(f"Error: {e}") # "Not enough shares to reconstruct"
```
### Vault Initialization with Shamir
```bash
# Initialize Vault with Shamir shares
vault operator init \
-key-shares=5 \
-key-threshold=3
# Output:
# Unseal Key 1: abc123...
# Unseal Key 2: def456...
# Unseal Key 3: ghi789...
# Unseal Key 4: jkl012...
# Unseal Key 5: mno345...
# Initial Root Token: hvs.CAES...
# Distribute shares to different key holders
# - Share 1 → CTO (printed to paper, secure safe)
# - Share 2 → Security Lead (password manager)
# - Share 3 → DevOps Lead (HSM)
# - Share 4 → Compliance Officer (encrypted USB)
# - Share 5 → Backup (bank vault)
```
### Unsealing Vault (Requires 3 of 5 Shares)
```bash
# Key holder 1 (CTO) provides share
vault operator unseal abc123...
# Output: Sealed: true, Progress: 1/3
# Key holder 2 (Security Lead) provides share
vault operator unseal def456...
# Output: Sealed: true, Progress: 2/3
# Key holder 3 (DevOps Lead) provides share
vault operator unseal ghi789...
# Output: Sealed: false, Progress: 3/3
# Vault is now unsealed (3 of 5 threshold met)
```
### TypeScript Implementation (sss-wasm)
```typescript
import { split, combine } from 'sss-wasm';
// Split secret
const secret = Buffer.from('my-vault-root-token', 'utf8');
const shares = split(secret, { shares: 5, threshold: 3 });
console.log(`Share 1: ${shares[0].toString('hex')}`);
console.log(`Share 2: ${shares[1].toString('hex')}`);
console.log(`Share 3: ${shares[2].toString('hex')}`);
console.log(`Share 4: ${shares[3].toString('hex')}`);
console.log(`Share 5: ${shares[4].toString('hex')}`);
// Reconstruct with 3 shares
const recovered = combine([shares[0], shares[2], shares[4]]);
console.log(recovered.toString('utf8')); // 'my-vault-root-token'
// Only 2 shares? Cannot reconstruct
try {
combine([shares[0], shares[1]]);
} catch (err) {
console.error('Error: Not enough shares');
}
```
## Use Cases
### Use Case 1: Password Manager
```typescript
// User registers
const masterPassword = 'user-chosen-password';
const salt = crypto.getRandomValues(new Uint8Array(32));
// Server stores salt (NOT the password)
await fetch('/api/register', {
method: 'POST',
body: JSON.stringify({
username: 'alice',
salt: btoa(String.fromCharCode(...salt)),
}),
});
// User stores a password
const encrypted = await WebCryptoVault.encrypt('my-gmail-password', masterPassword);
await fetch('/api/passwords', {
method: 'POST',
body: JSON.stringify({
site: 'gmail.com',
...encrypted,
}),
});
// Server has ZERO knowledge of:
// - Master password
// - Stored password (my-gmail-password)
// - Encryption key
```
### Use Case 2: Vault Root Token Recovery
```bash
# Scenario: All Vault nodes lost, need to recover root token
# Step 1: Gather 3 of 5 key holders
# - CTO provides share 1
# - Security Lead provides share 2
# - DevOps Lead provides share 3
# Step 2: Reconstruct root token (offline)
python3 <<EOF
from secretsharing import PlaintextToHexSecretSharer
shares = [
"abc123...", # Share 1 from CTO
"def456...", # Share 2 from Security Lead
"ghi789..." # Share 3 from DevOps Lead
]
root_token = PlaintextToHexSecretSharer.recover_secret(shares)
print(f"Root Token: {root_token}")
EOF
# Step 3: Use root token to initialize new Vault cluster
vault login hvs.CAES...
vault operator init -recovery-shares=5 -recovery-threshold=3
```
### Use Case 3: Multi-Party Approval
```python
# Scenario: Deploy requires approval from 2 of 3 leads
from secretsharing import PlaintextToHexSecretSharer
# Deploy key (required to trigger production deployment)
deploy_key = "prod-deploy-key-2025-12-03"
# Split into 3 shares (require 2 to reconstruct)
shares = PlaintextToHexSecretSharer.split_secret(deploy_key, 2, 3)
# Distribute shares
# - Tech Lead: shares[0]
# - Product Lead: shares[1]
# - Security Lead: shares[2]
# Deployment request: Tech Lead + Product Lead provide shares
recovered_key = PlaintextToHexSecretSharer.recover_secret([shares[0], shares[1]])
# CI/CD validates key before deploying
if recovered_key == deploy_key:
print("✓ Deployment approved by 2 of 3 leads. Proceeding...")
deploy_to_production()
else:
print("✗ Invalid deployment key. Approval required.")
```
## Security Considerations
**Key Derivation:**
- Use PBKDF2 with 100k+ iterations (or Argon2id)
- Random salt (32 bytes, unique per user)
- Never transmit master password
**Encryption Algorithm:**
- AES-256-GCM (authenticated encryption)
- Random IV (12 bytes for GCM, 16 for CBC)
- Verify auth tag on decryption
**Shamir Shares:**
- Distribute shares to independent key holders
- Store in different locations (geographic separation)
- Document recovery procedure
- Test recovery annually
**Password Strength:**
- Enforce minimum entropy (80+ bits)
- Use password strength meter
- Consider passphrase (4+ random words)
- Offer 2FA for account recovery
```
### references/cloud-providers.md
```markdown
# Cloud Provider Secret Managers
Quick reference for AWS Secrets Manager, GCP Secret Manager, and Azure Key Vault.
## Table of Contents
1. [Comparison Matrix](#comparison-matrix)
2. [AWS Secrets Manager](#aws-secrets-manager)
3. [GCP Secret Manager](#gcp-secret-manager)
4. [Azure Key Vault](#azure-key-vault)
5. [Kubernetes Integration (ESO)](#kubernetes-integration-eso)
## Comparison Matrix
| Feature | AWS Secrets Manager | GCP Secret Manager | Azure Key Vault |
|---------|--------------------|--------------------|-----------------|
| **Pricing** | $0.40/secret/month + $0.05/10k API calls | $0.06/secret version/month + $0.03/10k ops | $0.03/10k ops, Free tier |
| **Rotation** | Automatic (Lambda) | Manual (Cloud Functions) | Automatic (Event Grid) |
| **Versioning** | Yes (auto-versioned) | Yes (explicit versions) | Yes (versions) |
| **Replication** | Multi-region | Global | Multi-region |
| **IAM Integration** | Native (IAM policies) | Native (IAM policies) | Native (RBAC) |
| **Kubernetes** | ESO, CSI driver | ESO, CSI driver | ESO, CSI driver |
## AWS Secrets Manager
### CLI Commands
```bash
# Create secret
aws secretsmanager create-secret \
--name prod/database/credentials \
--secret-string '{"username":"admin","password":"secret123"}'
# Retrieve secret
aws secretsmanager get-secret-value \
--secret-id prod/database/credentials \
--query SecretString --output text
# Update secret
aws secretsmanager update-secret \
--secret-id prod/database/credentials \
--secret-string '{"username":"admin","password":"newsecret456"}'
# Enable automatic rotation
aws secretsmanager rotate-secret \
--secret-id prod/database/credentials \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:rotate-secret
```
### IAM Policy
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/*"
}
]
}
```
### Python SDK (boto3)
```python
import boto3
import json
client = boto3.client('secretsmanager', region_name='us-east-1')
# Get secret
response = client.get_secret_value(SecretId='prod/database/credentials')
secret = json.loads(response['SecretString'])
print(f"Username: {secret['username']}")
print(f"Password: {secret['password']}")
```
## GCP Secret Manager
### gcloud Commands
```bash
# Create secret
echo -n "secret-password" | gcloud secrets create db-password \
--data-file=- \
--replication-policy=automatic
# Add version
echo -n "new-password" | gcloud secrets versions add db-password \
--data-file=-
# Access secret
gcloud secrets versions access latest --secret=db-password
# Delete secret
gcloud secrets delete db-password
```
### IAM Binding
```bash
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor"
```
### Python SDK
```python
from google.cloud import secretmanager
client = secretmanager.SecretManagerServiceClient()
# Access secret
name = "projects/my-project/secrets/db-password/versions/latest"
response = client.access_secret_version(request={"name": name})
secret = response.payload.data.decode('UTF-8')
print(f"Secret: {secret}")
```
## Azure Key Vault
### Azure CLI Commands
```bash
# Create Key Vault
az keyvault create \
--name my-keyvault \
--resource-group my-rg \
--location eastus
# Set secret
az keyvault secret set \
--vault-name my-keyvault \
--name db-password \
--value "secret123"
# Get secret
az keyvault secret show \
--vault-name my-keyvault \
--name db-password \
--query value --output tsv
# Enable soft-delete
az keyvault update \
--name my-keyvault \
--enable-soft-delete true \
--retention-days 90
```
### Access Policy
```bash
az keyvault set-policy \
--name my-keyvault \
--spn <service-principal-id> \
--secret-permissions get list
```
### Python SDK
```python
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
client = SecretClient(vault_url="https://my-keyvault.vault.azure.net", credential=credential)
# Get secret
secret = client.get_secret("db-password")
print(f"Secret: {secret.value}")
```
## Kubernetes Integration (ESO)
### AWS Secrets Manager + ESO
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: app-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
target:
name: db-credentials
data:
- secretKey: username
remoteRef:
key: prod/database/credentials
property: username
- secretKey: password
remoteRef:
key: prod/database/credentials
property: password
```
### GCP Secret Manager + ESO
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: gcp-secrets
spec:
provider:
gcpsm:
projectID: "my-project-123"
auth:
workloadIdentity:
clusterLocation: us-central1
clusterName: production-cluster
serviceAccountRef:
name: app-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: gcp-secrets
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: db-password
version: latest
```
### Azure Key Vault + ESO
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: azure-secrets
spec:
provider:
azurekv:
vaultUrl: "https://my-keyvault.vault.azure.net"
authType: WorkloadIdentity
serviceAccountRef:
name: app-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: azure-secrets
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: db-password
```
```
### scripts/setup_vault.sh
```bash
#!/bin/bash
#
# setup_vault.sh - Automated Vault installation and configuration
#
# Usage:
# ./setup_vault.sh [kubernetes|docker|local]
#
# Modes:
# kubernetes - Deploy Vault on Kubernetes (default)
# docker - Run Vault in Docker container
# local - Install and run Vault locally
set -e
MODE=${1:-kubernetes}
echo "🔐 Vault Setup Script"
echo "Mode: $MODE"
echo ""
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
info() {
echo -e "${GREEN}✓${NC} $1"
}
warn() {
echo -e "${YELLOW}⚠${NC} $1"
}
error() {
echo -e "${RED}✗${NC} $1"
exit 1
}
# Check if Vault CLI is installed
check_vault_cli() {
if ! command -v vault &> /dev/null; then
warn "Vault CLI not installed. Installing..."
if [[ "$OSTYPE" == "darwin"* ]]; then
brew install hashicorp/tap/vault
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
else
error "Unsupported OS. Install Vault manually: https://www.vaultproject.io/downloads"
fi
info "Vault CLI installed"
else
info "Vault CLI already installed ($(vault version))"
fi
}
# Kubernetes deployment
setup_kubernetes() {
info "Deploying Vault on Kubernetes..."
# Check if kubectl is available
if ! command -v kubectl &> /dev/null; then
error "kubectl not found. Install kubectl first."
fi
# Create namespace
kubectl create namespace vault --dry-run=client -o yaml | kubectl apply -f -
info "Namespace 'vault' created"
# Deploy using Helm (recommended)
if command -v helm &> /dev/null; then
info "Using Helm for deployment..."
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault \
--namespace vault \
--set "server.dev.enabled=true" \
--set "injector.enabled=false"
info "Vault deployed via Helm"
else
# Fallback to manual deployment
warn "Helm not found. Using manual deployment..."
kubectl apply -f ../examples/vault-eso-setup/vault-deployment.yaml
info "Vault deployed via kubectl"
fi
# Wait for pod to be ready
info "Waiting for Vault pod to be ready..."
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault -n vault --timeout=120s
# Initialize and unseal (dev mode)
info "Vault is ready!"
info "To access Vault:"
echo " kubectl port-forward -n vault svc/vault 8200:8200"
echo " export VAULT_ADDR='http://localhost:8200'"
echo " export VAULT_TOKEN='root' # Dev mode root token"
}
# Docker deployment
setup_docker() {
info "Running Vault in Docker (dev mode)..."
docker run -d \
--name vault-dev \
--cap-add=IPC_LOCK \
-e 'VAULT_DEV_ROOT_TOKEN_ID=root' \
-p 8200:8200 \
hashicorp/vault:latest
info "Vault running in Docker"
info "Access at: http://localhost:8200"
info "Root token: root"
echo ""
echo "Set environment variables:"
echo " export VAULT_ADDR='http://localhost:8200'"
echo " export VAULT_TOKEN='root'"
}
# Local installation
setup_local() {
check_vault_cli
info "Starting Vault in dev mode..."
# Create config directory
mkdir -p ~/.vault
# Start Vault in background
nohup vault server -dev \
-dev-root-token-id=root \
-dev-listen-address=127.0.0.1:8200 \
> ~/.vault/vault.log 2>&1 &
VAULT_PID=$!
echo $VAULT_PID > ~/.vault/vault.pid
sleep 2
info "Vault started (PID: $VAULT_PID)"
info "Access at: http://localhost:8200"
info "Root token: root"
echo ""
echo "Set environment variables:"
echo " export VAULT_ADDR='http://localhost:8200'"
echo " export VAULT_TOKEN='root'"
echo ""
echo "To stop Vault:"
echo " kill $(cat ~/.vault/vault.pid)"
}
# Configure Vault basics
configure_vault() {
info "Configuring Vault..."
# Enable KV v2 secrets engine
vault secrets enable -path=secret kv-v2 || warn "KV v2 already enabled"
info "KV v2 secrets engine enabled at secret/"
# Enable database secrets engine
vault secrets enable database || warn "Database engine already enabled"
info "Database secrets engine enabled"
# Enable Kubernetes auth (if in Kubernetes mode)
if [[ "$MODE" == "kubernetes" ]]; then
vault auth enable kubernetes || warn "Kubernetes auth already enabled"
info "Kubernetes auth method enabled"
fi
info "Basic configuration complete!"
}
# Main execution
main() {
check_vault_cli
case $MODE in
kubernetes)
setup_kubernetes
;;
docker)
setup_docker
;;
local)
setup_local
;;
*)
error "Unknown mode: $MODE. Use: kubernetes, docker, or local"
;;
esac
echo ""
info "Vault setup complete!"
echo ""
echo "Next steps:"
echo "1. Set environment variables (see above)"
echo "2. Verify connection: vault status"
echo "3. Create secrets: vault kv put secret/myapp/config api_key=EXAMPLE"
echo "4. Read secrets: vault kv get secret/myapp/config"
}
main
```