feat!: drop the built-in auth in favour of Authelia
check / check (push) Successful in 2m14s

BREAKING CHANGE: PASSWORD is gone and AUTH and CERTRESOLVER are required.
The server's compose.yaml and .env must be updated in the same deploy — the
new image ignores PASSWORD, and the old one refuses to start without it.

Authelia now sits in front of Traefik, so the app was asking for a second
password at the same door. Two prompts, and the weaker of the two was the one
holding a single shared secret with no sessions, no MFA and no revocation.
Deleting it is the whole change: Authelia already does this properly, once,
for every service on the host.

Gone: auth(), challenge(), the whole of throttle.go and its tests, and
golang.org/x/time with them. routes() returns the bare mux, /healthz is an
ordinary route on it, and the smoke script drops sixty -u flags. Roughly 230
lines removed and nothing written to replace them.

What holds the app up now, both asserted in compose.yaml:

- The router names the Authelia middleware through AUTH. Traefik takes a
  router out of service when its middleware does not resolve, so a typo or an
  unset variable fails shut rather than serving the app open.
- The container still publishes no ports, so the proxy is the only thing that
  can reach it. Publishing 8080 would now bypass authentication outright, not
  merely TLS — the comment there says so.

certresolver replaces the bare tls=true, parameterised as CERTRESOLVER: the
server had been carrying that label by hand since the first deploy. Naming a
resolver implies tls=true, so it stays one label.

TestAuth and TestHealthzSkipsAuth are replaced by one test asserting every
route answers without credentials — a 401 from here would now mean auth had
crept back in.
This commit is contained in:
Esa Kataja
2026-09-06 13:31:17 +03:00
parent 6d5f3e3de3
commit 2974ffc621
13 changed files with 159 additions and 446 deletions
+29 -26
View File
@@ -25,12 +25,12 @@ polished, it may be released as FOSS under MIT.
- No grocery list generation (possible future add-on).
- No per-recipe ingredient tracking — meals are just names.
- No calendar/scheduling with times, reminders, or calendar exports.
- No user accounts, per-person profiles, or permissions. A single shared
password gates the whole app (§9).
- No user accounts, per-person profiles, or permissions *in the app*.
Authentication is the reverse proxy's job (§9).
- No nutrition tracking, calorie counting, or dietary-goal optimization.
- No mobile-native apps. Web only (mobile-friendly responsive is enough).
- No per-user accounts or sessions. The app *is* reachable from the internet
(§9, §10), gated by a single shared password over TLS.
- No per-user accounts or sessions in the app. It *is* reachable from the
internet (§9, §10), behind Authelia at the proxy.
## 4. Delivery stages
@@ -72,9 +72,9 @@ weighting to be meaningful (a few weeks of logged meals).
## 5. Users
A single household. One shared instance, no per-person accounts. Anyone on the
home network who knows the shared password can open the app and interact with
it.
A single household. One shared instance, no per-person accounts. Everyone who
gets past Authelia sees and edits the same log; the app draws no distinction
between them.
The interface is written in **Finnish** — every user of this instance is a
Finnish speaker, so there is no i18n layer and no language switcher. Strings
@@ -350,21 +350,21 @@ build and no asset bundler.
to UTC would shift logged dinners to the wrong calendar day. `time/tzdata`
is imported because the runtime image carries no zoneinfo. All date logic
uses that location explicitly and never `time.Local`.
- **Auth**: HTTP Basic with one shared household password read from
`PASSWORD`; the username is ignored. Compared using
`subtle.ConstantTimeCompare` over SHA-256 digests so neither the value nor
its length leaks through timing. `/healthz` is the only route outside auth.
- **Exposure**: the app is served on a public hostname behind Traefik, which
terminates TLS, so Basic credentials are encrypted in transit. A shared
password is therefore the only thing between the internet and the app, and
it is guarded by a per-address rate limiter: five wrong guesses, then one
per ten seconds, answered with `429`. Only requests that actually present
a wrong password spend the allowance — a request with no `Authorization`
header is the normal browser handshake that opens every session.
`X-Forwarded-For` is trusted only when the connection arrived from a
private address, so a direct client cannot forge a new identity per
attempt. None of this substitutes for a strong password; it only removes
brute force as a practical route.
- **Auth**: none in the app. Every route is served unauthenticated, because
the only client that can reach the app is Traefik, which forwards each
request to **Authelia** first. Sessions, brute-force protection and
multi-factor are configured there once for every service on the host.
Deliberately not reimplemented per app: the earlier in-app HTTP Basic layer
meant two prompts for one door, and the weaker of the two was the one
holding a shared password.
- **Exposure**: served on a public hostname behind Traefik, which terminates
TLS. Two invariants carry the whole security model, and both are asserted
in `compose.yaml`. The router names the Authelia middleware through `AUTH`
— unset or misspelt, Traefik takes the router out of service, so a typo
fails shut. And the container publishes no ports, so it is reachable only
over the shared proxy network; publishing `8080` would expose an
unauthenticated plaintext copy on the host. `/healthz` returns only the
version and is safe to bypass in Authelia for monitoring.
- **Containers**: built with Podman in development, run under Docker Compose
in production. Images are OCI, so one image works with both engines.
@@ -406,8 +406,10 @@ the server and run with Docker Compose.
`.env.example`):
Names carry no application prefix: the container namespaces them already.
- `REPO` and `TAG` — image coordinates.
- `PASSWORD` — the shared password. Required; the app refuses to start
without it.
- `AUTH` — the Traefik middleware that authenticates the app, e.g.
`authelia@docker`. Required; it is the app's only access control.
- `HOST` and `CERTRESOLVER` — the hostname Traefik matches on and the
resolver that issues its certificate.
- `DB` — database file path, default `./data/foodster.db`. The directory is
created on startup if missing.
- `ENV` — anything but `prod` is prefixed to the browser tab title, so a
@@ -422,8 +424,9 @@ the server and run with Docker Compose.
doing so would put 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.
- **Health**: `GET /healthz` returns the build version and is exempt from
auth. There is no Docker `HEALTHCHECK` directive, because a `scratch` image
- **Health**: `GET /healthz` returns the build version and nothing else, so it
is safe to exempt in Authelia. There is no Docker `HEALTHCHECK` directive,
because a `scratch` image
has no shell to run one and `restart: unless-stopped` already covers a dead
process. Adding one would mean giving the binary a `-healthcheck` flag that
calls its own endpoint.