This document defines the secrets management strategy, rotation procedures, and security best practices for the Nivesh financial platform.
- Secrets Management Overview
- Local Development
- Docker Compose
- Kubernetes Secrets (Production)
- Secret Rotation Procedures
- Pre-commit Secret Detection
- CI/CD Secret Scanning
- Incident Response
| Environment | Strategy | Rotation |
|---|---|---|
| Local dev | .env file (git-ignored) |
N/A |
| CI/CD | GitHub Actions Secrets | On key events |
| Staging | ExternalSecrets → HashiCorp Vault | Automated |
| Production | ExternalSecrets → HashiCorp Vault | Automated + audit |
Golden Rules:
- Never commit real secrets to version control
- Never use default/example passwords in production
- All secrets must be rotatable without downtime
- All secret access must be auditable
-
Copy the env example files:
cp docker-compose.env.example .env # Docker Compose passwords cp backend/.env.example backend/.env # Backend app config cp ml-services/.env.example ml-services/.env # ML services config
-
Generate secure local secrets:
# JWT Secret (min 32 chars) openssl rand -hex 32 # Encryption Key (exactly 32 chars) openssl rand -hex 16 # Database passwords openssl rand -base64 24
-
The
.envfiles are listed in.gitignoreand will never be committed.
All credentials in docker-compose.yml use ${VAR} substitution. No passwords are hardcoded.
Docker Compose reads variables from:
- Shell environment variables
.envfile in the project root (auto-loaded)
Required variables (will fail fast if missing):
POSTGRES_PASSWORDNEO4J_PASSWORDMONGO_PASSWORDREDIS_PASSWORDCLICKHOUSE_PASSWORD
Optional variables (have defaults for dev):
POSTGRES_USER(default:nivesh_user)POSTGRES_DB(default:nivesh_db)GRAFANA_ADMIN_USER(default:admin)
-
Install ExternalSecrets Operator:
helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets external-secrets/external-secrets \ -n external-secrets --create-namespace
-
Configure ClusterSecretStore:
apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: name: vault-backend spec: provider: vault: server: "https://vault.nivesh.internal:8200" path: "secret" version: "v2" auth: kubernetes: mountPath: "kubernetes" role: "nivesh-backend"
-
Create ExternalSecret for backend:
apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: nivesh-backend-secrets namespace: nivesh spec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: vault-backend target: name: nivesh-backend-secret creationPolicy: Owner data: - secretKey: JWT_SECRET remoteRef: key: nivesh/backend property: jwt_secret - secretKey: ENCRYPTION_KEY remoteRef: key: nivesh/backend property: encryption_key - secretKey: DATABASE_URL remoteRef: key: nivesh/backend property: database_url - secretKey: REDIS_PASSWORD remoteRef: key: nivesh/backend property: redis_password - secretKey: NEO4J_PASSWORD remoteRef: key: nivesh/backend property: neo4j_password - secretKey: MONGO_PASSWORD remoteRef: key: nivesh/backend property: mongo_password - secretKey: CLICKHOUSE_PASSWORD remoteRef: key: nivesh/backend property: clickhouse_password
-
Reference in Deployment:
envFrom: - secretRef: name: nivesh-backend-secret
# Install Sealed Secrets controller
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# Encrypt a secret
echo -n "my-jwt-secret-value" | kubeseal --raw \
--from-file=/dev/stdin --namespace nivesh --name nivesh-backend-secret-
Generate a new JWT secret:
NEW_SECRET=$(openssl rand -hex 32) -
Update Vault (or your secrets store):
vault kv put nivesh/backend jwt_secret=$NEW_SECRET -
Deploy with dual-secret support (overlap window):
- Set
JWT_SECRETto the new value - Set
JWT_SECRET_PREVIOUSto the old value - The auth middleware should accept tokens signed with either secret during the overlap window (default: 7 days)
- Set
-
After the overlap window, remove
JWT_SECRET_PREVIOUS
⚠️ Critical: Encryption key rotation requires re-encrypting all existing encrypted data.
- Generate new key:
openssl rand -hex 16 - Run the re-encryption migration script (TBD)
- Update the key in Vault
- Deploy and verify
- Create new credentials in the database
- Update Vault with new credentials
- ExternalSecrets auto-syncs within
refreshInterval - Rolling restart of affected services
- Drop old credentials after verification
- Immediately revoke all API keys associated with the departing member
- Regenerate any shared API keys
- Audit access logs for the 30 days prior to offboarding
We use detect-secrets to prevent accidentally committing real secrets.
pip install pre-commit detect-secrets
pre-commit install
# Create initial baseline (marks existing false positives)
detect-secrets scan > .secrets.baseline- Pre-commit hook runs automatically on
git commit - To scan all files manually:
pre-commit run detect-secrets --all-files - To update baseline after resolving findings:
detect-secrets scan --update .secrets.baseline
# .github/workflows/secret-scan.yml
name: Secret Scan
on: [pull_request]
jobs:
detect-secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install detect-secrets
- run: detect-secrets scan --baseline .secrets.baseline
- run: detect-secrets audit --report --baseline .secrets.baselineIf available, enable:
- Secret scanning — detects known secret patterns from 100+ providers
- Push protection — blocks pushes containing secrets before they reach the repo
If a secret is accidentally committed:
- Immediately rotate the exposed secret
- Force-push to remove from Git history:
git filter-branch --force --index-filter \ 'git rm --cached --ignore-unmatch <file>' HEAD git push origin --force --all - Audit access logs for unauthorized usage
- Notify the security team and affected users
- Post-mortem — update pre-commit rules to prevent recurrence
Important: Even after force-pushing, assume the secret is compromised. GitHub caches commits and forks may retain the data. Always rotate.