feat(files): gate files. browse behind FILES_BROWSE (on|off)
Per-host toggle for the read-only, no-auth browse of the shared upload dir,
so it can stay on for the LAN-party host (oxps) and be off on the
internet-facing one (cochi). Defaults to `on` — existing hosts unchanged.
Neither layer has conditionals, so each gets the value substituted in:
- caddy: docker-compose mounts 62-files.${FILES_BROWSE:-on}.caddy at the
fixed conf.d target (62-files.caddy -> 62-files.on.caddy, plus a
comments-only 62-files.off.caddy stub — caddy needs a parseable file
there). Same trick as FILES_MOUNT_MODE.
- nginx: render-nginx.sh splices nginx/files-browse.server.tmpl into
${FILES_BROWSE_SERVER} and adds/omits the name in the port-80 redirect.
This is the gate that matters; caddy is bound to 127.0.0.1.
Applying a change needs render-nginx.sh --install plus a caddy
--force-recreate (single-file bind mount pins the inode). With `off`,
drop `files` from LE_SUBDOMAINS on that host. upload. is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
92c6f5fc8f
commit
8507ffd020
9 files changed
+122
-33
No files matched your search
+35
-9
@@ -7,9 +7,9 @@
|
||||
>
|
||||
> The same `${FILES_DATA_DIR}` is also exposed at **`files.${BASE_DOMAIN}`** as a
|
||||
> plain caddy `file_server browse` — **read-only, no auth**, every uploaded file
|
||||
> publicly listable (`62-files.caddy`). Filestash gets its own subdomain because
|
||||
> it cannot be served under a subpath (SPA hardcodes `/api`, `/assets`, `/login`
|
||||
> at root).
|
||||
> publicly listable (`62-files.on.caddy`, toggled per host by `FILES_BROWSE`).
|
||||
> Filestash gets its own subdomain because it cannot be served under a subpath
|
||||
> (SPA hardcodes `/api`, `/assets`, `/login` at root).
|
||||
>
|
||||
> **Not** a backup browser, **not** a mod mirror. See "What goes in it".
|
||||
>
|
||||
@@ -30,7 +30,7 @@ Internet ── host nginx (TLS, LE cert files.) ──► caddy file_server (ro
|
||||
| Thing | Value |
|
||||
|---|---|
|
||||
| Vhost (filestash) | `upload.${BASE_DOMAIN}` |
|
||||
| Vhost (static browse) | `files.${BASE_DOMAIN}` — read-only, no auth, `62-files.caddy` |
|
||||
| Vhost (static browse) | `files.${BASE_DOMAIN}` — read-only, no auth, `62-files.on.caddy`, per-host `FILES_BROWSE=on\|off` |
|
||||
| Image | `machines/filestash` **pinned by digest** (upstream ships only `latest`) |
|
||||
| Container port | `8334`, runs as **uid/gid 1000** (`filestash`) |
|
||||
| Auth | htpasswd, **one shared account**, `FILES_USER` / `FILES_PASSWORD_HASH` |
|
||||
@@ -64,13 +64,38 @@ blocklisting (which would take `auth.` and the apex down with it, breaking login
|
||||
and the launcher for every player) all land on you. `render-config.sh` prints a
|
||||
loud warning when it renders this mode.
|
||||
|
||||
**`files.` is unconditionally public read.** Independent of `FILES_AUTH`, the
|
||||
`62-files.caddy` static browse serves the whole share with **no auth** — anyone
|
||||
who knows `files.${BASE_DOMAIN}` can list and download every uploaded file. This
|
||||
is deliberate (chosen 2026-07-24). The htpasswd login only gates *writes* (via
|
||||
**`files.` is public read wherever it is enabled.** Independent of `FILES_AUTH`,
|
||||
the static browse serves the whole share with **no auth** — anyone who reaches
|
||||
`files.${BASE_DOMAIN}` can list and download every uploaded file. This is
|
||||
deliberate (chosen 2026-07-24). The htpasswd login only gates *writes* (via
|
||||
`upload.`); it never gated reads at this URL. Don't put anything private in the
|
||||
share.
|
||||
|
||||
**`FILES_BROWSE=on|off` turns the whole `files.` vhost off per host** (added
|
||||
2026-07-30) — the answer to "I want it on `oxps` (LAN party) but not on `cochi`
|
||||
(internet-facing)". One var, two layers, because each has its own conditional
|
||||
problem:
|
||||
|
||||
| Layer | Mechanism |
|
||||
|---|---|
|
||||
| caddy | `docker-compose.yml` substitutes the value into the mount source: `./caddy/conf.d/62-files.${FILES_BROWSE:-on}.caddy` → `/etc/caddy/conf.d/62-files.caddy`. `62-files.off.caddy` is a comments-only stub (compose has no conditionals, and caddy needs a parseable file at the fixed mount target — same trick as `FILES_MOUNT_MODE`). |
|
||||
| nginx | `render-nginx.sh` splices `nginx/files-browse.server.tmpl` into `${FILES_BROWSE_SERVER}` when `on`, or a comment when `off`, and adds/omits the name in the port-80 redirect (nginx has no conditionals either). |
|
||||
|
||||
nginx is the gate that matters — with no TLS vhost the name is unreachable from
|
||||
the internet regardless of caddy (caddy is bound to `127.0.0.1`). The caddy stub
|
||||
is the second layer, and it also matters on hosts reached **directly** on caddy's
|
||||
port rather than through nginx (`oxps` on the LAN). Applying a change:
|
||||
|
||||
```sh
|
||||
tooling/render-nginx.sh --install # nginx vhost in/out + reload
|
||||
docker compose up -d --force-recreate caddy # single-file bind mount pins the inode
|
||||
```
|
||||
|
||||
`restart caddy` is NOT enough (the conf mount is a single file — see
|
||||
`plan/14-deploy.md`). With `off`, `files` also needs dropping from
|
||||
`LE_SUBDOMAINS` on that host: no vhost, no cert to renew. `upload.` (filestash,
|
||||
authenticated, writable) is untouched by this var.
|
||||
|
||||
**Per-host auth status (the two prod envs diverge):**
|
||||
|
||||
| Host | `upload.` reachability | `FILES_AUTH` | Notes |
|
||||
@@ -106,13 +131,14 @@ See `.env.example` for the annotated block. Summary:
|
||||
|---|---|
|
||||
| `FILES_DATA_DIR` | host path of the share, outside the repo |
|
||||
| `FILES_MOUNT_MODE` | literal mount flag, `rw` \| `ro` |
|
||||
| `FILES_BROWSE` | `on` \| `off` — serve the `files.` read-only browse on this host |
|
||||
| `FILES_AUTH` | `htpasswd` \| `passthrough` \| `none` |
|
||||
| `FILES_USER` | shared login name |
|
||||
| `FILES_PASSWORD_HASH` | **`$6$`**, from `openssl passwd -6 '<pw>'` |
|
||||
| `FILES_ADMIN_PASSWORD_HASH` | bcrypt, from `docker run --rm caddy:alpine caddy hash-password --plaintext '<pw>'` |
|
||||
| `FILES_SECRET_KEY` | `openssl rand -hex 16`, must be non-empty |
|
||||
| `FILES_LOCAL_SECRET` | `openssl rand -hex 24`, unlocks the local backend |
|
||||
| `LE_SUBDOMAINS` | must include `upload` **and** `files` (both are vhosts) |
|
||||
| `LE_SUBDOMAINS` | must include `upload`, plus `files` when `FILES_BROWSE=on` |
|
||||
|
||||
`filestash/config.json` is **rendered and gitignored** — it holds every hash and
|
||||
both secrets. Only the `.tmpl` is tracked.
|
||||
|
||||
Reference in new issue
Block a user