Serve behind Traefik and rate limit password guesses

The deployment is a public hostname behind Traefik rather than a LAN-only
box, which changes two things.

TLS is now terminated by the proxy, so Basic credentials are no longer in
cleartext. The container publishes no ports: doing so would leave an
unencrypted copy of the app on the host, bypassing the proxy. The hostname
lives in .env rather than compose.yaml, so no infrastructure detail is
committed and the MIT publication option stays open.

The password is now the only thing between the internet and the app, and a
500 ms sleep is not a defence at that exposure. Wrong guesses are rate
limited per client address: five in a burst, then one per ten seconds,
answered with 429.

Two details that decide whether this works at all:

- a request with no Authorization header is not charged. That is the
  handshake every browser session opens with, and counting it would lock the
  household out for simply opening the app a few times.
- X-Forwarded-For is believed only when the connection arrived from a private
  address, i.e. through the proxy, and then only its last entry, which is the
  one the proxy observed. A direct client could otherwise forge a fresh
  address per attempt and walk past the limiter entirely.

None of this substitutes for a strong password. It removes brute force as a
practical route, nothing more. PRD §3, §9 and §10 are updated: "no external
internet exposure" is no longer true.
This commit is contained in:
Esa Kataja
2026-09-05 20:12:28 +03:00
parent b624712b88
commit eb95bd0a03
9 changed files with 311 additions and 28 deletions
+17 -3
View File
@@ -184,9 +184,23 @@ that is. Get them from `id -u` and `id -g`.
## Security
Access is a single shared password over HTTP Basic — no accounts, no
sessions. Credentials are compared in constant time, but Basic auth sends
them in cleartext, so this belongs on a private LAN. Put TLS in front of it
before exposing it anywhere else.
sessions. Credentials are compared in constant time over SHA-256 digests, so
neither the password nor its length leaks through timing.
The app is served on a public hostname behind Traefik, which terminates TLS,
so the credentials are encrypted in transit. That leaves the password as the
only thing between the internet and the app, so wrong guesses are rate
limited per client address: five in a burst, then one per ten seconds,
answered with `429`. Requests carrying no `Authorization` header are not
charged — that is the handshake every browser session begins with, and
counting it would lock the household out for simply opening the app.
`X-Forwarded-For` is trusted only when the connection came from a private
address, meaning it arrived through the proxy. A client connecting directly
could otherwise forge a new address per attempt and skip the limiter.
**None of this replaces a strong `FOODSTER_PASSWORD`.** Rate limiting removes
brute force as a practical route; it does not make a guessable password safe.
## Mockups