feat(files): run filestash as a chosen uid so share files are host-owned

Add an env-driven `user:`/`group_add` to the filestash service plus
tooling/filestash-set-run-user.sh. Running the container as a real host
account (oxps: oier=1002) makes every file it writes into the share owned
by that account, so scp/ssh and the web UI share one owner — no chown dance.

Defaults FILES_UID/FILES_GID=1000, leaving cochi (image's own uid) untouched.
group_add 1000 keeps the 0730 image binary + 0770 /app/data usable under any
uid; the helper also chowns the filestash_state volume (1000:1000) so a
non-1000 uid doesn't crash-loop unable to write its own sqlite/config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
oierandClaude Opus 4.8 committed 2026-07-24 16:25:03 +02:00
1 parent 9322e39a8c
commit 243787c530
4 files changed
+153

No files matched your search

+51
View File
@@ -194,6 +194,57 @@ The SPA sends it; anything else (curl, a naive monitor) trips intrusion
detection and gets a 403 that looks like an auth bug. Only matters when probing
by hand — the Kuma monitor hits `/`, which is fine.
### Share dir must be owned by uid 1000 or writes silently 403
`filestash` runs as **uid/gid 1000**. `FILES_MOUNT_MODE=rw` and `FILES_AUTH=none`
(or a valid login) are **not enough** — the kernel still enforces the share
directory's own perms. If `${FILES_DATA_DIR}` is owned by a different uid with
mode `0775`, uid 1000 lands in *other* (`r-x`) and every upload/mkdir/delete dies
with `Permission denied`, while the UI, `curl /`, and the Kuma monitor all stay
green (same false-green trap as the SPA gotchas). Hit on `oxps` 2026-07-24: the
share (`/home/oier/mc/distribution-ulicraft/files`) was `1002:1002 0775`.
Fix — give the share to filestash's uid:
```sh
sudo chown -R 1000:1000 "$FILES_DATA_DIR"
sudo chmod g+s "$FILES_DATA_DIR" # new files inherit the group
docker exec -u 1000 filestash sh -c 'touch /data/files/.wtest && echo OK && rm /data/files/.wtest'
```
Reversible (`chown -R <old> "$FILES_DATA_DIR"`). If another account also writes
the share directly, use a shared group + `g+rwx` instead of chowning it away.
#### Cleaner: run filestash *as* the owning uid (share files become `oier:oier`)
Chowning the share to filestash's 1000 works, but then everything filestash
writes is owned by an anonymous `1000` nobody logs in as. To make the share
files owned by a **real host account** (oxps `oier` = uid 1002) so you can manage
them over ssh, run the container as that uid. Compose exposes it:
```yaml
user: "${FILES_UID:-1000}:${FILES_GID:-1000}"
group_add: ["1000"]
```
Two things make a bare uid override fail, both handled above:
- The image binary `/app/filestash` is `0730 filestash:filestash` and `/app/data`
is `0770` — a non-1000 uid is *other* and can neither exec nor traverse. The
`group_add: ["1000"]` restores that via the filestash group.
- The `filestash_state` volume (config, sqlite, search index) is owned `1000:1000`.
Running as another uid without chowning it → filestash can't write its own
state and crash-loops. **Chown the state volume too.**
Do all of it with **`tooling/filestash-set-run-user.sh`** (reads `FILES_UID`/
`FILES_GID` from `.env`, chowns the state volume + `FILES_DATA_DIR`, recreates,
write-tests). oxps: set `FILES_UID=FILES_GID=1002` in `.env`, run it. Default
`1000:1000` leaves cochi untouched. Per-host, host-local — nothing but the
compose knob and this script is committed.
### Accessing the share files directly (bypassing the web UI)
`${FILES_DATA_DIR}` is a plain host directory — the web UI is just a view of it.
- **On the host:** `ls "$FILES_DATA_DIR"` (oxps: `/home/oier/mc/distribution-ulicraft/files`).
Write as uid 1000 to match filestash ownership: `sudo -u '#1000' cp foo "$FILES_DATA_DIR"/`.
- **From your machine (oxps is LAN-only):** `scp file oxps:/home/oier/mc/distribution-ulicraft/files/`
then `ssh oxps 'sudo chown 1000:1000 /home/oier/mc/distribution-ulicraft/files/file'`,
or mount over sshfs: `sshfs oxps:/home/oier/mc/distribution-ulicraft/files ~/mnt/ulifiles`.
- **Via the container:** `docker exec -u 1000 filestash ls /data/files`.
- **Web (LAN):** `http://files.${BASE_DOMAIN}` — no login on oxps (auth=none).
### Disk is the shared failure domain
`FILES_DATA_DIR` grows without a quota, driven by whoever has the password. A
full disk on `cochi` does not just break `files.` — it takes **Minecraft, Drasl,