Skip to content

Run the Docker image as a non-root user #1121

Description

@gaul

The Docker image currently runs everything as root. With #401 complete (jlink-trimmed JRE, #993), the remaining obvious hardening gap is dropping root: container scanners flag it (CIS Docker Benchmark 4.1), and the Kubernetes restricted Pod Security Standard rejects root images unless deployments override runAsUser. Non-root was deliberately deferred from the #993 review as out of scope, and it changes long-standing defaults, so filing this to collect feedback before committing to an approach.

Current behavior

  • dumb-init, run-docker-container.sh, and java all run as uid 0.
  • The default S3PROXY_ENDPOINT=http://0.0.0.0:80 binds a privileged port.
  • The run script creates JCLOUDS_FILESYSTEM_BASEDIR (default /data) at startup, which works only because root may create directories under /.

Proposal

  1. Create a dedicated s3proxy user and group with a fixed, documented uid/gid and add a numeric USER <uid>:<gid> (numeric so Kubernetes runAsNonRoot validates without an explicit runAsUser).
  2. Change the default endpoint to http://0.0.0.0:8080, EXPOSE 8080, and use 8443 in the secure-endpoint examples.
  3. Pre-create /data in the Dockerfile owned by that user, keeping the script's mkdir as a best-effort for custom basedirs.

Breaking changes and workarounds

1. Default port moves from 80 to 8080. Existing docker run --publish x:80 invocations, compose files, Kubernetes Services with containerPort: 80, and health checks must switch to port 8080. Workarounds:

  • Update the mapping (--publish 80:8080 preserves the same host-side port), or
  • Keep the old behavior with --env S3PROXY_ENDPOINT=http://0.0.0.0:80. Binding port 80 as non-root actually works on Docker ≥ 20.10 and rootless Podman because they set net.ipv4.ip_unprivileged_port_start=0 inside the container. It does not work on Kubernetes with containerd/CRI-O defaults; there you would additionally set that sysctl via securityContext.sysctls (allowed as a safe sysctl in recent Kubernetes) or fall back to --user root.

2. /data ownership. Bind mounts (--volume /host/dir:/data) whose host directory is owned by root or another uid will fail with permission errors. Named Docker volumes populated after this change inherit the image ownership and work; volumes created by older versions keep root ownership. Workarounds: chown the host directory to the documented uid, run with --user $(id -u) so the container matches the host owner, use Podman's :U volume flag or --userns=keep-id, or set fsGroup in the Kubernetes pod spec and let the kubelet fix ownership.

3. Derived images. Anything building FROM this image and running apt-get or writing to /opt/s3proxy must insert USER root before those steps (and ideally switch back afterwards). Files COPYed into derived images need to be readable by the s3proxy uid.

4. Mounted keystores and configs. A keystore for S3PROXY_SECURE_ENDPOINT (or any mounted config) must be readable by the non-root uid; previously root could read any mounted file. Workaround: chmod/chown the mounted file, or --user root.

In all cases --user root (or runAsUser: 0) restores the exact old behavior, so nothing is unfixable, but every workaround is action required from existing users, which is why this should land with a prominent release note, likely in a major version.

Alternatives considered

  • Keep root as the default and only document --user. Non-breaking, but running with --user today fails anyway because /data cannot be created and port 80 cannot be bound in some runtimes, so the image changes (pre-created /data, higher default port) are needed regardless; at that point the remaining difference is just whether USER is the default.
  • Publish a parallel -nonroot tag (as nginx does with nginx-unprivileged, or distroless with :nonroot). Zero breakage and lets users opt in, at the cost of a second image variant to build, test, and document. Could also serve as a transition period before flipping the default.
  • setcap cap_net_bind_service on the java launcher to keep port 80. Rejected: setcap puts the binary in secure-execution mode, which disables the $ORIGIN RPATH the JVM uses to locate libjli, and file capabilities are unreliable across image copy/flatten steps.

Open questions

  1. Which uid? ubuntu:24.04+ base images already reserve uid 1000 for the stock ubuntu user, so the choice is roughly: reuse ubuntu (uid 1000, convenient for bind mounts matching the common first host user), delete it and create s3proxy as 1000, use a system uid (999), or follow the distroless convention (65532).
  2. Flip the default in the next major release, or start with a -nonroot tag and flip later?
  3. Is 8080 the right default port, or is there a strong reason to prefer another (e.g. 9000 as MinIO users expect)?

Comments welcome, especially from anyone running the image on Kubernetes or building derived images.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions