Commit Graph
2 Commits
Author SHA1 Message Date
bvandeusen a85c53ba2c Trust proxy headers by hop count, and log every credential event
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
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
bvandeusenandClaude Opus 5 02c932260e Updater signing key, a rising dev version, and a production compose
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m33s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m37s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Three things, all needed before the update loop can be tested.

The public signing key is committed. Verified before trusting it: algorithm
`Ed`, key ID 90E96FEA2F6D9B6A matching its own comment, 32-byte Ed25519 key.

Dev builds now carry a version that RISES. Every build took its version from
Cargo.toml, so each one was 0.1.0 — an installed 0.1.0 would read a manifest
advertising 0.1.0, conclude it was current, and never update. The rolling
channel would have looked broken while working exactly as written. Dev builds
are now 0.1.<ci-run-number>, from one helper shared by both bundle jobs and the
manifest writer, because three separate derivations of "what version is this"
is three chances for the binary and the manifest to disagree.

Plain semver, not a `-dev.N` prerelease: prerelease versions sort BELOW the
release they qualify, so a tagged build would never update to a newer dev one,
and Windows installer metadata wants a numeric X.Y.Z regardless. Bumping the
minor still beats any dev build on the old line — 0.2.0 > 0.1.2932.

The Windows job also gets the signing environment it was missing, so its NSIS
installer is signed too. Without that the manifest would have had a Linux entry
and nothing for the platform actually being tested.

docker-compose.yml is now the production stack, per request: it pulls the
published image instead of building, keeps Postgres OFF the host network, sets
restart policies, health checks and log rotation, and refuses to start without
a POSTGRES_PASSWORD rather than shipping a known one. Volume names are
deliberately unchanged so an existing deployment upgrades in place instead of
silently coming up against an empty database. Development keeps its own
clearly-named file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
2026-07-27 10:33:17 -04:00