fix(image): let the runtime directories accept an arbitrary uid:gid - #81
Merged
zebby76 merged 1 commit intoSep 8, 2026
Merged
Conversation
Smals-Webtech#73 replaced COPY --chmod=777 with --chmod=775 on /opt and /app, and Smals-Webtech#74 carried that to 8.4. It locks out a process started with an explicit GID. docker run -u 1000 … id -> uid=1000 gid=0(root) docker run -u 1000:1000 … id -> uid=1000 gid=1000 Docker only falls back to group 0 when the user is given as a bare uid. With a uid:gid pair the process belongs to neither the owner (1001) nor the group (0) of a 775 directory, so it cannot write: docker run --rm -u 1000:1000 <cli> php -v [ERROR] ! /opt/etc is not writable. exit 1 That pair is not an exotic invocation. It is how the demo Makefiles run the builder container -- `--user $(id -u):$(id -g)` -- so that composer's output belongs to the person who started the build rather than to root. Every downstream team building an application image the same way hits the same wall. Nothing published is affected: 8.5.9 predates Smals-Webtech#73 and still ships 777. The regression exists only in main and 8.4, unreleased. Why the check I added in Smals-Webtech#73 did not catch it: I measured `-u 1000` and `-u 1000670000`, both bare uids that Docker completes with gid 0, where 775 is enough. The existing bats assertion does exactly the same, which is why it stayed green. It now asserts an explicit uid:gid instead. Reverting to 777 was the obvious fix and is not the one taken. Of the 89 world-writable entries the old image shipped, 61 were the configuration templates under /opt/config, which are never written at runtime; the ~24 that matter are the runtime directories. Those are 1777 now -- the /tmp semantics, anyone may create, only the owner may remove -- and everything else keeps 775. That leaves 22 world-writable entries instead of 89, all of them empty runtime directories carrying the sticky bit, which is stricter than what the image shipped before Smals-Webtech#73. The "nothing under /opt or /app is world-writable" assertion from Smals-Webtech#73 goes with it. It was too strong: it forbade what the design needs. It now states the real invariant -- nothing world-writable outside the four runtime paths, and every world-writable entry sticky -- which is a tighter thing to promise than the original, not a looser one. Verified with the workflow that surfaced this: `make -C test/demo-symfony composer-install` completes and its output is owned by the invoking user.
zebby76
added a commit
that referenced
this pull request
Sep 8, 2026
…backport of #81 to 8.4) (#82) #73 replaced COPY --chmod=777 with --chmod=775 on /opt and /app, and #74 carried that to 8.4. It locks out a process started with an explicit GID. docker run -u 1000 … id -> uid=1000 gid=0(root) docker run -u 1000:1000 … id -> uid=1000 gid=1000 Docker only falls back to group 0 when the user is given as a bare uid. With a uid:gid pair the process belongs to neither the owner (1001) nor the group (0) of a 775 directory, so it cannot write: docker run --rm -u 1000:1000 <cli> php -v [ERROR] ! /opt/etc is not writable. exit 1 That pair is not an exotic invocation. It is how the demo Makefiles run the builder container -- `--user $(id -u):$(id -g)` -- so that composer's output belongs to the person who started the build rather than to root. Every downstream team building an application image the same way hits the same wall. Nothing published is affected: 8.5.9 predates #73 and still ships 777. The regression exists only in main and 8.4, unreleased. Why the check I added in #73 did not catch it: I measured `-u 1000` and `-u 1000670000`, both bare uids that Docker completes with gid 0, where 775 is enough. The existing bats assertion does exactly the same, which is why it stayed green. It now asserts an explicit uid:gid instead. Reverting to 777 was the obvious fix and is not the one taken. Of the 89 world-writable entries the old image shipped, 61 were the configuration templates under /opt/config, which are never written at runtime; the ~24 that matter are the runtime directories. Those are 1777 now -- the /tmp semantics, anyone may create, only the owner may remove -- and everything else keeps 775. That leaves 22 world-writable entries instead of 89, all of them empty runtime directories carrying the sticky bit, which is stricter than what the image shipped before #73. The "nothing under /opt or /app is world-writable" assertion from #73 goes with it. It was too strong: it forbade what the design needs. It now states the real invariant -- nothing world-writable outside the four runtime paths, and every world-writable entry sticky -- which is a tighter thing to promise than the original, not a looser one. Verified with the workflow that surfaced this: `make -C test/demo-symfony composer-install` completes and its output is owned by the invoking user. (cherry picked from commit 3acad65)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Repairs a regression introduced by #73 and carried to
8.4by #74. Nothing published isaffected —
8.5.9predates #73 and still ships777; the regression exists only inmainand8.4, unreleased.What broke
#73 replaced
COPY --chmod=777with--chmod=775on/optand/app. That locks out a processstarted with an explicit GID:
With a
uid:gidpair the process is neither the owner (1001) nor in the group (0) of a775directory, so it cannot write.
That pair is not an exotic invocation. It is how the demo Makefiles run the builder container —
--user $(id -u):$(id -g)— so that composer's output belongs to the person who started the buildrather than to root. Every downstream team building an application image the same way hits the same
wall.
Why the check added in #73 did not catch it: the measurements used
-u 1000and-u 1000670000, both bare uids that Docker completes with gid 0, where775suffices. The existingbats assertion (
tests.cli.bats:47) does exactly the same, which is why it stayed green.What this does instead of reverting to 777
Of the 89 world-writable entries the old image shipped, 61 were the configuration templates under
/opt/config— never written at runtime. The ~24 that matter are the runtime directories. Thosebecome
1777(the/tmpsemantics: anyone may create, only the owner may remove); everything elsekeeps
775.RUN find /opt/etc /opt/sbin /app/tmp /app/var -type d -exec chmod 1777 {} +maintoday-u 1000:1000-u 1000,-u 1000670000,-u 4242:4242drwxrwxrwt/opt/config22 sticky directories is stricter than the 89 the image shipped before #73, so this is not a
revert.
The #73 assertion is tightened, not dropped
Nothing under /opt or /app is world-writablefailed here, correctly — it forbade what the designneeds. It now states the real invariant, which promises more than the original, not less:
/opt/etc,/opt/sbin,/app/var,/app/tmp;/opt/configin particular has none.A new
tests.cli.batsassertion runs the image as an explicituid:gid— the case that would havecaught this.
Verification
The workflow that surfaced the defect is the one that closes it:
make -C test/demo-symfony composer-installcompletes, andvendor/is owned by the invoking user(
sem:sem).Image suites:
nginx48/48,apache48/48,cli9/9.Demo stacks,
demo-infraup:demo-origin19/19,demo-prime12/12,demo-symfony6/6.Two failures seen along the way were traced rather than assumed, and neither is a regression: the
first
curl_containercall pulled its image and the pull progress polluted the captured output, andthe Symfony demo carried a stale compiled cache in its persistent volume
(
Call to undefined function deepclone_from_array(), present in/app/var/cache/dev/pools/systemand in neither the source nor
vendor/). Both pass once the image is cached and the cache cleared.MARKDOWN,NATURAL_LANGUAGE,BASH_EXECandSHELL_SHFMTlinters clean.DOCKERFILE_HADOLINTreports the same 9 pre-existing warnings as
mainand is disabled in CI.Backport to
8.4to follow — it carries the same--chmod=775lines and no1777.