forked from RoushTech/docker
Grant the app user Docker socket access and a writable state dir, so downstreams stop wrapping the entrypoint #36
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The base image drops privileges to
appbut ships nothing that makesappable to dothe two things every downstream then needs, so each project reimplements the same root-time
entrypoint wrapper. Both belong here, behind opt-in env vars.
#22, #23 and #24 fixed the three concrete instances of this — the Mercure bolt store, the
session directory, the privilege drop that has to reach both. This is the same family one
level up: those were specific paths the base image already owned; these are the paths a
downstream owns and currently has to fix itself.
appto the Docker socket's group before thesu-execdropFRANKENPHP_DOCKER_SOCKET=On(+DOCKER_SOCKETpath override)chown appa bind-mounted writable state directory at bootAPP_VAR_DIR=/app/varBoth must run as root, before
/usr/local/bin/entrypoint— which is exactly the slotthe base image's own ENTRYPOINT occupies, so downstreams can only get there by wrapping it:
1. Docker socket group membership
Any image that mounts
/var/run/docker.sockand talks to the Engine API from PHP needsappin the socket's owning group. That group is not knowable at build time: the socketis
root:root(gid 0) on Docker Desktop androot:<docker>on a Linux host, so it has to beresolved at runtime.
someones.computer's wrapper, in full:Pure boilerplate, with three ways to get it wrong that are only discoverable by hitting them:
dockeror a gid at build timeaddgroup -gfails; membership never grantedinitgroups()The third is #22 verbatim, and it's the one that argues hardest for upstreaming: the base
image's choice of
su-execoverchpstis load-bearing for this script, and nothing aboutthe script says so unless the author already knows. Downstream services that also drop
privileges have to make the same choice by hand —
docker/services.d/worker/runhere carriesa paragraph of comment explaining why it is
su-exec app php bin/console messenger:consume …and not
chpst -u app.Defaulting the knob off keeps images with no socket mounted byte-identical in behaviour; the
[ -S "$SOCK" ]guard already makes the script a no-op there, but an explicit opt-in alsomeans no surprise
/etc/groupmutation in images that never asked.2. Ownership of the writable state directory
Anything the app writes must be owned by
app. Nothing keeps it that way, becausedocker compose exec <svc> …runs as root — the image'sUSERis root and runit dropsprivileges only for supervised services. So every ordinary operational command leaves
root-owned files behind in a bind-mounted state directory:
frankenphp, ours:worker)appdocker compose exec app …(migrations, seeds, asset builds)composer install/composer test:coveragevar/cache/,var/coverage/,var/test_*.dbOur concrete failure (Grey.ooo/someones.computer#197): a root-owned SQLite session file.
Still world-readable, so sign-in succeeded — and was then instantly forgotten when the
write at session close failed. A 302 straight back to
/loginwith no flash, no errorpage, and no log line. It reads exactly like wrong credentials, so the search goes to the
user table, the firewall config and CSRF before it goes to
ls -la var/.chown app:appfixed it on the next attempt.
Suggested behaviour: if
APP_VAR_DIRis set,chown appit at boot, non-recursivelyby default. Recursion is the wrong default here, not just a slow one — these directories hold
caches and coverage data, and recursing over tens of thousands of small files on a bind mount
(especially a Docker Desktop virtiofs/gRPC-FUSE mount) adds seconds to every container start
for no benefit. A
APP_VAR_DIR_RECURSIVE=Onescape hatch covers the cases that genuinelyneed it.
Why this one generalises
Every de-privileged image with a bind-mounted writable directory has this bug latent in it.
The ingredients are all supplied by the base image and the standard workflow, not by anything
the downstream chose:
USERstays root, soexecis root — that is Docker's behaviour, not ours;And the failure mode is silence, not an error. #23 at least emitted
SessionHandler::write(): … Permission deniedwarnings; this emits nothing, because fromPHP's point of view SQLite simply reported a failed write on a file it could open. A class of
bug where the only signal is "the thing you did didn't happen" is worth fixing centrally,
because the per-project cost is not the one-line
chown— it's the hour before you think tolook at ownership.
Done when
FRANKENPHP_DOCKER_SOCKET=Onresolves${DOCKER_SOCKET:-/var/run/docker.sock}'s gid atboot, finds-or-creates a matching group, and adds
appto it — before the privilege dropOffleaves/etc/groupuntouched; set-but-socket-absent warns on stderr andcontinues rather than failing the boot
root:root(gid 0, Docker Desktop) androot:docker(Linux host), including the case where the socket's gid already belongs to a
differently-named group
docker run … sh -c 'su-exec app curl -s --unix-socket /var/run/docker.sock http://localhost/version'returns 200 with the flag on
APP_VAR_DIR=<path>chowns that path toappat boot, non-recursivelyAPP_VAR_DIR_RECURSIVE=On(or equivalent) opts into recursion; unsetAPP_VAR_DIRis ano-op
/usr/local/bin/entrypoint, so no downstream needs to wrap theENTRYPOINT to reach that slot
PHP_*/FRANKENPHP_*toggles, with thesu-execvs
chpstnote (#22) stated where the socket flag is described — a downstream servicescript that drops privileges itself has to make the same choice
someones.computercan deletedocker/socket-entrypoint.shand itsENTRYPOINT/CMDwrapping, keeping only the two env vars
Related
chpst -u appdrops supplementary groups — the mechanism both items here depend on/var/lib/php-zts/sessionunwritable by the server userHOME=/rootsurvives the drop, blocking the Mercure bolt store#22/#23/#24 fixed base-image-owned paths. Item 2 is the same failure on a
downstream-owned path, which is why it needs a knob rather than a fix.