This document outlines procedures for rotating secrets in the CryptoFunk trading system using HashiCorp Vault.
- Overview
- Prerequisites
- Secret Types
- Rotation Schedules
- Rotation Procedures
- Emergency Rotation
- Vault Setup
- Verification
- CryptoFunk API Key Management
CryptoFunk uses HashiCorp Vault for secure secrets management in production. All production secrets should be stored in Vault and never committed to version control or stored in plain text.
Security Principle: Regular secret rotation reduces the window of vulnerability if credentials are compromised.
Implementation Status: ✅ Complete - Vault integration is fully implemented in internal/config/secrets.go and integrated into all services through config.Load(). Services automatically load secrets from Vault when VAULT_ENABLED=true, with graceful fallback to environment variables when disabled.
Key Features:
- Automatic Vault Integration: All services use
config.Load()which automatically loads secrets from Vault when enabled - Graceful Fallback: If Vault is unavailable or disabled, services fall back to environment variables (Kubernetes secrets)
- Multiple Auth Methods: Supports Kubernetes service account, token, and AppRole authentication
- Zero Code Changes: MCP servers and agents automatically benefit from Vault integration through the config package
- Development Friendly: Local development uses environment variables by default (no Vault required)
Architecture:
┌─────────────────────────────────────────────────────────────┐
│ CryptoFunk Services │
│ (orchestrator, api, mcp-servers, agents, bifrost) │
└──────────────────────┬──────────────────────────────────────┘
│ config.Load()
↓
┌─────────────────────────────────────────────────────────────┐
│ internal/config/secrets.go │
│ • GetVaultConfigFromEnv() │
│ • LoadSecretsFromVault() │
│ • NewVaultClient() │
└──────────┬──────────────────────────┬──────────────────────┘
│ VAULT_ENABLED=true │ VAULT_ENABLED=false
↓ ↓
┌─────────────────────┐ ┌─────────────────────────────┐
│ HashiCorp Vault │ │ Environment Variables │
│ • K8s Auth │ │ (from K8s secrets) │
│ • KV v2 Engine │ │ • POSTGRES_PASSWORD │
│ • Secrets: │ │ • BINANCE_API_KEY │
│ - database │ │ • ANTHROPIC_API_KEY │
│ - redis │ │ • etc. │
│ - exchanges/* │ │ │
│ - llm │ │ Fallback for dev/testing │
└─────────────────────┘ └─────────────────────────────┘
- HashiCorp Vault deployed and accessible
- Vault CLI installed (
brew install vaultor see Vault installation) - Vault authentication credentials (token, Kubernetes service account, or AppRole)
- Appropriate Vault policies for reading/writing secrets
CryptoFunk manages the following secret types:
-
Database Credentials (PostgreSQL)
- User:
postgres - Password: Rotated quarterly
- Path:
secret/data/cryptofunk/production/database
- User:
-
Redis Credentials
- Password: Rotated quarterly
- Path:
secret/data/cryptofunk/production/redis
-
Exchange API Keys (Binance, etc.)
- API Key and Secret Key
- Rotated monthly (or immediately if compromised)
- Path:
secret/data/cryptofunk/production/exchanges/<exchange-name>
-
LLM API Keys (Anthropic, OpenAI, Gemini)
- Provider-specific API keys
- Rotated monthly
- Path:
secret/data/cryptofunk/production/llm
-
CryptoFunk API Keys (User Authentication)
- SHA-256 hashed keys stored in PostgreSQL
- Used for REST API authentication
- Managed via database functions
- See CryptoFunk API Key Management
| Secret Type | Rotation Frequency | Reason |
|---|---|---|
| Database Password | Quarterly (90 days) | Low exposure risk, high rotation cost |
| Redis Password | Quarterly (90 days) | Low exposure risk |
| Exchange API Keys | Monthly (30 days) | High security risk, financial implications |
| LLM API Keys | Monthly (30 days) | Moderate risk, usage tracking |
| All Secrets | Immediately | If compromise suspected |
Downtime: ~30 seconds (connection pool refresh)
Steps:
-
Generate new password:
# Generate a strong password (32 characters, alphanumeric + special) NEW_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32) echo "New password: $NEW_PASSWORD"
-
Update PostgreSQL:
# Connect to PostgreSQL kubectl exec -it -n cryptofunk deployment/postgres -- \ psql -U postgres -c "ALTER USER postgres WITH PASSWORD '$NEW_PASSWORD';"
-
Update Vault:
# Authenticate to Vault vault login # Write new password to Vault vault kv put secret/cryptofunk/production/database \ password="$NEW_PASSWORD" \ user="postgres"
-
Rolling restart applications:
# Restart orchestrator (will reload secrets from Vault) kubectl rollout restart deployment/orchestrator -n cryptofunk # Restart API server kubectl rollout restart deployment/api -n cryptofunk # Restart MCP servers kubectl rollout restart deployment/market-data-server -n cryptofunk kubectl rollout restart deployment/technical-indicators-server -n cryptofunk kubectl rollout restart deployment/risk-analyzer-server -n cryptofunk kubectl rollout restart deployment/order-executor-server -n cryptofunk # Restart agents kubectl rollout restart deployment/technical-agent -n cryptofunk kubectl rollout restart deployment/orderbook-agent -n cryptofunk kubectl rollout restart deployment/sentiment-agent -n cryptofunk kubectl rollout restart deployment/trend-agent -n cryptofunk kubectl rollout restart deployment/reversion-agent -n cryptofunk kubectl rollout restart deployment/risk-agent -n cryptofunk
-
Verify connectivity:
# Check orchestrator logs for successful database connection kubectl logs -f deployment/orchestrator -n cryptofunk | grep -i database # Verify health endpoint curl http://orchestrator-service:8080/health
Downtime: ~15 seconds (cache refresh)
Steps:
-
Generate new password:
NEW_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32) -
Update Redis configuration:
# For Docker Compose docker-compose exec redis redis-cli CONFIG SET requirepass "$NEW_PASSWORD" # For Kubernetes kubectl exec -it -n cryptofunk deployment/redis -- \ redis-cli CONFIG SET requirepass "$NEW_PASSWORD"
-
Update Vault:
vault kv put secret/cryptofunk/production/redis \ password="$NEW_PASSWORD" -
Rolling restart applications (same as database rotation above)
-
Verify:
# Test Redis connection kubectl exec -it -n cryptofunk deployment/redis -- \ redis-cli -a "$NEW_PASSWORD" PING
Downtime: None (key rotation can be done without downtime)
Steps:
-
Generate new API keys on exchange:
- Log into Binance (or other exchange)
- Navigate to API Management
- Create new API key with same permissions as old key
- IMPORTANT: Restrict IP addresses to your cluster IPs
- Copy API Key and Secret Key
-
Update Vault:
# For Binance vault kv put secret/cryptofunk/production/exchanges/binance \ api_key="<NEW_API_KEY>" \ secret_key="<NEW_SECRET_KEY>"
-
Rolling restart order executor (to load new keys):
kubectl rollout restart deployment/order-executor-server -n cryptofunk
-
Verify connectivity:
# Check order executor logs kubectl logs -f deployment/order-executor-server -n cryptofunk # Test with --verify-keys flag kubectl exec -it deployment/orchestrator -n cryptofunk -- \ /app/orchestrator --verify-keys
-
Delete old API key (after verification):
- Log into exchange
- Delete the old API key
- IMPORTANT: Only delete after confirming new key works!
Downtime: None (Bifrost handles key rotation gracefully)
Steps:
-
Generate new API keys from providers:
-
Update Vault:
vault kv put secret/cryptofunk/production/llm \ anthropic_api_key="<NEW_ANTHROPIC_KEY>" \ openai_api_key="<NEW_OPENAI_KEY>" \ gemini_api_key="<NEW_GEMINI_KEY>"
-
Restart Bifrost:
kubectl rollout restart deployment/bifrost -n cryptofunk
-
Restart agents (to reload LLM keys):
kubectl rollout restart deployment/orchestrator -n cryptofunk kubectl rollout restart deployment/technical-agent -n cryptofunk kubectl rollout restart deployment/orderbook-agent -n cryptofunk kubectl rollout restart deployment/sentiment-agent -n cryptofunk kubectl rollout restart deployment/trend-agent -n cryptofunk kubectl rollout restart deployment/reversion-agent -n cryptofunk kubectl rollout restart deployment/risk-agent -n cryptofunk
-
Verify:
# Test LLM connectivity kubectl logs -f deployment/bifrost -n cryptofunk # Verify agents can make LLM calls kubectl logs -f deployment/technical-agent -n cryptofunk | grep -i "llm"
If you suspect credentials have been compromised:
-
Immediate Actions:
# STEP 1: Disable compromised credentials immediately # For exchange keys - disable on exchange portal immediately # For database - change password immediately # STEP 2: Stop all trading activity kubectl scale deployment/orchestrator --replicas=0 -n cryptofunk # STEP 3: Review logs for suspicious activity kubectl logs deployment/orchestrator -n cryptofunk --since=24h > logs.txt kubectl logs deployment/order-executor-server -n cryptofunk --since=24h >> logs.txt # STEP 4: Check for unauthorized trades kubectl exec -it -n cryptofunk deployment/postgres -- \ psql -U postgres -d cryptofunk -c \ "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '24 hours' ORDER BY created_at DESC;"
-
Rotate all affected credentials following procedures above
-
Investigate root cause:
- Check application logs for anomalies
- Review access logs
- Scan for security vulnerabilities
- Check if secrets were accidentally committed to version control
-
Resume operations:
# After rotation and investigation kubectl scale deployment/orchestrator --replicas=1 -n cryptofunk -
Document incident in incident log and update security procedures
CryptoFunk's Vault integration is designed to be transparent and backwards-compatible:
-
On Service Startup:
- Service calls
config.Load("")to load configuration config.Load()checksVAULT_ENABLEDenvironment variable- If enabled, it calls
LoadSecretsFromVault()to fetch secrets from Vault - If disabled or Vault fails, it falls back to environment variables
- Configuration is validated before service continues
- Service calls
-
Secret Loading Priority:
1. Vault (if VAULT_ENABLED=true and connection succeeds) 2. Environment variables (fallback or when Vault disabled) 3. Configuration file placeholders (development only) -
Supported Secrets:
- Database:
secret/data/cryptofunk/production/database→password,user - Redis:
secret/data/cryptofunk/production/redis→password - Exchanges:
secret/data/cryptofunk/production/exchanges/{name}→api_key,secret_key - LLM:
secret/data/cryptofunk/production/llm→anthropic_api_key,openai_api_key,gemini_api_key
- Database:
All services use the same pattern:
package main
import (
"github.com/ajitpratap0/cryptofunk/internal/config"
)
func main() {
// Load configuration (automatically handles Vault if enabled)
cfg, err := config.Load("")
if err != nil {
log.Fatal().Err(err).Msg("Failed to load configuration")
}
// Access secrets - they're already loaded from Vault or env vars
dbPassword := cfg.Database.Password
binanceKey := cfg.Exchanges["binance"].APIKey
// Use secrets...
}vaultCfg := config.GetVaultConfigFromEnv()
if vaultCfg.Enabled {
log.Info().Msg("Vault integration is enabled")
} else {
log.Info().Msg("Using environment variables for secrets")
}For custom secret loading:
import "github.com/ajitpratap0/cryptofunk/internal/config"
vaultCfg := config.GetVaultConfigFromEnv()
vaultClient, err := config.NewVaultClient(vaultCfg)
if err != nil {
return err
}
// Get a specific secret
secret, err := vaultClient.GetSecretString(ctx, "custom/path", "key_name")
if err != nil {
return err
}# Apply Vault service account, role bindings, and config
kubectl apply -f deployments/k8s/base/vault-integration.yaml# Edit vault-config ConfigMap
kubectl edit configmap vault-config -n cryptofunk
# Update:
# VAULT_ADDR: "https://your-vault-server:8200"
# VAULT_ENABLED: "true"All deployments now include Vault configuration. Example for orchestrator:
spec:
template:
spec:
serviceAccountName: cryptofunk-vault # Required for K8s auth
containers:
- name: orchestrator
env:
# Vault Configuration (from vault-config ConfigMap)
- name: VAULT_ENABLED
valueFrom:
configMapKeyRef:
name: vault-config
key: VAULT_ENABLED
- name: VAULT_ADDR
valueFrom:
configMapKeyRef:
name: vault-config
key: VAULT_ADDR
# ... more Vault config ...
# Fallback secrets (used when VAULT_ENABLED=false)
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: cryptofunk-secrets
key: POSTGRES_PASSWORDSee deployments/k8s/base/vault-example-deployment.yaml for complete example.
kubectl rollout restart deployment -n cryptofunkFor local development (without Vault):
# In your terminal or .env file
export VAULT_ENABLED=false # This is the default
# Set secrets as environment variables
export POSTGRES_PASSWORD="dev_password"
export BINANCE_API_KEY="dev_key"
export BINANCE_API_SECRET="dev_secret"
# Run service
go run cmd/orchestrator/main.goServices will log:
INFO Vault integration disabled - using environment variables for secrets
# Test with Vault enabled
export VAULT_ENABLED=true
export VAULT_ADDR="http://localhost:8200"
export VAULT_TOKEN="your-token"
go run cmd/orchestrator/main.go
# Expected logs:
# INFO Vault integration enabled - loading secrets from Vault
# INFO Vault client initialized successfully
# INFO ✓ Loaded database password from Vault
# INFO ✓ Loaded exchange API keys from Vault-
Enable KV secrets engine (v2):
vault secrets enable -path=secret kv-v2 -
Create CryptoFunk secrets path:
vault kv put secret/cryptofunk/production/database \ password="<initial-password>" \ user="postgres" vault kv put secret/cryptofunk/production/redis \ password="<initial-password>" vault kv put secret/cryptofunk/production/exchanges/binance \ api_key="<binance-api-key>" \ secret_key="<binance-secret-key>" vault kv put secret/cryptofunk/production/llm \ anthropic_api_key="<anthropic-key>" \ openai_api_key="<openai-key>" \ gemini_api_key="<gemini-key>"
-
Create Vault policy for CryptoFunk:
# Create policy file cat > cryptofunk-policy.hcl <<EOF # Allow reading all CryptoFunk secrets path "secret/data/cryptofunk/production/*" { capabilities = ["read", "list"] } # Allow reading secret metadata path "secret/metadata/cryptofunk/production/*" { capabilities = ["read", "list"] } EOF # Apply policy vault policy write cryptofunk cryptofunk-policy.hcl
-
Configure Kubernetes authentication:
# Enable Kubernetes auth vault auth enable kubernetes # Configure Kubernetes auth vault write auth/kubernetes/config \ kubernetes_host="https://kubernetes.default.svc:443" \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token # Create role for CryptoFunk vault write auth/kubernetes/role/cryptofunk \ bound_service_account_names=cryptofunk-vault \ bound_service_account_namespaces=cryptofunk \ policies=cryptofunk \ ttl=24h
Set these environment variables in Kubernetes deployments:
env:
- name: VAULT_ENABLED
value: "true"
- name: VAULT_ADDR
value: "https://vault.example.com:8200"
- name: VAULT_AUTH_METHOD
value: "kubernetes"
- name: VAULT_MOUNT_PATH
value: "secret"
- name: VAULT_SECRET_PATH
value: "cryptofunk/production"
- name: VAULT_K8S_ROLE
value: "cryptofunk"# Check if Vault is accessible
vault status
# List secrets
vault kv list secret/cryptofunk/production
# Read a secret (without revealing value)
vault kv get secret/cryptofunk/production/database
# Test Kubernetes authentication
kubectl exec -it deployment/orchestrator -n cryptofunk -- \
env | grep VAULT# Check orchestrator logs for Vault messages
kubectl logs deployment/orchestrator -n cryptofunk | grep -i vault
# Should see messages like:
# "Vault client initialized successfully"
# "✓ Loaded database password from Vault"
# "✓ Loaded exchange API keys from Vault"# 1. Note current secret version
vault kv metadata get secret/cryptofunk/production/database
# 2. Rotate secret
vault kv put secret/cryptofunk/production/database \
password="new-test-password" \
user="postgres"
# 3. Restart application
kubectl rollout restart deployment/orchestrator -n cryptofunk
# 4. Verify new secret is loaded
kubectl logs deployment/orchestrator -n cryptofunk | tail -20-
Never commit secrets to version control
- Use
.gitignorefor sensitive files - Use Vault for all production secrets
- Use environment variables for development
- Use
-
Use strong, unique passwords
- Minimum 32 characters for database/Redis
- Use
openssl rand -base64 32for generation - Never reuse passwords across systems
-
Restrict Vault access
- Use least-privilege policies
- Audit Vault access logs regularly
- Rotate Vault tokens periodically
-
Automate rotation
- Set calendar reminders for scheduled rotations
- Consider Vault's dynamic secrets for auto-rotation
- Document all rotations in change log
-
Test rotation procedures
- Practice rotations in staging environment
- Verify applications handle secret updates gracefully
- Have rollback plan ready
-
Monitor for suspicious activity
- Alert on failed authentication attempts
- Monitor unusual API usage patterns
- Track secret access in Vault audit logs
# Check Vault status
kubectl exec -it deployment/orchestrator -n cryptofunk -- \
curl -v https://vault.example.com:8200/v1/sys/health
# Verify service account token
kubectl exec -it deployment/orchestrator -n cryptofunk -- \
cat /var/run/secrets/kubernetes.io/serviceaccount/token# List all secrets
vault kv list secret/cryptofunk/production
# Check specific path
vault kv get secret/cryptofunk/production/database
# Verify path format (KV v2 uses /data/ in path)
# Correct: secret/data/cryptofunk/production/database
# Incorrect: secret/cryptofunk/production/database# Check Kubernetes auth configuration
vault read auth/kubernetes/config
# Check role configuration
vault read auth/kubernetes/role/cryptofunk
# Verify service account exists
kubectl get serviceaccount cryptofunk-vault -n cryptofunkCryptoFunk uses API key authentication for the REST API. This section covers how to create, rotate, and revoke API keys for user authentication.
API keys are used to authenticate requests to the CryptoFunk REST API. They provide:
- SHA-256 hashed storage (raw keys are never stored)
- Permission-based authorization
- Expiration support
- Usage tracking (last_used_at)
- Revocation capability
-
Run migration
009_api_keys.sqlto create theapi_keystable:task db-migrate
-
Enable authentication in
config.yaml:api: auth: enabled: true header_name: "X-API-Key" require_https: true
Use the create_api_key() PostgreSQL function to create new keys:
# Connect to PostgreSQL
task db-shell
# Create a new API key with specific permissions
SELECT create_api_key(
'My Service Key', -- name
'system', -- user_id
ARRAY['read', 'write'], -- permissions
NULL -- expires_at (NULL = never expires)
);
# Create an API key that expires in 90 days
SELECT create_api_key(
'Temporary Key',
'admin',
ARRAY['read'],
NOW() + INTERVAL '90 days'
);
# Create an admin key with full permissions
SELECT create_api_key(
'Admin Key',
'admin',
ARRAY['admin'], -- 'admin' or '*' grants all permissions
NULL
);The function returns the raw API key (e.g., cfk_abc123...). Store this securely - it cannot be recovered!
Include the API key in requests using either method:
# Method 1: X-API-Key header (preferred)
curl -H "X-API-Key: cfk_abc123..." \
http://localhost:8080/api/v1/decisions
# Method 2: Authorization Bearer header
curl -H "Authorization: Bearer cfk_abc123..." \
http://localhost:8080/api/v1/decisionsRecommended rotation frequency: Every 90 days, or immediately if compromised.
Zero-downtime rotation steps:
-
Create new API key (before expiring old one):
SELECT create_api_key( 'My Service Key v2', 'system', ARRAY['read', 'write'], NULL );
-
Update applications to use the new key.
-
Verify new key works:
curl -H "X-API-Key: <new-key>" \ http://localhost:8080/api/v1/health -
Revoke old key:
-- Find old key by name SELECT id, name, created_at FROM api_keys WHERE name LIKE 'My Service Key%' ORDER BY created_at DESC; -- Revoke the old key UPDATE api_keys SET revoked = true WHERE id = '<old-key-id>';
Revoke keys immediately if compromised:
-- Revoke by key ID
UPDATE api_keys SET revoked = true WHERE id = '<key-id>';
-- Revoke all keys for a user
UPDATE api_keys SET revoked = true WHERE user_id = '<user-id>';
-- Verify revocation
SELECT id, name, revoked FROM api_keys WHERE id = '<key-id>';Revoked keys are immediately rejected by the auth middleware.
List and monitor API keys (the hash is never displayed):
-- List all active keys
SELECT
id,
name,
user_id,
permissions,
last_used_at,
created_at,
expires_at,
revoked
FROM api_keys
WHERE revoked = false
ORDER BY created_at DESC;
-- Find unused keys (potential security risk)
SELECT id, name, user_id, created_at
FROM api_keys
WHERE last_used_at IS NULL
AND created_at < NOW() - INTERVAL '30 days';
-- Find expired keys (cleanup candidates)
SELECT id, name, user_id, expires_at
FROM api_keys
WHERE expires_at < NOW();Available permissions:
read- Read-only access to all GET endpointswrite- Create/update/delete operationsadmin- Full access (equivalent to*)*- Wildcard, grants all permissions- Custom permissions can be checked with
RequirePermission("custom")middleware
Permission examples:
-- Read-only dashboard access
SELECT create_api_key('Dashboard Reader', 'user1', ARRAY['read'], NULL);
-- Trading bot with write access
SELECT create_api_key('Trading Bot', 'bot1', ARRAY['read', 'write'], NULL);
-- Admin access
SELECT create_api_key('Admin Console', 'admin', ARRAY['admin'], NULL);- Never log or expose raw API keys - Only the SHA-256 hash is stored
- Use HTTPS in production - Set
api.auth.require_https: true - Set expiration dates for temporary access
- Audit key usage - Monitor
last_used_atfor anomalies - Revoke unused keys - Remove keys that haven't been used in 90+ days
- Use minimal permissions - Grant only necessary permissions
- Rotate regularly - At least every 90 days for long-lived keys
Run periodically to clean up expired and revoked keys:
-- Delete keys that have been revoked for more than 30 days
DELETE FROM api_keys
WHERE revoked = true
AND updated_at < NOW() - INTERVAL '30 days';
-- Delete expired keys older than 30 days
DELETE FROM api_keys
WHERE expires_at < NOW() - INTERVAL '30 days';If a key is compromised:
# 1. Immediately revoke the key
psql -d cryptofunk -c "UPDATE api_keys SET revoked = true WHERE key_hash = '$(echo -n '<raw-key>' | sha256sum | cut -d' ' -f1)';"
# 2. Check audit logs for unauthorized access
grep '<key-id>' /var/log/cryptofunk/audit.log
# 3. Review recent API activity
psql -d cryptofunk -c "SELECT * FROM audit_logs WHERE api_key_id = '<key-id>' ORDER BY created_at DESC LIMIT 100;"
# 4. Create new key if needed
psql -d cryptofunk -c "SELECT create_api_key('Replacement Key', 'user', ARRAY['read', 'write'], NULL);"For questions or issues with secret rotation:
- Create an issue in GitHub: cryptofunk/issues
- Consult
docs/ALERT_RUNBOOK.mdfor operational procedures - Review
CLAUDE.mdfor architecture details