Skip to content

fix(deploy): restore read-only-rootfs tmpfs ownership — podman 5.6.2 rejects tmpfs uid= (unblocks #333 deploy)#337

Merged
FlyM1ss merged 1 commit into
mainfrom
fix/rootfs-tmpfs-ownership-podman562
Jul 3, 2026
Merged

fix(deploy): restore read-only-rootfs tmpfs ownership — podman 5.6.2 rejects tmpfs uid= (unblocks #333 deploy)#337
FlyM1ss merged 1 commit into
mainfrom
fix/rootfs-tmpfs-ownership-podman562

Conversation

@FlyM1ss

@FlyM1ss FlyM1ss commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Why

The push-to-main deploy for #333 (read-only rootfs + full-boot pre-cutover gate) failed on the dropletActions run 28664065475. build ✅ and test ✅ passed; the failure was purely on-box:

Validating full boot (root-init + read-only rootfs + /health/) on the new image (pre-cutover)...
Error: unknown mount option "uid=1001": invalid mount option
ERROR: full-boot pre-cutover validation FAILED; aborting deploy (old container left serving)

Root cause: the droplet's podman 5.6.2 rejects tmpfs uid=/gid= mount options outright — both --tmpfs=…:uid=1001,gid=1001 and --mount type=tmpfs,tmpfs-uid=… fail (verified live on the box). #333 introduced those options on /home/fingpt and /app/staticfiles in both the pre-cutover gate and the ExecStart.

No outage: the gate fail-closed before cutover, so prod kept serving the pre-#333 image (verified: fingpt-api Up … (healthy) on the old digest, not the #333 digest). This is deterministic, not transient — a re-run fails identically.

Why CI didn't catch it: test_dockerfile_nonroot.test_tmpfs_options_pinned asserted the literal string uid=1001,gid=1001 as the "correct" invariant — so the test and the workflow agreed with each other but not with podman, and nothing in build/test ever runs podman.

What

Restore uid1001 ownership the way podman 5.6.2 actually supports — chown inside the container instead of via a (rejected) tmpfs option:

  • entrypoint.sh — root-init now chown fingpt:fingpt /home/fingpt /app/staticfiles, 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 (the baked ~/.cache/fontconfig) is already fingpt-owned.
  • backend-deploy.ymluid=1001,gid=1001mode=0755 on both the gate and ExecStart tmpfs flags (flag parity preserved). mode=0755 lands the dirs owner-writable, not the tmpfs-default world-writable 1777, once chowned.
  • test_dockerfile_nonroot.py — corrected test_tmpfs_options_pinned to pin the real invariant (no uid=/gid=; mode=0755) and added test_entrypoint_chowns_read_only_tmpfs_dirs (asserts the root-init chown runs, before setpriv).

Net effect is behaviorally identical to the intended-but-rejected uid=1001 tmpfs: 1001-owned mount point + fingpt-owned copied content.

Verification

  • TDD red→green: both new assertions fail on current main, pass after the fix. test_dockerfile_nonroot + test_entrypoint_store_build = 28 passed; gate↔ExecStart parity test still green.
  • Droplet-proven against the real security(backend): --read-only rootfs + full-boot pre-cutover gate (BOOT_CHECK_ONLY) #333 image (9b7762a9…): podman accepts mode=0755; root-init chown yields drwxr-xr-x 1001 1001 (owner-writable, not world-writable); a setpriv-dropped uid1001 writes both dirs; copied-up .cache stays fingpt-owned.
  • No other occurrences of the pattern in the repo (only backend-deploy.yml uses --read-only/--tmpfs).

Deploy note

Both halves land atomically on merge to main: the rebuild bakes the new entrypoint chown and the new workflow runs the mode=0755 gate in the same deploy. (Workflow-change-alone would clear the mount error but then fail the BOOT_CHECK $HOME writability probe on the root-owned tmpfs.) This unblocks #333's deploy.

Fixes the deploy regression from #333.

🤖 Generated with Claude Code

…ejected 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) <noreply@anthropic.com>
@FlyM1ss FlyM1ss merged commit 400d0ff into main Jul 3, 2026
7 checks passed
@FlyM1ss FlyM1ss deleted the fix/rootfs-tmpfs-ownership-podman562 branch July 3, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant