-
-
Notifications
You must be signed in to change notification settings - Fork 33
Zitadel Production Security
This page covers the security features and configuration required for production deployments.
Before going live, ensure all of the following are addressed:
-
APP_ENV=productionis set - Zitadel master key is a unique 32-character random string (not the default placeholder)
- All PostgreSQL passwords are unique and strong (postgres, zitadel, litcal users)
- Zitadel default admin password has been changed after first login
-
JWT_SECRETis a unique, randomly generated 64+ character hex string -
ADMIN_PASSWORD_HASHis set with a strong Argon2id hash -
ALLOW_ENV_ADMIN_FALLBACK=falsein production -
CORS_ALLOWED_ORIGINSlists specific origins (not*) - HTTPS is configured (either directly or via reverse proxy)
-
HTTPS_ENFORCEMENT=true -
X-Forwarded-Protoheader is set by the reverse proxy - Rate limit storage path is persistent (not
/tmp) - Adminer is removed or restricted in production
- Database backups are configured
- Authentication logs are being monitored
IP-based rate limiting protects the /auth/login endpoint against brute-force attacks:
- Failed login attempts are tracked per IP address
- After exceeding the limit, further attempts are blocked with HTTP 429
- The
Retry-Afterheader indicates when the client can retry - Successful login clears the rate limit for that IP
RATE_LIMIT_LOGIN_ATTEMPTS=5 # Max failed attempts (default: 5)
RATE_LIMIT_LOGIN_WINDOW=900 # Window in seconds (default: 900 = 15 minutes)
RATE_LIMIT_STORAGE_PATH=/var/lib/litcal/rate_limits # Persistent storage pathHTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 847
{
"type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429",
"title": "Too Many Requests",
"status": 429,
"detail": "Too many login attempts. Please try again later.",
"retryAfter": 847
}In staging and production environments, the API requires HTTPS for all authentication and admin endpoints (/auth/*, /admin/*, /applications/*). Non-HTTPS requests receive HTTP 403.
The API detects HTTPS via (in order):
$_SERVER['REQUEST_SCHEME'] === 'https'$_SERVER['HTTPS'] === 'on'$_SERVER['SERVER_PORT'] === '443'-
$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'(from reverse proxy)
When TLS terminates at the reverse proxy, the X-Forwarded-Proto header must be set. See Production Deployment for proxy configuration.
HTTPS_ENFORCEMENT=falseOnly disable this if HTTPS is handled externally and you understand the implications.
- Minimum 32 characters
- Must not contain placeholder patterns in staging/production
In staging and production, the API rejects secrets containing:
change-this, change-me, replace-this, replace-me, your-secret, my-secret, secret-key, example, placeholder, default, insecure, password, test-secret, dev-secret, xxxxxxxx
php -r "echo bin2hex(random_bytes(32));"
# or
openssl rand -hex 32Authentication tokens are stored in HttpOnly cookies:
| Cookie | Path | SameSite | Secure | Purpose |
|---|---|---|---|---|
litcal_access_token |
/ |
Lax | Over HTTPS | Access token for API requests |
litcal_refresh_token |
/auth |
Strict | Over HTTPS | Refresh token (auth endpoints only) |
These cookies:
- Cannot be accessed by JavaScript (XSS protection)
- Are automatically sent with requests
- Include the
Secureflag when served over HTTPS
For cross-origin requests with cookies, wildcard origins (*) are not supported. Specify allowed origins explicitly:
CORS_ALLOWED_ORIGINS=https://litcal.johnromanodorazio.com,https://www.liturgicalcalendar.comAuth endpoint error responses only reflect validated origins as a security measure.
| Environment | Behavior |
|---|---|
development / test
|
Falls back to default password "password" if hash is invalid/missing |
staging / production
|
Requires valid ADMIN_PASSWORD_HASH (throws exception if missing) |
php -r "echo password_hash('your-secure-password', PASSWORD_ARGON2ID);"Authentication events are logged to:
-
logs/auth-YYYY-MM-DD.log(plain text) -
logs/auth.json-YYYY-MM-DD.log(JSON format)
| Level | Event |
|---|---|
| INFO | Successful logins |
| INFO | Logouts |
| WARNING | Failed login attempts |
| WARNING | Rate-limited requests |
[2025-12-06 10:30:00] INFO: Login successful
username: admin
client_ip: 192.168.1.100
[2025-12-06 10:35:00] WARNING: Login failed
username: attacker
client_ip: 192.168.1.50
reason: Invalid credentials
remaining_attempts: 2
[2025-12-06 10:40:00] WARNING: Login rate limited
client_ip: 192.168.1.50
retry_after: 600
- Multiple failed login attempts from the same IP
- Rate-limited requests appearing in logs
- Attempts from multiple IPs targeting the same account
- Review auth logs for attack patterns
- Consider temporarily blocking offending IPs at the firewall level
- If an account may be compromised, rotate the
ADMIN_PASSWORD_HASH - Rotate
JWT_SECRETto invalidate all existing tokens - Review and potentially increase rate limit thresholds
For Users
For Webmasters
For Liturgists
For Developers
For Contributors
Testing
Authentication & RBAC