From 9e19fb52190216778fe9a72ac1995fe0626feef6 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr <139972147+ZdenekSrotyr@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:52:53 +0200 Subject: [PATCH] chore(deploy): trust proxy headers + document HTTPS env vars (#48) * chore(deploy): trust proxy headers + document HTTPS env vars - uvicorn: add --proxy-headers --forwarded-allow-ips='*' so the app honors X-Forwarded-Proto/Host from a TLS-terminating reverse proxy (Caddy, Cloudflare Tunnel, nginx, LB). Without this the app saw every request as plain HTTP and built redirect/OAuth URLs from the raw Host, which is fragile behind a proxy. - .env.template: document DOMAIN (enables Secure cookie flag) and new SERVER_URL (deterministic base URL for OAuth callbacks and external links). Grouped under a dedicated HTTPS / REVERSE PROXY section. * chore(deploy): add proxy header flags to Dockerfile CMD and Kamal config Matches the docker-compose changes so non-compose deployments (docker run, Kubernetes, ECS, Kamal) also trust X-Forwarded-Proto/X-Forwarded-For. * fix(auth): align Google OAuth cookie Secure flag with password/email providers Google OAuth set the access_token cookie Secure flag based on the TESTING env var, while password and email providers use DOMAIN. This meant the DOMAIN env var (now documented in config/.env.template) did not actually control Secure for Google cookies. Align all three providers on DOMAIN so the documented behavior holds consistently. --- Dockerfile | 2 +- app/auth/providers/google.py | 8 +++++--- config/.env.template | 20 +++++++++++++++++++- config/deploy.yml | 2 +- docker-compose.override.yml | 2 +- docker-compose.test.yml | 2 +- docker-compose.yml | 2 +- 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 098aaef..c3c2a85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,4 +24,4 @@ RUN uv build --wheel --out-dir /app/dist RUN uv pip install --system --no-cache . EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips", "*"] diff --git a/app/auth/providers/google.py b/app/auth/providers/google.py index 9d5b86e..b91249b 100644 --- a/app/auth/providers/google.py +++ b/app/auth/providers/google.py @@ -111,13 +111,15 @@ async def google_callback(request: Request): request.session.pop("login_next", None), default="/dashboard" ) - # Redirect to target with token in cookie - is_production = os.environ.get("TESTING", "").lower() not in ("1", "true") + # Redirect to target with token in cookie. Match password/email providers: + # Secure only when DOMAIN is set (production with TLS), so the cookie is + # actually sent over plain HTTP in dev. + use_secure = os.environ.get("DOMAIN", "") != "" response = RedirectResponse(url=target, status_code=302) response.set_cookie( key="access_token", value=jwt_token, httponly=True, max_age=86400, samesite="lax", - secure=is_production, + secure=use_secure, ) return response diff --git a/config/.env.template b/config/.env.template index 66122e0..7f0f52d 100644 --- a/config/.env.template +++ b/config/.env.template @@ -44,4 +44,22 @@ SESSION_SECRET= # python -c "import secrets; print(secrets.token_he # DATA_DIR=/data # Default: /data in Docker, ./data locally # LOG_LEVEL=info # debug, info, warning, error # CORS_ORIGINS=http://localhost:3000,http://localhost:8000 -# DOMAIN=data.yourcompany.com # For Caddy TLS (production profile) + +# ── HTTPS / REVERSE PROXY ─────────────────────────── +# Set these when the app runs behind a TLS terminator (Caddy, Cloudflare +# Tunnel, nginx, GCP LB, etc.). The app itself speaks plain HTTP on :8000; +# the terminator is responsible for TLS. +# +# DOMAIN: public hostname. When set, session cookies get the `Secure` flag +# (browser only sends them over HTTPS). Also used by the Caddy +# profile to auto-provision Let's Encrypt certs. +# DOMAIN=data.yourcompany.com +# +# SERVER_URL: absolute base URL used to build OAuth callback URLs and other +# external links. Set this to avoid relying on the incoming +# request's Host header (which a misconfigured proxy can get +# wrong). Must match the redirect URI registered in OAuth apps. +# SERVER_URL=https://data.yourcompany.com +# +# Uvicorn is started with `--proxy-headers --forwarded-allow-ips='*'` so it +# trusts X-Forwarded-Proto / X-Forwarded-For from the reverse proxy. diff --git a/config/deploy.yml b/config/deploy.yml index 444c8b6..68220c8 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -16,7 +16,7 @@ servers: web: hosts: - YOUR_SERVER_IP - cmd: uvicorn app.main:app --host 0.0.0.0 --port 8000 + cmd: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*' options: volume: - /data:/data diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 6f911e5..c3ce141 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -2,7 +2,7 @@ # This file is auto-loaded by `docker compose up` when present services: app: - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --proxy-headers --forwarded-allow-ips='*' volumes: - .:/app - data:/data diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d916a75..317fb6f 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,7 +1,7 @@ services: app: build: . - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*' ports: - "8000:8000" environment: diff --git a/docker-compose.yml b/docker-compose.yml index 94d4932..0b408eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: app: build: . - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*' ports: - "8000:8000" volumes: