Repository navigation
Description
Problem
The published Server image does not declare an OCI USER. Plain docker run and the reference Compose services therefore start as root. The Helm chart explicitly sets UID/GID 1000, runAsNonRoot, RuntimeDefault seccomp, dropped capabilities and no privilege escalation for the API, worker and scheduler roles, but the bootstrap Job defaults both security-context objects to {} and therefore also inherits root from the image.
This creates different privilege defaults for the same image depending on the launcher. The Dockerfile already creates/chowns application and Apache runtime directories for UID/GID 1000, and the 2.11.7 read-only-root reproduction in #694 proved the API, worker, scheduler and bootstrap entrypoints can all reach their intended runtime/dependency boundary as UID 1000. Root is not required for those bounded startup paths.
Current immutable-image evidence:
- Server
2.11.7, sourcef55f90e54a213872f1c164a1b22c082d859c18aa; - amd64 digest
sha256:be7c2288ff935396262d743590530b204fac11b76582e56a525cc81a8da840d3; - image
Config.Useris empty; - reference Compose services do not set
user; - chart API/worker/scheduler set UID 1000, while bootstrap security contexts are empty.
This is defense-in-depth hardening for the existing stable surface, not an incident or a hold on the current 2.11.8 patch train.
Acceptance criteria
- Make the published image non-root by default, or explicitly run every shipped Docker/Compose role as a fixed non-root UID/GID. Prefer the image default so plain
docker runis safe without launcher folklore. - Give the Helm bootstrap Job the same non-root pod/container controls as the other roles: fixed UID/GID,
runAsNonRoot,RuntimeDefaultseccomp, no privilege escalation, and all capabilities dropped. - Verify API, worker, scheduler and bootstrap with the exact built image as non-root against SQLite and the supported external-persistence shape. Record effective UID/GID and prove no role silently escalates.
- Preserve writable-volume ownership and document the upgrade path for existing root-owned SQLite or mounted runtime directories; do not use broad chmod/chown fallbacks at every start.
- Add image/chart/Compose contracts that fail if the non-root default or role security contexts regress.
- Coordinate the writable-path mounts with Run the Server image and Helm chart with a read-only root filesystem #694 so non-root and read-only-root support are qualified together where practical, while keeping their acceptance claims distinct.
No release hold or operator action is requested.
Activity
rmcdaniel commentedon Nov 1, 2026
Security/runtime-ownership review against published Server 2.11.7 amd64
sha256:be7c2288ff935396262d743590530b204fac11b76582e56a525cc81a8da840d3, sourcef55f90e54a213872f1c164a1b22c082d859c18aa, and currentmain7fe74f88f4a83e583d4df7a37e0954d7e1142ea1: no release hold; the proposed hardening is sound, with one Helm detail and a narrower SQLite upgrade boundary.Current exact-image/source observations:
Config.Useris empty and a default isolatedidreportsuid=0(root) gid=0(root); explicit--user 1000:1000reports the image'swww-dataidentity at UID/GID 1000.mainstill has no DockerfileUSER, no published-Composeuser, and empty bootstrap pod/container security contexts.www-dataUID and GID to 1000 and chowns the application/runtime paths accordingly.runAsUser: 1000andfsGroup: 1000, but do not setrunAsGroup: 1000.fsGroupcontrols volume/supplementary access; it is not an explicit primary-GID pin. The implementation should addrunAsGroup: 1000to those three roles and bootstrap rather than describing the current chart as already fixing both UID and GID.Disposable, network-isolated SQLite ownership controls used only task-labelled synthetic Docker volumes:
/app/database, has directory1000:1000 0775and database1000:1000 0664. The current root-defaultserver-ensure-sqlitepath passed, and the same volume passed again with the container explicitly running as1000:1000.volume-nocopyvolume starts with a root-owned mount root. The current root-default image already fails becauseserver-ensure-sqlitedeliberately drops to UID/GID 1000 before checking or creating the database:Parent ... must be writable and searchable by UID 1000, GID 1000. After a one-time exact directory ownership correction to 1000:1000, the non-root helper created a1000:1000 0644database and passed.root:root 0600database inside an otherwise 1000:1000 directory also fails under today's root-default image after the same privilege drop. Correcting that exact file to 1000:1000 makes the explicit non-root check pass.This narrows the migration claim: a SQLite volume that succeeds through the current shipped entrypoint is already writable by UID/GID 1000 and should not break merely because the OCI default becomes non-root. Empty no-copy/bind mounts and root-owned database files are existing invalid ownership states, not a newly introduced incompatibility. They still need clear recovery guidance because operators may have bypassed the entrypoint or copied data manually.
Implementation/acceptance details:
USER 1000:1000after the root-only build preparation, and keep the existing runtime directories owned by that identity.runAsNonRoot, explicitrunAsUserandrunAsGroup,RuntimeDefaultseccomp, dropped capabilities, and no-privilege-escalation defaults. Add the missing explicitrunAsGroup: 1000to API/worker/scheduler too.id, plain named-volume SQLite startup, the empty/misowned negative controls, and all four real role entrypoints. Do not treat an entrypoint-only pass as API readiness.-wal,-shm, or journal cohort, establish that no hot journal remains, correct only the known directory/cohort to 1000:1000, then restart and verify. Do not add recursive startupchown,chmod 777, or silent ownership mutation.No product source, published image, production configuration, or persistent operator data was changed. General retains implementation ownership. Security's next useful step is exact-head review of the source PR and its synthetic/role acceptance; no repeat of these unchanged 2.11.7 ownership probes is needed before then.
rmcdaniel commentedon Nov 4, 2026
Server open-item pass, 2026-11-04.
Owner: Server for native image/chart scope, General for legacy PHP disposition, Security for hardening review. Current Dockerfile.native declares USER1000:1000, and the accepted immutable F image configuration records both platforms as1000:1000 in the #1019 preflight. That completes the native image-default component, not every legacy PHP bootstrap/volume acceptance criterion.
Next source work is a mode-specific chart/Compose rendering audit: native has no PHP bootstrap/queue/scheduler role, while legacy bootstrap and primary-GID defaults need explicit disposition. Preserve root-owned SQLite upgrade failures and avoid broad startup chown/chmod. General: confirm remaining legacy chart scope under the no-PHP-release stop; Server will return the concrete native render/volume test plan and Security reviews it with #694.
rmcdaniel commentedon Nov 6, 2026
Security #187 public-source reconciliation maps Sonar key AaFA7UiME2B5KEujJvSV (docker:S6471) to the exact current condition already owned here: Server main 59d5bb3 still has no OCI USER in the published Dockerfile, so plain Docker/Compose starts at root. The other three S6471 rows in the snapshot are disposable perf experiment images and remain contextual, not published-image evidence. No new hold, runtime action, or competing branch is proposed; the existing #878 acceptance criteria remain the right minimal product correction. Full disposition: durable-workflow/.github#187 (comment)
rmcdaniel commentedon Nov 11, 2026
Closing: the native image runs as USER 1000:1000 and the schema-upgrade job uses the same non-root context. The PHP image and bootstrap job receive no further releases.