Repository navigation
fix(docker): read .env last, and restore RUN_AS_UID/GID from positional parameters - #2235
Conversation
…al parameters Follow-up to Kpa-clawbot#2125, the six non-blocking notes on its approval. A sourced file shares the shell's variables: every global, the locals of load_env_file, which sources it, and, by dynamic scoping, the locals of prepare_service_user too. So a trusted .env that assigned _uid_before or _uid_was skipped the RUN_AS_UID/RUN_AS_GID restore and left RUN_AS_UID=0 in the services' environment (note 3), and one that assigned SERVICE_DATA_DIR pointed the hand-over elsewhere, since it ran after the file was read. Now .env is read last, after the hand-over, and the only code after it reads prepare_service_user's positional parameters: the account, the container's RUN_AS_UID/RUN_AS_GID (set or unset, and value) and the file's path are parked in $1..$6 before the file is sourced. The comment states the real reason the account was already safe (the `.` happens inside load_env_file, whose own $1.. is what a `set -- root` would reach; note 1) and that a trusted .env is root's code, which this does not defend against. Also: svc_log uses printf, so dash's echo no longer cuts a logged path at a backslash escape (note 5); the entrypoint check also pins that it does not assign CORESCOPE_SERVICE_USER (note 4); the test header is reflowed (note 6); and TEST_SHELL lets the suite run under dash, bash or busybox without editing it. Tests (notes 1 and 2): .env running `set -- root`; .env assigning the restore's names, the SERVICE_* paths, and RUN_AS_GID alone; a logged path with \012. 116 assertions under dash and bash. Each of these makes the suite fail: the hand-over moved back after .env, the restore from locals of either function, the file sourced inline in prepare_service_user, the restore dropped, only the uid compared, the gid restored from the uid's parameter, set/unset flipped, the log reading SERVICE_ENV_FILE, the entrypoint reassigning the account, and svc_log back to echo (dash only). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
efiten
left a comment
There was a problem hiding this comment.
Thanks. Approving and merging.
What I checked at 195b6161
-
The fix. The account, both RUN_AS values (set or unset) and the env-file path are parked in
prepare_service_user's positional parameters before.envis read, so a sourced.envcan no longer reach them, and the hand-over (find/chown) runs before.envcan changeSERVICE_DATA_DIRorPATH. -
Tests, under WSL Debian:
scripts test file dash bash head head 116 passed 116 passed base base 103 passed 103 passed base head 110 passed, 6 failed 111 passed, 5 failed The red failures are the expected ones: restored
RUN_AS_UID=0 RUN_AS_GID=0, the missing "ignoring" log line, no hand-over withSERVICE_DATA_DIR=/, and (dash only) the\012log line split. -
Precedence is unchanged for every variable other than RUN_AS_*, and
docs/deployment.md:100-107already describes the behaviour.
Not checked: busybox ash (no binary here) and the image-level runs A, B and C (no Docker here).
For the description, not blocking
- "The new cases fail on master" holds for three of the five.
.env runs set -- rootandRUN_AS_GID alonepass on master; they guard against a future regression. - The
load_env_filecomment "Nothing runs here after the file" is not literal, sinceset +afollows. "Nothing reads a variable here after the file" would be exact.
Release notes for v3.15.0: `docs/release-notes/v3.15.0.md` and the `CHANGELOG.md` entry. The release workflow uses the notes file as the GitHub release body. ## What is in it - 77 commits since v3.14.0 (76 merged pull requests plus this one): 27 fix, 23 perf, 19 feat, 3 test, 2 ci, 2 docs, 1 chore. Nineteen feats, so minor. - "Read this before upgrading" first. It covers: - the liveness change (#2261, #2262); - the `users.db` v6 migration and what it means for a rollback (#2174); - the backfill at every ingestor start (#2133) and the 24 h planner-statistics refresh (#2158); - the caches that trade freshness for latency (#2165, #2222, #2147, #2156); - changes in hop names and region placement (#2188, #2190, #2234, #2245, #2220); - the role thresholds on favorites (#2215); - the geo-filter save on a single-file mount (#2137); - the `/api/stats` polling in hidden tabs (#2162). - Features, fixes, performance, then tests/CI/docs. - 29 issues closed, taken from each pull request's `closingIssuesReferences`. - Contributors: pull request authors and issue reporters with counts. The commit co-author trailers only name the pull request's own author or tooling. ## How it was checked - I read every pull request body. Every number in the notes comes from a pull request body. - I checked the config keys and defaults against `config.example.json` on `2bb000f2`. - I checked the UNASKED and opt-in claims for #2125, #2133, #2235 and #2239 against the source. - For the other pull requests I relied on their bodies and did not open the diffs. ## Not covered - The uncertain count is a single count on one staging instance on the day of the change (1,682 repeaters and rooms). It is not a before/after comparison. - #2133 gives no measured cost for its backfill on a large database, and the notes say so. ## After merge Tag `v3.15.0` on the merge commit once its `:edge` image exists. Then check in the registry that `v3.15.0`, `v3.15`, `v3` and `latest` share one digest, and that the release has the notes as its body and both `corescope-decrypt` binaries. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #2125: the six non-blocking notes on its approval, with note 3 as the one that changes behaviour.
What changes
.envis now read last. The hand-over of the data directory moves before it, and the only code that runs after the file is sourced readsprepare_service_user's positional parameters: the account, the container'sRUN_AS_UID/RUN_AS_GID(set or unset, and value) and the file's path are parked in$1..$6before the...happens insideload_env_file, so aset -- rootin.envlands in that function's$1, not inprepare_service_user's (note 1). It also says that a trusted.envis root's code and this does not defend against it (it can redefine functions or setPATH); what it cannot do by assigning variables is change the account, where the hand-over goes, orRUN_AS_UID/RUN_AS_GIDas the services see them.svc_logusesprintf(note 5); the entrypoint check also pins that it does not assignCORESCOPE_SERVICE_USER(note 4); the test header is reflowed (note 6);TEST_SHELLpicks the inner shell so the suite runs under dash, bash or busybox without editing.Why
A sourced file shares the shell's variables: every global, the locals of
load_env_file, and, by dynamic scoping, the locals of its caller too (checked under dash, bash 5.3 and busybox ash 1.37). So a trusted.envthat assigned_uid_beforeor_uid_wasskipped the restore and leftRUN_AS_UID=0in the services' environment, as note 3 says. The same class reached the hand-over: withSERVICE_DATA_DIR=/in.env, the walk ran over/. Nothing but the entrypoint readsRUN_AS_UID/RUN_AS_GID, so the effect was a wrong value in the services' environment and a log line that said "ignoring" when it had not.What I checked
docker/test-select-service-user.sh: 116 assertions, 0 failed, under dash and bash. The new cases fail on master for the stated reasons and nothing else does..envrunningset -- root(note 1; this is the case that catches sourcing the file inline, the names case alone does not);.envassigning the restore's names,_env_file,SERVICE_ENV_FILEandRUN_AS_*, with uid 1000 and gid 1001 so a swapped parameter shows;RUN_AS_GIDalone;.envpointing the threeSERVICE_*paths elsewhere; a logged path containing\012..env; restore fromload_env_file's locals (master); restore fromprepare_service_user's locals; file sourced inline inprepare_service_userwith the refusal log kept; restore dropped; only the uid compared; gid restored from the uid's parameter; set/unset flipped; the log readingSERVICE_ENV_FILEafter the.; the entrypoint reassigning the account.svc_logback toechofails under dash only, which is where note 5 applies..envcontaining all of the above plusDISABLE_MOSQUITTO=true,DISABLE_CADDY=trueandset -- root: (A)RUN_AS_UIDunset: root, nothing re-owned,DISABLE_*honoured,RUN_AS_*not in the services' environment. (B)RUN_AS_UID=1000 RUN_AS_GID=1001on A's data directory, root-ownedmeshcore.dbinside: services run as 1000, their environment hasRUN_AS_UID=1000 RUN_AS_GID=1001,meshcore.dband the directory are 1000:1001,.envstays root's,/etc/passwdstays root's. (C).envowned by uid 1000: refused with the reason, services run as 1000. The same image with master's two scripts mounted over it, in B's shape: services' environmentRUN_AS_UID=0 RUN_AS_GID=0,/etc/passwdowned by 1000,.envowned by 1000:1001.sh test-all.shpasses; no JS or Go change.Not checked
TEST_SHELLon my machine: Ubuntu's busybox prefers its own applets overPATH, so the stubs are bypassed and 28 cases fail, on master and on this branch alike. Your WSL Debian run (103 passed) and the image itself, which runs Alpine's ash with real binaries, are what cover ash.userns-remap: unchanged, docs gap already noted.Note 2: kept the names case as a second guard; the
set -- rootcase is the one that pins the design.🤖 Generated with Claude Code