Skip to content

Run the Server image and bootstrap job as non-root by default #878

Closed

Description

@rmcdaniel

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, source f55f90e54a213872f1c164a1b22c082d859c18aa;
  • amd64 digest sha256:be7c2288ff935396262d743590530b204fac11b76582e56a525cc81a8da840d3;
  • image Config.User is 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 run is safe without launcher folklore.
  • Give the Helm bootstrap Job the same non-root pod/container controls as the other roles: fixed UID/GID, runAsNonRoot, RuntimeDefault seccomp, 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

  1. added
    kind:featureA public product capability or experience is requested
    priority:P3Lower-priority product improvement
    status:readyReady for implementation
    on Oct 30, 2026
  2. rmcdaniel commented on Nov 1, 2026

    @rmcdaniel
    MemberAuthor

    Security/runtime-ownership review against published Server 2.11.7 amd64 sha256:be7c2288ff935396262d743590530b204fac11b76582e56a525cc81a8da840d3, source f55f90e54a213872f1c164a1b22c082d859c18aa, and current main 7fe74f88f4a83e583d4df7a37e0954d7e1142ea1: no release hold; the proposed hardening is sound, with one Helm detail and a narrower SQLite upgrade boundary.

    Current exact-image/source observations:

    • OCI Config.User is empty and a default isolated id reports uid=0(root) gid=0(root); explicit --user 1000:1000 reports the image's www-data identity at UID/GID 1000.
    • Current main still has no Dockerfile USER, no published-Compose user, and empty bootstrap pod/container security contexts.
    • The image changes both the www-data UID and GID to 1000 and chowns the application/runtime paths accordingly.
    • API, worker and scheduler values set runAsUser: 1000 and fsGroup: 1000, but do not set runAsGroup: 1000. fsGroup controls volume/supplementary access; it is not an explicit primary-GID pin. The implementation should add runAsGroup: 1000 to 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:

    1. A normal new named volume, populated from the image's /app/database, has directory 1000:1000 0775 and database 1000:1000 0664. The current root-default server-ensure-sqlite path passed, and the same volume passed again with the container explicitly running as 1000:1000.
    2. An empty volume-nocopy volume starts with a root-owned mount root. The current root-default image already fails because server-ensure-sqlite deliberately 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 a 1000:1000 0644 database and passed.
    3. A synthetic root:root 0600 database 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:

    • Declare USER 1000:1000 after the root-only build preparation, and keep the existing runtime directories owned by that identity.
    • Give bootstrap the same runAsNonRoot, explicit runAsUser and runAsGroup, RuntimeDefault seccomp, dropped capabilities, and no-privilege-escalation defaults. Add the missing explicit runAsGroup: 1000 to API/worker/scheduler too.
    • Contract-test the built image's OCI user and effective 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.
    • Document a one-time offline repair for a misowned SQLite mount: stop every role, preserve/backup the database plus any -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 startup chown, chmod 777, or silent ownership mutation.
    • Preserve a rollback snapshot/ownership record for that operator action. External MySQL/PostgreSQL Compose users do not have an application SQLite-volume migration, but still require the role/runtime-path checks from Run the Server image and Helm chart with a read-only root filesystem #694.

    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.

  3. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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.

  4. rmcdaniel commented on Nov 6, 2026

    @rmcdaniel
    MemberAuthor

    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)

  5. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:featureA public product capability or experience is requestedpriority:P3Lower-priority product improvementstatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions