From 889f18ec18fcaf5d83795f03b0952ac61642f7aa Mon Sep 17 00:00:00 2001 From: FlyM1ss Date: Fri, 3 Jul 2026 22:30:09 +0800 Subject: [PATCH] fix(deploy): chown read-only-rootfs tmpfs in root-init; drop podman-rejected tmpfs uid= (#333 deploy fix) The droplet's podman 5.6.2 rejects `--tmpfs=...:uid=1001,gid=1001` outright (`Error: unknown mount option "uid=1001": invalid mount option`; and there is no working `--mount type=tmpfs,tmpfs-uid=` either). This fail-closed #333's pre-cutover BOOT_CHECK gate on the first push-to-main deploy. build+test were green because nothing there runs podman -- and test_tmpfs_options_pinned even pinned the rejected string, so the test agreed with the workflow but not with reality. The gate aborted before cutover, so prod kept serving the pre-#333 image (no outage; the gate did its job). Restore uid1001 ownership the way podman 5.6.2 supports: - mount /home/fingpt and /app/staticfiles at mode=0755 (owner-writable once chowned, not the tmpfs-default world-writable 1777), on both the gate and the ExecStart podman run (flag parity preserved); - chown both dirs to fingpt in entrypoint.sh root-init, before the setpriv drop, while PID1 still holds CAP_CHOWN. Non-recursive is sufficient: only the tmpfs mount point comes up root-owned; tmpcopyup content (baked ~/.cache/fontconfig) is already fingpt-owned. This is behaviorally identical to the intended-but- rejected uid=1001 tmpfs. Correct the invariant that hid the bug: test_tmpfs_options_pinned now asserts no uid=/gid= on the tmpfs (+ mode=0755), and a new test_entrypoint_chowns_read_only_tmpfs_dirs pins the root-init chown ordering. Verified on the droplet against the real #333 image: podman accepts the new flags, root-init chown yields `drwxr-xr-x 1001 1001`, and a setpriv-dropped uid1001 writes both dirs successfully. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/backend-deploy.yml | 16 +++++-- Main/backend/entrypoint.sh | 9 ++++ Main/backend/tests/test_dockerfile_nonroot.py | 47 ++++++++++++++++--- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.github/workflows/backend-deploy.yml b/.github/workflows/backend-deploy.yml index 4fcee55..9373273 100644 --- a/.github/workflows/backend-deploy.yml +++ b/.github/workflows/backend-deploy.yml @@ -237,7 +237,7 @@ jobs: # and ops/egress_firewall.py consume REDIS_URL, and the firewall # self-test checks host:port reachability only, DB-agnostic). echo "Validating full boot (root-init + read-only rootfs + /health/) on the new image (pre-cutover)..." - podman run --rm --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID --cap-add=SETPCAP --pids-limit=1024 --read-only --tmpfs=/tmp:rw,size=512m,mode=1777 --tmpfs=/app/staticfiles:rw,uid=1001,gid=1001 --tmpfs=/home/fingpt:rw,uid=1001,gid=1001 --tmpfs=/app/runtime:rw,size=512m --memory=1.7g --memory-swap=2g --network fingpt-net \ + podman run --rm --cap-drop=ALL --cap-add=NET_ADMIN --cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID --cap-add=SETPCAP --pids-limit=1024 --read-only --tmpfs=/tmp:rw,size=512m,mode=1777 --tmpfs=/app/staticfiles:rw,mode=0755 --tmpfs=/home/fingpt:rw,mode=0755 --tmpfs=/app/runtime:rw,size=512m --memory=1.7g --memory-swap=2g --network fingpt-net \ --env-file /home/deploy/fingpt/envs/.env.production \ --env REDIS_URL=redis://fingpt-redis:6379/15 \ --env BOOT_CHECK_ONLY=1 \ @@ -287,10 +287,16 @@ jobs: # (--disable-dev-shm-usage), nft mktemp at root-init, # /tmp/fingpt_cache; sized + world-writable-sticky; # /app/staticfiles collectstatic writes it at boot (0 files today, kept - # writable as future-proofing); uid1001 tmpfs; + # writable as future-proofing); mode=0755 tmpfs, + # chowned to fingpt by root-init (see below); # /home/fingpt fontconfig cache, edgartools ~/.edgar import-time - # marker, yfinance cache; MUST carry uid=1001,gid=1001 - # or the MCP-child EACCES bug (#331 class) returns. + # marker, yfinance cache; MUST end up fingpt-owned or + # the MCP-child EACCES bug (#331 class) returns. podman + # 5.6.2 rejects tmpfs uid=/gid= mount options outright, + # so ownership is NOT set here -- entrypoint.sh root-init + # chowns both dirs while PID1 still holds CAP_CHOWN. + # mode=0755 (not the tmpfs default 1777) lands them + # owner-writable, not world-writable. # /app/logs and /app/media are vestigial with ZERO writers -- deliberately # NOT tmpfs, so a future stray write fails LOUDLY instead of vanishing # into RAM. Playwright's DEPENDENCIES_VALIDATED marker is pre-baked at @@ -307,7 +313,7 @@ jobs: cat > "$OVERRIDE_DIR/override.conf" <