veza/docs/PRODUCTION_DEPLOYMENT.md
senke 712bfb6b8c config(template): add comprehensive .env.template
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
2026-01-29 23:32:18 +01:00

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)
  • .env files in .gitignore
  • COOKIE_SECURE=true in production
  • COOKIE_SAME_SITE=strict in 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)