Auto-derive COOKIE_SECURE from ENVIRONMENT setting

COOKIE_SECURE now defaults to None and auto-derives from ENVIRONMENT
(production → true, else false) via a Pydantic model_validator. Explicit
env var values are still respected as an override escape hatch. Adds a
startup log line showing the resolved value. Restructures .env.example
with clear sections and inline docs, removes redundant production
checklist block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-02 15:38:54 +08:00
parent 21aa670a39
commit 0c7d057654
3 changed files with 34 additions and 23 deletions

View File

@ -1,32 +1,34 @@
# ──────────────────────────────────────
# Database
# ──────────────────────────────────────
POSTGRES_USER=umbra
POSTGRES_PASSWORD=changeme_in_production
POSTGRES_DB=umbra
# Backend
DATABASE_URL=postgresql+asyncpg://umbra:changeme_in_production@db:5432/umbra
# ──────────────────────────────────────
# Application
# ──────────────────────────────────────
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=change-this-to-a-random-secret-key-in-production
# Environment (development|production — controls Swagger/ReDoc visibility)
# ENVIRONMENT=development
# development | production — controls Swagger/ReDoc visibility and cookie defaults
ENVIRONMENT=development
# CORS allowed origins (comma-separated, default: http://localhost:5173)
# CORS_ORIGINS=http://localhost:5173
# CORS_ORIGINS=https://umbra.example.com
# Timezone (applied to backend + db containers via env_file)
TZ=Australia/Perth
# Session cookie security
# Set to true when serving over HTTPS. Required before any TLS deployment.
# COOKIE_SECURE=true
# ──────────────────────────────────────
# Integrations
# ──────────────────────────────────────
OPENWEATHERMAP_API_KEY=your-openweathermap-api-key
# Production security checklist (enable all before any non-internal deployment):
# 1. Set SECRET_KEY to output of: openssl rand -hex 32
# 2. Set POSTGRES_PASSWORD to a strong unique value
# 3. Set ENVIRONMENT=production (disables Swagger/ReDoc on backend:8000)
# 4. Set COOKIE_SECURE=true (requires TLS termination at nginx or upstream)
# 5. Add HSTS to nginx.conf: add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# 6. Complete user_id migration (migration 026) before enabling multi-user accounts
# ──────────────────────────────────────
# Overrides (rarely needed)
# ──────────────────────────────────────
# COOKIE_SECURE auto-derives from ENVIRONMENT (production → true).
# Only set explicitly to override, e.g. false for a non-TLS prod behind a proxy.
# COOKIE_SECURE=false

View File

@ -143,17 +143,13 @@ python3 -c "import secrets; print(secrets.token_hex(32))"
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
# or: openssl rand -base64 24
# Set ENVIRONMENT to disable Swagger/ReDoc
# Set ENVIRONMENT to disable Swagger/ReDoc and auto-enable secure cookies
ENVIRONMENT=production
# Enable secure cookies (requires HTTPS)
COOKIE_SECURE=true
```
Additionally for production:
- Place behind a reverse proxy with TLS termination (e.g., Caddy, Traefik, or nginx with Let's Encrypt)
- Set `COOKIE_SECURE=true` to enforce HTTPS-only session cookies
- Set `ENVIRONMENT=production` to disable API documentation endpoints
- Set `ENVIRONMENT=production` — this disables API docs and auto-enables HTTPS-only session cookies (`COOKIE_SECURE` derives from `ENVIRONMENT`; override with `COOKIE_SECURE=false` if running non-TLS prod behind a proxy)
- Set `CORS_ORIGINS` to your actual domain (e.g., `https://umbra.example.com`)
- Consider adding HSTS headers at the TLS-terminating proxy layer

View File

@ -1,4 +1,5 @@
import sys
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
@ -6,7 +7,7 @@ class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/umbra"
SECRET_KEY: str = "your-secret-key-change-in-production"
ENVIRONMENT: str = "development"
COOKIE_SECURE: bool = False
COOKIE_SECURE: bool | None = None # Auto-derives from ENVIRONMENT if not explicitly set
OPENWEATHERMAP_API_KEY: str = ""
# Session config — sliding window
@ -28,8 +29,20 @@ class Settings(BaseSettings):
case_sensitive=True
)
@model_validator(mode="after")
def derive_cookie_secure(self) -> "Settings":
if self.COOKIE_SECURE is None:
self.COOKIE_SECURE = self.ENVIRONMENT == "production"
assert self.COOKIE_SECURE is not None # type narrowing
return self
settings = Settings()
print(
f"INFO: COOKIE_SECURE={settings.COOKIE_SECURE} "
f"(ENVIRONMENT={settings.ENVIRONMENT})",
file=sys.stderr,
)
if settings.SECRET_KEY == "your-secret-key-change-in-production":
if settings.ENVIRONMENT != "development":