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:
@@ -55,7 +55,7 @@ One static Go binary. No Node.js, no bundler, no separate database server.
|
||||
| Interactivity | [Datastar](https://data-star.dev) — signals and DOM patching in one ~11 kB script |
|
||||
| Styling | hand-written CSS, `light-dark()` for themes |
|
||||
| Database | SQLite via `modernc.org/sqlite` (pure Go) |
|
||||
| Auth | HTTP Basic, one shared household password |
|
||||
| Auth | none in-app — Authelia, via a Traefik forward-auth middleware |
|
||||
| Runtime image | `FROM scratch` |
|
||||
|
||||
Working on it: [CONTRIBUTING.md](CONTRIBUTING.md) — branches, commit messages,
|
||||
@@ -196,7 +196,6 @@ Everything is environment variables. `.env` is gitignored; start from
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `PASSWORD` | *required* | Shared password. The app will not start without it. |
|
||||
| `DB` | `./data/foodster.db` | SQLite file path; the directory is created if missing. |
|
||||
| `ENV` | `prod` | Anything else is prefixed to the tab title (`dev · Foodster`). |
|
||||
| `ADDR` | `:8080` | Listen address. Only useful for a second local instance. |
|
||||
@@ -205,6 +204,8 @@ Everything is environment variables. `.env` is gitignored; start from
|
||||
| `REPO` | *required to run* | Image repository, no tag. Used by `compose.yaml`. |
|
||||
| `TAG` | `latest` | Tag to run under compose. |
|
||||
| `HOST` | *required to run* | Hostname Traefik routes to. |
|
||||
| `AUTH` | *required to run* | Traefik middleware that authenticates the app, e.g. `authelia@docker`. |
|
||||
| `CERTRESOLVER` | *required to run* | Traefik certificate resolver for `HOST`. |
|
||||
|
||||
Names carry no prefix: the container gives them their own namespace already.
|
||||
`PUID`/`PGID` are the exception — `UID` is read-only in bash, so a value set
|
||||
@@ -254,24 +255,27 @@ docker compose restart
|
||||
|
||||
## Security
|
||||
|
||||
Access is a single shared password over HTTP Basic — no accounts, no
|
||||
sessions. Credentials are compared in constant time over SHA-256 digests, so
|
||||
neither the password nor its length leaks through timing.
|
||||
**The app has no authentication of its own.** It trusts every request it
|
||||
receives, because the only thing that can reach it is Traefik, and Traefik
|
||||
hands each request to Authelia first. Access control, sessions, brute-force
|
||||
protection and multi-factor all live there, where they are configured once
|
||||
for every service on the host instead of reimplemented per app.
|
||||
|
||||
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.
|
||||
Two things make that safe, and both must hold:
|
||||
|
||||
`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.
|
||||
- **`AUTH` names the Authelia middleware** on the router. It is the whole of
|
||||
the app's access control. Traefik takes a router out of service when its
|
||||
middleware does not resolve, so a typo fails shut rather than open.
|
||||
- **The container publishes no ports.** It is reachable only over the shared
|
||||
`traefik` network. Publishing `8080` would put an unauthenticated,
|
||||
unencrypted copy of the app on the host and defeat both of the above.
|
||||
|
||||
**None of this replaces a strong `PASSWORD`.** Rate limiting removes
|
||||
brute force as a practical route; it does not make a guessable password safe.
|
||||
`/healthz` returns nothing but the version, so it is safe to bypass in
|
||||
Authelia if a monitor needs to poll it from outside.
|
||||
|
||||
Earlier versions carried HTTP Basic auth and a per-IP guess limiter. Both
|
||||
were removed once Authelia was in front: two prompts for one door, and the
|
||||
weaker of the two was the one holding a shared password.
|
||||
|
||||
## Mockups
|
||||
|
||||
|
||||
Reference in New Issue
Block a user