Created centralized environment template with all configuration variables documented and categorized. Categories: - REQUIRED: DATABASE_URL, JWT_SECRET (min 32 chars), REDIS - RECOMMENDED: SENTRY_DSN, COOKIE_SECURE, CORS_ALLOWED_ORIGINS - OPTIONAL: RABBITMQ, SMTP, CLAMAV, S3 Features: - Clear documentation for each variable - Default values specified - Validation rules documented - Environment-specific guidance (dev vs prod) - Security notes for sensitive values Impact: Single source of truth for configuration, reduces config drift. Fixes: P3.4 (part 1) from audit AUDIT_TEMP_29_01_2026.md
6.2 KiB
6.2 KiB
Production Deployment Guide — Veza
Overview
This guide documents how to securely deploy Veza to production with proper secrets management. Never commit secrets to Git.
🔐 Secrets Management
Required Secrets
The following secrets must be injected at runtime:
| Secret | Description | Example Source |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | K8s Secret, AWS RDS |
JWT_SECRET |
Token signing key (min 32 chars) | AWS Secrets Manager, Vault |
REDIS_PASSWORD |
Redis authentication | K8s Secret |
SENTRY_DSN |
Error tracking endpoint | Sentry dashboard |
SMTP_PASSWORD |
Email service password | AWS SES, SendGrid |
Optional Secrets
| Secret | Description | Required If |
|---|---|---|
RABBITMQ_URL |
Message queue connection | Using async events |
CLAMAV_* |
Antivirus configuration | File upload scanning enabled |
📦 Deployment Methods
Method 1: Kubernetes Secrets
1. Create secrets:
kubectl create secret generic veza-backend-secrets \
--from-literal=database-url="postgresql://user:pass@host:5432/veza" \
--from-literal=jwt-secret="$(openssl rand -base64 32)" \
--from-literal=redis-password="$(openssl rand -base64 16)" \
--namespace=production
2. Reference in deployment:
# k8s/backend-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: veza-backend
spec:
template:
spec:
containers:
- name: api
image: veza/backend:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: veza-backend-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: veza-backend-secrets
key: jwt-secret
Method 2: AWS Secrets Manager
1. Store secrets:
aws secretsmanager create-secret \
--name veza/production/database \
--secret-string '{"url":"postgresql://..."}'
aws secretsmanager create-secret \
--name veza/production/jwt \
--secret-string '{"secret":"..."}'
2. Inject at runtime (ECS task definition):
{
"containerDefinitions": [{
"name": "veza-backend",
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:veza/production/database:url::"
},
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:veza/production/jwt:secret::"
}
]
}]
}
Method 3: HashiCorp Vault
1. Store secrets:
vault kv put secret/veza/production \
database_url="postgresql://..." \
jwt_secret="..." \
redis_password="..."
2. Inject via Vault Agent:
# vault-agent-config.hcl
template {
source = "/etc/veza/.env.production.tpl"
destination = "/etc/veza/.env.production"
}
Template file:
# /etc/veza/.env.production.tpl
DATABASE_URL={{ with secret "secret/veza/production" }}{{ .Data.data.database_url }}{{ end }}
JWT_SECRET={{ with secret "secret/veza/production" }}{{ .Data.data.jwt_secret }}{{ end }}
Method 4: Docker Compose (Staging)
For staging/testing only, not production:
# docker-compose.production.yml
services:
backend:
image: veza/backend:latest
env_file:
- .env.production.local # NOT committed to Git
environment:
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
Create .env.production.local (add to .gitignore):
DATABASE_URL=postgresql://user:pass@postgres:5432/veza
JWT_SECRET=$(openssl rand -base64 32)
REDIS_PASSWORD=$(openssl rand -base64 16)
✅ Security Checklist
Before Deployment
- All secrets stored in secrets manager (not Git)
.envfiles in.gitignoreCOOKIE_SECURE=truein productionCOOKIE_SAME_SITE=strictin production- CORS origins explicitly listed (no wildcard)
- JWT secret minimum 32 characters
- Database credentials rotated
- HTTPS enabled for all endpoints
- Sentry DSN configured for error tracking
After Deployment
- Health endpoint returns 200:
curl https://api.veza.com/api/v1/health - CORS headers present on OPTIONS requests
- Login/logout flow works end-to-end
- No secrets visible in logs
- Database migrations applied successfully
- Redis connection established
- Email sending works (if configured)
🔄 Secret Rotation
JWT Secret Rotation
Impact: All active sessions invalidated
# 1. Generate new secret
NEW_SECRET=$(openssl rand -base64 32)
# 2. Update in secrets manager
kubectl patch secret veza-backend-secrets \
-p "{\"data\":{\"jwt-secret\":\"$(echo -n $NEW_SECRET | base64)\"}}"
# 3. Rolling restart
kubectl rollout restart deployment/veza-backend
# 4. Monitor for errors
kubectl logs -f deployment/veza-backend
Database Password Rotation
Impact: Requires coordinated update
# 1. Create new user with new password in PostgreSQL
# 2. Update DATABASE_URL in secrets manager
# 3. Rolling restart with zero downtime
# 4. Revoke old user after verification
🚨 Troubleshooting
"Invalid JWT secret" errors
Cause: Secret not injected or too short
Fix:
# Verify secret is set
kubectl get secret veza-backend-secrets -o jsonpath='{.data.jwt-secret}' | base64 -d | wc -c
# Should be >= 32 characters
"Database connection failed"
Cause: DATABASE_URL not set or incorrect
Fix:
# Test connection from pod
kubectl exec -it deployment/veza-backend -- psql $DATABASE_URL -c "SELECT 1"
"CORS errors in production"
Cause: Frontend domain not in CORS_ALLOWED_ORIGINS
Fix:
# Update backend .env.production
CORS_ALLOWED_ORIGINS=https://app.veza.com,https://www.veza.com
📚 References
Last Updated: 2026-01-29
Audit Reference: AUDIT_TEMP_29_01_2026.md (P2.3)