fix(make): build-assets drops privileges via --user; bump CLAUDE.md Grav to 2.0.7

Close the remaining root-owned bind-mount vector: build-assets (a docker
run, missed by the docker-exec fix in 209b804) now runs as the host
uid/gid with HOME=/tmp for npm's cache. Verified: build completes clean,
zero root-owned files under user/themes, bundles byte-identical.

Solution doc updated from "still open" to fixed; CLAUDE.md stack section
now matches the Dockerfile's Grav 2.0.7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
This commit is contained in:
2026-07-08 23:41:10 +02:00
co-authored by Claude Fable 5
parent 6cf50920df
commit 9ffeb4d2d8
3 changed files with 13 additions and 9 deletions
@@ -102,11 +102,11 @@ install-plugins:
$(MAKE) apply-plugin-patches
```
### The `build-assets` vector (same principle, `docker run`) — still open
### The `build-assets` vector (same principle, `docker run`) — fixed 2026-07-08
The 2026-07-08 fix (`209b804`) hardened `install-plugins` only. `build-assets` remains a root-writing target and surfaced later: `git worktree remove` aborted with `Permission denied` on root-owned esbuild bundles under `user/themes/intotheeast/js/post/`, and earlier a `build-assets` run had produced a root-owned `css-compiled/` dir that blocked a `git merge` on the main checkout. (session history)
The first 2026-07-08 fix (`209b804`) hardened `install-plugins` only. `build-assets` remained a root-writing target and surfaced later: `git worktree remove` aborted with `Permission denied` on root-owned esbuild bundles under `user/themes/intotheeast/js/post/`, and earlier a `build-assets` run had produced a root-owned `css-compiled/` dir that blocked a `git merge` on the main checkout. (session history)
Apply the same drop-privileges principle — with `--user` on `docker run`:
The same drop-privileges principle applies — with `--user` on `docker run` (fixed later the same day):
```makefile
# Before — writes root-owned node_modules + bundles into the tracked theme tree
@@ -116,15 +116,16 @@ build-assets:
-w /app node:20-alpine \
sh -c "npm install && npm run build"
# After — outputs owned by the host user (uid 1000)
# After — outputs owned by the host user; HOME=/tmp gives npm a writable
# cache when running as a non-root uid
build-assets:
docker run --rm --user $(HOST_UID):$(HOST_GID) \
docker run --rm --user $(HOST_UID):$(HOST_GID) -e HOME=/tmp \
-v $(PWD)/user/themes/intotheeast:/app \
-w /app node:20-alpine \
sh -c "npm install && npm run build"
```
Caveat: run as a non-root uid, npm needs a writable `$HOME`/cache. If the build errors on a read-only home dir, add `-e HOME=/tmp` (or `-e npm_config_cache=/tmp/.npm`). Recovery for the existing root-owned output is the same as anywhere else — `chown -R $(HOST_UID):$(HOST_GID)` from a container that already has root, then `rm`.
Verified: `make build-assets` with the fix completes clean (esbuild bundles emitted), `find user/themes/intotheeast -uid 0` counts zero, and the output bundles are byte-identical to the previously committed ones. Recovery for any pre-existing root-owned output is the same as anywhere else — `chown -R $(HOST_UID):$(HOST_GID)` from a container that already has root, then `rm`.
## Why This Works
@@ -138,7 +139,7 @@ The preliminary chown of `cache/` and `tmp/` is container-internal: those paths
The reusable principle, worth internalizing beyond this one repo:
- **Any make/CI target that writes files into a host bind mount must drop privileges — whether it uses `docker exec` (`-u $(HOST_UID):$(HOST_GID)`) or `docker run` (`--user $(HOST_UID):$(HOST_GID)`).** A container booting as root does *not* mean the commands you run in it must write as root. `build-assets` (a `docker run`) is the easy one to miss, because the original fix only patched the `docker exec` targets — so audit `docker run` invocations too, not just `docker exec`.
- **Any make/CI target that writes files into a host bind mount must drop privileges — whether it uses `docker exec` (`-u $(HOST_UID):$(HOST_GID)`) or `docker run` (`--user $(HOST_UID):$(HOST_GID)`).** A container booting as root does *not* mean the commands you run in it must write as root. `build-assets` (a `docker run`) was the easy one to miss, because the original fix only patched the `docker exec` targets — so audit `docker run` invocations too, not just `docker exec`.
- **Derive host identity once in the Makefile and reuse it:** `HOST_UID := $(shell id -u)` / `HOST_GID := $(shell id -g)`.
- **Don't rely on `APACHE_RUN_USER` or compose-level `UID`/`GID` env vars to fix exec ownership** — they don't apply to `docker exec`. `APACHE_RUN_USER` only affects Apache workers; compose `user:`/env vars only affect services wired to consume them.
- **You can't just add `user:` to a service whose entrypoint needs root** (to bind privileged ports, set up cron, etc.). Drop privileges per-exec instead of per-container.