release: authentication moves to Authelia (#5)
Authelia now runs in front of Traefik, so the app was asking for a second
password at the same door. This removes its own authentication entirely
rather than layering the two.
## Breaking — the server needs both files in this deploy
`compose.yaml` and `.env` are not pulled from this repository. The new image
ignores `PASSWORD`, and the old image refuses to start without it, so the
image and the compose file have to move together or the container dies at
startup.
| Variable | Change |
|---|---|
| `PASSWORD` | **removed** — the app no longer reads it |
| `AUTH` | **new, required** — the Traefik middleware that authenticates the app, e.g. `authelia@docker` |
| `CERTRESOLVER` | **new, required** — the resolver issuing the certificate for `HOST` |
The `tls=true` label is replaced by `tls.certresolver=${CERTRESOLVER}`.
Naming a resolver implies TLS, so it stays one label rather than two — and
the resolver had been carried by hand on the server since the first deploy.
## What was removed
- `auth()` and `challenge()` — HTTP Basic over a single shared password
- `throttle.go` and its tests — the per-IP guess limiter and the
`X-Forwarded-For` handling that fed it
- `golang.org/x/time`, which existed only for that limiter
- Sixty `-u` flags from the smoke script
`routes()` returns the bare mux and `/healthz` is an ordinary route on it.
159 insertions against 446 deletions; nothing was written to replace what
went.
## What holds the app up now
Both invariants live in `compose.yaml`, next to comments saying why:
- **The router names the Authelia middleware through `AUTH`.** Traefik takes
a router out of service when its middleware does not resolve, so an unset
or misspelt value fails shut rather than serving the app open.
- **The container publishes no ports.** It is reachable only over the shared
proxy network. Publishing `8080` would now bypass authentication outright,
not merely TLS.
`/healthz` returns the version and nothing else, so it is safe to exempt in
Authelia if a monitor needs to reach it.
## Why this is stronger, not weaker
The layer being deleted was one shared secret with no sessions, no second
factor and no way to revoke access for one person. Authelia does all three,
configured once for every service on the host instead of reimplemented per
app. The weaker of the two prompts was the one being kept.
## Tests
`TestAuth` and `TestHealthzSkipsAuth` are replaced by a single test asserting
every route answers without credentials — a 401 from the app would now mean
authentication had crept back in. `make check` green; CI green on `dev`.
## Note on the commit list
Nine of the ten commits below are already in `main` via #4, squash-merged
under a different SHA. They contribute nothing to the diff, which is the
auth removal alone.
---------
Co-authored-by: Esa Kataja <[email protected]>
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user