diff --git a/filestash/config.json.tmpl b/filestash/config.json.tmpl index fdce85d..43518ed 100644 --- a/filestash/config.json.tmpl +++ b/filestash/config.json.tmpl @@ -1,7 +1,8 @@ { "general": { "name": "Ulicraft Files", - "host": "https://files.${BASE_DOMAIN}", + "host": "files.${BASE_DOMAIN}", + "force_ssl": true, "secret_key": "${FILES_SECRET_KEY}", "editor": "base", "display_hidden": false, diff --git a/plan/20-files.md b/plan/20-files.md index bc0c303..19093a7 100644 --- a/plan/20-files.md +++ b/plan/20-files.md @@ -121,12 +121,35 @@ config nobody can log into. (The *admin* password is checked by Go's bcrypt directly, which does accept `$2y$` — but `caddy hash-password` gives `$2a$` anyway, so just use that for both.) -### `general.host` must match the incoming `Host` header -Filestash blocks every non-`/admin/` request whose `Host` differs from its -configured `general.host`, with *"only traffic from X is allowed"*. It's a 403 -that looks like an auth failure. nginx forwards the original Host and caddy -passes it through — don't rewrite it anywhere. If `files.` starts 403ing after a -domain change, this is why. +### `general.host` is a BARE HOSTNAME — a scheme breaks the browser only +`"host": "files.ulicraft.net"`, **not** `"https://files.ulicraft.net"`, plus +`"force_ssl": true`. The SPA computes its redirect origin as +`(force_ssl ? "https://" : "http://") + host`, so a scheme in `host` yields +**`http://https://files.ulicraft.net`** and every client-side redirect dies — +the page loads, then silently fails to route anywhere. + +The vicious part: the server-side Host check (`SecureOrigin`) **strips** the +scheme before comparing, so the server is perfectly happy. `curl /` returns 200, +the API answers, and the Uptime Kuma monitor stays green — **only real browsers +break**. Symptom in the log is a `POST /report?...msg=Redirecting to +http://https://...` line. Hit this on 2026-07-14; fixed in `config.json.tmpl`. + +`force_ssl` only sets an HSTS header — it does **not** redirect, so it can't loop +behind the nginx→caddy (plain http) hop. It must be `true` or the SPA builds +`http://` origins on an https page. + +### `general.host` must also match the incoming `Host` header +Filestash blocks every non-`/admin/` request whose `Host` differs from +`general.host`, with *"only traffic from X is allowed"* — a 403 that looks like +an auth failure. nginx forwards the original Host and caddy passes it through — +don't rewrite it anywhere. + +### The login form only renders at `?action=redirect` +`GET /api/session/auth/` renders the htpasswd form **only** when +`action=redirect` is present; any other request falls through to the credential +check, fails with empty creds, and 303s back to `?action=redirect`. So the real +form URL is `/api/session/auth/?action=redirect&label=files`. A bare +`/api/session/auth/?label=files` returning a 303 is **normal**, not a bug. ### `config.json` is a single-file bind mount → stale inode Same shape as the caddy `conf.d` gotcha. The mount pins the **inode**, so diff --git a/tooling/render-config.sh b/tooling/render-config.sh index de784a2..de17bac 100755 --- a/tooling/render-config.sh +++ b/tooling/render-config.sh @@ -34,6 +34,13 @@ render nmsr/config.toml.tmpl nmsr/config.toml # admin console writes it back at runtime. We render it and mount it read-only, # so git stays authoritative (the console renders but cannot save — by design). # +# general.host in the template is a BARE HOSTNAME (files.$BASE_DOMAIN, no +# scheme) and general.force_ssl is true. The SPA builds its redirect origin as +# (force_ssl ? "https://" : "http://") + host, so a scheme in `host` produces +# "http://https://files..." and every client-side redirect breaks. The +# server-side Host check strips the scheme first, so curl/monitors see a healthy +# 200 and ONLY browsers fail. Don't "fix" it by putting the URL back. +# # FILES_AUTH picks the identity_provider block. The params fields are JSON # *strings* holding escaped JSON (that's Filestash's schema, not a typo) — # built here rather than in the template because the shape differs per mode.