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:
parent
21aa670a39
commit
0c7d057654
34
.env.example
34
.env.example
@ -1,32 +1,34 @@
|
|||||||
|
# ──────────────────────────────────────
|
||||||
# Database
|
# Database
|
||||||
|
# ──────────────────────────────────────
|
||||||
POSTGRES_USER=umbra
|
POSTGRES_USER=umbra
|
||||||
POSTGRES_PASSWORD=changeme_in_production
|
POSTGRES_PASSWORD=changeme_in_production
|
||||||
POSTGRES_DB=umbra
|
POSTGRES_DB=umbra
|
||||||
|
|
||||||
# Backend
|
|
||||||
DATABASE_URL=postgresql+asyncpg://umbra:changeme_in_production@db:5432/umbra
|
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
|
SECRET_KEY=change-this-to-a-random-secret-key-in-production
|
||||||
|
|
||||||
# Environment (development|production — controls Swagger/ReDoc visibility)
|
# development | production — controls Swagger/ReDoc visibility and cookie defaults
|
||||||
# ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
|
||||||
# CORS allowed origins (comma-separated, default: http://localhost:5173)
|
# 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)
|
# Timezone (applied to backend + db containers via env_file)
|
||||||
TZ=Australia/Perth
|
TZ=Australia/Perth
|
||||||
|
|
||||||
# Session cookie security
|
# ──────────────────────────────────────
|
||||||
# Set to true when serving over HTTPS. Required before any TLS deployment.
|
|
||||||
# COOKIE_SECURE=true
|
|
||||||
|
|
||||||
# Integrations
|
# Integrations
|
||||||
|
# ──────────────────────────────────────
|
||||||
OPENWEATHERMAP_API_KEY=your-openweathermap-api-key
|
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
|
# Overrides (rarely needed)
|
||||||
# 2. Set POSTGRES_PASSWORD to a strong unique value
|
# ──────────────────────────────────────
|
||||||
# 3. Set ENVIRONMENT=production (disables Swagger/ReDoc on backend:8000)
|
# COOKIE_SECURE auto-derives from ENVIRONMENT (production → true).
|
||||||
# 4. Set COOKIE_SECURE=true (requires TLS termination at nginx or upstream)
|
# Only set explicitly to override, e.g. false for a non-TLS prod behind a proxy.
|
||||||
# 5. Add HSTS to nginx.conf: add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
# COOKIE_SECURE=false
|
||||||
# 6. Complete user_id migration (migration 026) before enabling multi-user accounts
|
|
||||||
|
|||||||
@ -143,17 +143,13 @@ python3 -c "import secrets; print(secrets.token_hex(32))"
|
|||||||
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
|
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
|
||||||
# or: openssl rand -base64 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
|
ENVIRONMENT=production
|
||||||
|
|
||||||
# Enable secure cookies (requires HTTPS)
|
|
||||||
COOKIE_SECURE=true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally for production:
|
Additionally for production:
|
||||||
- Place behind a reverse proxy with TLS termination (e.g., Caddy, Traefik, or nginx with Let's Encrypt)
|
- 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` — 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 `ENVIRONMENT=production` to disable API documentation endpoints
|
|
||||||
- Set `CORS_ORIGINS` to your actual domain (e.g., `https://umbra.example.com`)
|
- Set `CORS_ORIGINS` to your actual domain (e.g., `https://umbra.example.com`)
|
||||||
- Consider adding HSTS headers at the TLS-terminating proxy layer
|
- Consider adding HSTS headers at the TLS-terminating proxy layer
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
|
from pydantic import model_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
@ -6,7 +7,7 @@ class Settings(BaseSettings):
|
|||||||
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/umbra"
|
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/umbra"
|
||||||
SECRET_KEY: str = "your-secret-key-change-in-production"
|
SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||||
ENVIRONMENT: str = "development"
|
ENVIRONMENT: str = "development"
|
||||||
COOKIE_SECURE: bool = False
|
COOKIE_SECURE: bool | None = None # Auto-derives from ENVIRONMENT if not explicitly set
|
||||||
OPENWEATHERMAP_API_KEY: str = ""
|
OPENWEATHERMAP_API_KEY: str = ""
|
||||||
|
|
||||||
# Session config — sliding window
|
# Session config — sliding window
|
||||||
@ -28,8 +29,20 @@ class Settings(BaseSettings):
|
|||||||
case_sensitive=True
|
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()
|
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.SECRET_KEY == "your-secret-key-change-in-production":
|
||||||
if settings.ENVIRONMENT != "development":
|
if settings.ENVIRONMENT != "development":
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user