Files
thoughtsync/.env.example
T
bvandeusen a85c53ba2c
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 9s
CI & Build / integration (push) Successful in 12s
CI & Build / Build & push image (push) Successful in 32s
Trust proxy headers by hop count, and log every credential event
Operator, before exposing the instance: *"I'd expect that we should have a proxy
hops setting for how many proxy hops we should trust a shared real-ip at… and is
there any session logging."* Neither existed, and the first one was a real hole.

**The address was forgeable.** `client_address()` read the LEFTMOST
`X-Forwarded-For` entry — nominally "the original client", and precisely the one a
caller controls, because anything they send arrives before what proxies append. So
`curl -H "X-Forwarded-For: 1.2.3.4"`, rotated per request, minted a fresh
rate-limit bucket every time.

Concretely: stuffing ONE account stayed limited (the account key is unforgeable
and that is why it exists), but spraying MANY accounts from one source was not —
each account got its own budget, and the per-address cap meant to bound the total
was defeated by a header. On a LAN that is nothing. It is not nothing on a public
host.

Now it counts in from the RIGHT by `THOUGHTSYNC_TRUSTED_PROXY_HOPS`, default 1.
Each hop appends what it saw, so the rightmost entries are the ones our own
infrastructure wrote and a forged prefix lands to the left of them where it can
never be selected — proven for the honest, forged, padded, CDN and
shorter-than-configured cases. 0 ignores the header entirely; 2 is Cloudflare in
front of a proxy. Too high is the dangerous direction, so a header shorter than
configured falls back to the socket address rather than reaching further left.

`X-Forwarded-Proto` had the same bug and now shares the same rule. Both live in a
new `proxy.py` rather than being written twice — two places holding one decision
is how issue 2183 happened, and this is the same decision.

Env rather than the Settings UI, against rule 25's usual pull: it is deployment
topology rather than preference, and the limiter consults it BEFORE opening a
database connection, which is the entire point of checking a throttle before doing
expensive work. Easy to move if that reads wrong.

**And there was no logging at all** — `auth.py` had no logger, and the only record
of anything was `device_tokens.last_used_at`. Sign-ins, failures, throttle trips,
new accounts and device-token issuance now all log, with the attempted email and
the trusted address. Deliberately including the email: it is the operator's own
server, and "somebody failed a login" without saying against which account is not
actionable.

`basicConfig` at INFO in `create_app`, because hypercorn configures its own loggers
and leaves the root at WARNING — without it every line above would have gone
nowhere, which is a worse failure than not writing them.

This is the app log, not an audit table. Not queryable, not retained past log
rotation. The table is task 2939; this is what makes the next few days observable.
2026-08-23 15:12:14 -04:00

73 lines
3.2 KiB
Bash

# ThoughtSync production settings. Copy to `.env` and edit:
#
# cp .env.example .env
#
# Only POSTGRES_PASSWORD has no default — compose refuses to start without it.
# Everything else here is optional. Anything NOT in this file (site name, signups,
# attachment limits, trash retention, link previews) is configured in the admin
# Settings UI and stored in the database, not here.
# --- required ---------------------------------------------------------------
# Generate one and keep it: changing it later means also changing it inside the
# database, or Postgres will reject the app's connection.
#
# openssl rand -base64 24 | tr -d '/+=' | head -c 32
#
# Stick to letters and digits. This value goes into a connection URL, so a `@`,
# `/`, `:` or `#` in it will be misparsed as URL structure rather than password.
POSTGRES_PASSWORD=
# --- optional ---------------------------------------------------------------
# Which build to run.
#
# latest tracks the `main` branch — the production line (default)
# dev tracks the `dev` branch — newer, less settled
# <commit sha> pins one exact build; every push publishes one, and this is
# the rollback lever when an upgrade misbehaves
#
# NOTE: `main` can sit well behind `dev`. If a feature you expect is missing,
# check which branch it actually landed on before assuming a bug.
#THOUGHTSYNC_TAG=latest
# The host port the app is published on.
#THOUGHTSYNC_PORT=5000
# Which interface to bind. The default (all interfaces) is what lets desktop
# clients on your network reach the server. Behind a reverse proxy, set this to
# 127.0.0.1 so only the proxy can talk to it.
#THOUGHTSYNC_BIND=0.0.0.0
# How many proxies sit in front of this app. This is a SECURITY setting, not a
# preference: it decides which entry of X-Forwarded-For is believed, and therefore
# whether a caller can forge their own address and slip the per-address rate limit.
#
# 0 nothing in front — the app is directly exposed
# 1 one reverse proxy terminating TLS (the default, and the usual case)
# 2 a CDN in front of that proxy, e.g. Cloudflare
#
# Set it to the number you actually run. Too HIGH is the dangerous direction — it
# starts trusting entries no proxy of yours wrote. Too low just means callers share
# a rate-limit bucket.
#THOUGHTSYNC_TRUSTED_PROXY_HOPS=1
# How much the app says. Credential events (sign-ins, failures, throttles, new
# accounts, device tokens issued) are logged at INFO and read with
# `docker compose logs app`.
#THOUGHTSYNC_LOG_LEVEL=INFO
# Database identity. Changing these AFTER the first start does not rename anything
# that already exists — the volume keeps whatever the first run created.
#POSTGRES_USER=thoughtsync
#POSTGRES_DB=thoughtsync
# --- a note on HTTPS --------------------------------------------------------
#
# The app marks its session cookie Secure automatically when a request arrives over
# HTTPS, directly or via a proxy setting X-Forwarded-Proto — no setting needed.
#
# Worth knowing if you use the desktop app: typing a bare hostname there defaults to
# https://, deliberately, so a device token never crosses the wire in cleartext by
# accident. Serving over plain HTTP means typing the `http://` yourself.