You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On the php:8.4.26-apache image (also reproduced with a locally built variant), running the
container with all capabilities dropped while the Apache parent starts as root breaks Apache's
privilege separation silently from a security perspective: mod_unixd cannot setgid()
(no CAP_SETGID), logs an alert, but the worker still serves requests as UID 0 instead of www-data.
This matters because "root parent + --cap-drop ALL" is a common hardening recipe, and the
result is the opposite of the intended outcome: a compromised worker (any RCE in a PHP app)
executes as UID 0 rather than www-data.
Reproduction
docker run -d --name t --read-only --cap-drop ALL \
--tmpfs /var/run/apache2 \
-p 127.0.0.1:8080:80 php:8.4.26-apache
sleep 3
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/ # 200 — service looks healthy
docker top t -o user,pid,ppid,args
docker logs t 2>&1| grep AH02156
Observed:
$ docker top t -o user,pid,ppid,args
USER PID PPID COMMAND
root 1692 1669 /usr/sbin/apache2 -DFOREGROUND
root 3261 1692 /usr/sbin/apache2 -DFOREGROUND <- worker, UID 0
$ docker logs t | grep -m1 AH02156
[unixd:alert] [pid 36] (1)Operation not permitted: AH02156: setgid: unable to set group id to Group 33
/proc/<worker>/status confirms Uid: 0 0 0 0 (with CapEff: 0 — so this is not full
root capability-wise, but it still owns every root-owned file on the filesystem for DAC
purposes, e.g. it can read /etc/shadow via owner bits, which www-data cannot).
The alert appears once per forked worker; HTTP keeps serving 200s, so nothing in normal
operation tells the operator that privilege separation is gone.
For contrast, starting the same image properly as non-root
(--user 33:33, unprivileged port, tmpfs /var/run/apache2 owned by 33) works fine and
workers run as www-data as expected — no AH02156.
Context
We hit this while testing exploit containment for the recent PHP vulnerabilities
(CVE-2026-109618 / CVE-2026-109619, "php2shell") under hardened-container recipes. Full
write-up of the matched-pair experiment: https://github.com/srayuth089 (lab notes) — happy to attach raw logs if useful.
Questions
Is this behavior known / already being worked on (I couldn't find an existing issue)?
In the team's view, does this warrant a fix in the image (e.g. fail loudly at startup if
the parent is root but CAP_SETGID/CAP_SETUID are missing), or is it best handled as
documentation (a short "non-root usage" section in the image README describing the
supported pattern)?
If documentation is the preferred route, we're happy to send a PR with the verified
non-root recipe (user 33, port >1024, /etc/apache2/envvars, tmpfs ownership) — would
that be welcome in docker-library/docs, here, or both?
Not claiming this is a CVE by itself — it's a hardening-guidance foot-gun that inverts the
user's security intent, discovered during authorized lab research.
Apache treats setgid() failing as “not handled” and ultimately returns OK, so the MPM lets the child continue startup per the source code.
I find that a little odd... A hard error seems more logical to me, but admittedly it may be behavior from a more friendly-era.
Back to the image: I'm not sure the image logic should try to behave differently than Apache itself, but maybe it's worth exploring whether Apache should/could handle the condition more strictly...
Thanks @joshtrichards — that matches our observations exactly. For completeness, the failure mode we measured (in case it's useful for deciding where the fix belongs):
Worker keeps serving HTTP 200 as UID 0 (/proc/<worker>/status: Uid: 0, CapEff: 0) — only trace is the AH02156 alert per fork, so from an operator's view it is silent.
One deployment-relevant asymmetry worth noting: with all capabilities dropped, the resulting UID-0-no-caps worker can read root-owned files (owner bits, e.g. /etc/shadow in the container) but cannot read app-UID-owned 0600 files (no CAP_DAC_OVERRIDE), while a correct non-root startup inverts that. So the silent failure doesn't just reduce safety — it silently changes the post-compromise blast radius.
We verified a working non-root recipe end-to-end (user 33, unprivileged port, sourcing /etc/apache2/envvars, tmpfs /var/run/apache2 owned by the runtime UID): no AH02156, workers as www-data.
Agreed the image shouldn't diverge from httpd behavior; raising the strictness upstream with httpd sounds like the right home for the fix. Happy to send a docs PR for the non-root recipe here or in docker-library/docs if welcome.
Activity
joshtrichards commentedon Oct 24, 2026
Apache treats
setgid()failing as “not handled” and ultimately returns OK, so the MPM lets the child continue startup per the source code.I find that a little odd... A hard error seems more logical to me, but admittedly it may be behavior from a more friendly-era.
Back to the image: I'm not sure the image logic should try to behave differently than Apache itself, but maybe it's worth exploring whether Apache should/could handle the condition more strictly...
srayuth089 commentedon Oct 25, 2026
Thanks @joshtrichards — that matches our observations exactly. For completeness, the failure mode we measured (in case it's useful for deciding where the fix belongs):
/proc/<worker>/status:Uid: 0,CapEff: 0) — only trace is the AH02156 alert per fork, so from an operator's view it is silent./etc/shadowin the container) but cannot read app-UID-owned0600files (noCAP_DAC_OVERRIDE), while a correct non-root startup inverts that. So the silent failure doesn't just reduce safety — it silently changes the post-compromise blast radius./etc/apache2/envvars, tmpfs/var/run/apache2owned by the runtime UID): no AH02156, workers as www-data.Agreed the image shouldn't diverge from httpd behavior; raising the strictness upstream with httpd sounds like the right home for the fix. Happy to send a docs PR for the non-root recipe here or in docker-library/docs if welcome.