From 10b86ae23e896a23d99a47933d844bf506324601 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 14 Jul 2026 18:27:42 +0200 Subject: [PATCH] fix(files): general.host must be a bare hostname, not a URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SPA computes its redirect origin as (force_ssl ? "https://" : "http://") + general.host. With host set to "https://files.${BASE_DOMAIN}" that produced "http://https://files.ulicraft.net", so every client-side redirect broke and the page never routed anywhere. Server-side this was invisible: SecureOrigin strips the scheme before the Host check, so `curl /` returned 200, the API answered, and the uptime monitor stayed green — only real browsers failed. The tell is a `POST /report?...msg=Redirecting to http://https://...` line in the log. Set host to the bare hostname and force_ssl=true (which the origin needs to build https://, and which only emits an HSTS header — it does not redirect, so it can't loop behind the plain-http nginx→caddy hop). Verified: origin is now "https://files.ulicraft.net"; login + ls still work. Co-Authored-By: Claude Opus 4.8 (1M context) --- filestash/config.json.tmpl | 3 ++- plan/20-files.md | 35 +++++++++++++++++++++++++++++------ tooling/render-config.sh | 7 +++++++ 3 files changed, 38 insertions(+), 7 deletions(-) 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.