Move upload auth from nginx into the app
Add HTTP Basic auth on /upload, reading an Apache htpasswd file (bcrypt only, `htpasswd -B`) via a new -htpasswd flag. This lets nginx drop to a plain TLS-terminating reverse proxy with no auth of its own; the existing bcrypt htpasswd file is reused unchanged. auth.go: - LoadHtpasswd parses user:hash lines, keeps bcrypt entries ($2a/$2b/$2y, so Apache's $2y$ works), skips MD5/SHA/crypt with a warning. - Unknown users still pay for one bcrypt comparison against a dummy hash, so response timing can't enumerate valid usernames. - Failed attempts draw from a token-bucket limiter (x/time/rate, ~1/s, burst 10) returning 429 once drained; successful and bulk uploads never touch it. server.go guards both /upload routes (nil authenticator = passthrough for local --serve). /health stays open. Empty -htpasswd disables auth and logs a warning. module.nix: new top-level services.photogallery.htpasswdFile, delivered to the service via systemd LoadCredential (so a root-owned 0400 secret works). Removed nginx.htpasswdFile and basic_auth from the vhost. Both uploader apps already send Basic credentials, so no client change. Deps: golang.org/x/crypto/bcrypt, golang.org/x/time/rate (vendorHash bump).
This commit is contained in:
1 parent
3291ed67d5
commit
434d2437bf
9 files changed
+342
-27
No files matched your search
@@ -20,12 +20,29 @@ The binary does three things at once:
|
||||
- In dev mode (`--serve`), also serves `public/` as static files
|
||||
|
||||
Routes: `GET /upload` (HTML upload form), `POST /upload` (multipart), `GET /health`.
|
||||
Both `/upload` routes are gated by HTTP Basic auth when `-htpasswd` is set (see Auth); `/health` is always open.
|
||||
|
||||
In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, htpasswd on the upload endpoint. Note `/health` is **not** proxied — nginx only forwards `location /upload`, so anything outside the box can only reach `/upload`.
|
||||
In production nginx sits in front: serves `public/` directly, proxies `/upload` to localhost, terminates TLS. Auth is **no longer nginx's job** — the app checks credentials itself (`auth.go`), so the nginx vhost just proxies. Note `/health` is **not** proxied — nginx only forwards `location /upload`, so anything outside the box can only reach `/upload`.
|
||||
|
||||
## Auth
|
||||
|
||||
`-htpasswd <file>` points at an Apache htpasswd file — **bcrypt only**, generate
|
||||
with `htpasswd -B`. Each `POST /upload` and `GET /upload` then needs HTTP Basic
|
||||
credentials matching a line in that file; `GET /health` stays open. `$2y$`
|
||||
(Apache) and `$2a$`/`$2b$` hashes are all accepted; non-bcrypt lines (MD5
|
||||
`$apr1$`, SHA, crypt) are skipped with a warning. Empty `-htpasswd` disables
|
||||
auth entirely (dev default) and logs a warning — `/upload` is then open.
|
||||
|
||||
Verification lives in `auth.go`: unknown users still pay for one bcrypt
|
||||
comparison (against a dummy hash) so timing can't enumerate usernames, and
|
||||
**failed** attempts draw from a token-bucket limiter (`golang.org/x/time/rate`,
|
||||
~1/s, burst 10) that returns `429` once drained — successful and bulk uploads
|
||||
never touch it. Both apps (Android, Qt desktop) already send Basic auth, so no
|
||||
client change was needed. Deps: `golang.org/x/crypto/bcrypt`, `golang.org/x/time/rate`.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image`
|
||||
- **Go** — stdlib + `BurntSushi/toml`, `fsnotify/fsnotify`, `golang.org/x/image`, `golang.org/x/crypto` (bcrypt), `golang.org/x/time` (rate)
|
||||
- **Templates** — `html/template`, embedded via `embed.FS`
|
||||
- **Images** — four renditions per photo: original copy, 600×600 centre-cropped square thumbnail, a 1600px-longest-side "medium" for the detail page, and a 1200px "og" for link previews. Catmull-Rom resize. Outputs are skipped when newer than the source.
|
||||
- **Feed** — Atom (`encoding/xml`)
|
||||
@@ -136,10 +153,12 @@ inputs.photogallery.nixosModules.default
|
||||
services.photogallery = {
|
||||
enable = true;
|
||||
baseURL = "https://photos.yourdomain.tld";
|
||||
# bcrypt htpasswd (`htpasswd -B`); handed to the service via LoadCredential,
|
||||
# so a root-owned 0400 secret is fine. Omit to leave /upload open.
|
||||
htpasswdFile = "/run/secrets/photogallery-htpasswd";
|
||||
nginx = {
|
||||
enable = true;
|
||||
domain = "photos.yourdomain.tld";
|
||||
htpasswdFile = "/etc/nginx/.htpasswd-gallery";
|
||||
enable = true;
|
||||
domain = "photos.yourdomain.tld";
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -161,6 +180,7 @@ nix build # succeeds
|
||||
| `generator.go` | Scans content dir, renders HTML + Atom feed, generates thumbs/mediums |
|
||||
| `albums.go` | Tag parsing/normalization, `groupByTag`, album pages + `/albums/` + `/tags.json` |
|
||||
| `server.go` | Upload API + optional static file serving |
|
||||
| `auth.go` | htpasswd (bcrypt) HTTP Basic auth + failed-attempt rate limit for `/upload` |
|
||||
| `watcher.go` | fsnotify watcher with 500ms debounce |
|
||||
| `assets.go` | `//go:embed` for templates and static |
|
||||
| `templates/timeline.html` | Site root: photos grouped by day, paginated |
|
||||
@@ -183,6 +203,6 @@ nix build # succeeds
|
||||
- ActivityPub federation (planned later — start with RSS/Atom)
|
||||
- Multi-image upload
|
||||
- Delete/edit via UI
|
||||
- Any auth beyond nginx htpasswd
|
||||
- Any auth beyond the app's htpasswd Basic auth on `/upload` (no sessions, no per-user scopes)
|
||||
- The Android app has never been run on real hardware
|
||||
- Photo-page swipe has never been tested on a real touchscreen; the iOS Safari edge-gesture interaction is the untested part
|
||||
Reference in new issue
Block a user