Skip to content

Run the release image as uid 65532 - #374

Open
schronck wants to merge 3 commits into
mainfrom
fix/docker/nonroot-runtime-image
Open

Run the release image as uid 65532#374
schronck wants to merge 3 commits into
mainfrom
fix/docker/nonroot-runtime-image

Conversation

@schronck

Copy link
Copy Markdown
Collaborator

The runtime image had no USER, so the container ran as uid 0. The binary never needed it: it binds 8080 and 9000, both above 1024, and writes only under its data directory.

The image

Base moves to gcr.io/distroless/cc-debian12:nonroot. I read the image config from the registry rather than trusting the tag name:

User:       '65532'
WorkingDir: '/home/nonroot'

USER 65532:65532 is also stated explicitly in the Dockerfile. The base already sets it, but stating it keeps the runtime identity visible in the file and surviving a base-image change.

One thing the issue did not mention, and it would have broken docker run

DECPM_DIR defaulted to /. It is the root dir, and the app writes $DECPM_DIR/data — so the default put the database at /data, which uid 65532 cannot create. Switching the base without touching this would have left the image failing on its first write for anyone running it directly.

It now defaults to /home/nonroot, which the nonroot user owns. Deployments are unaffected either way: the guide mounts a volume and passes -d /app, and development/docker-compose.yml builds from a different Dockerfile and sets DECPM_DIR: /app itself.

The deployment guide

Pod-level:

securityContext:
  runAsNonRoot: true
  runAsUser: 65532
  runAsGroup: 65532
  fsGroup: 65532

Container-level: allowPrivilegeEscalation: false and capabilities: drop: ["ALL"].

Two details worth a reviewer's eye:

  • The init container matters. busybox defaults to root, and with runAsNonRoot: true at pod level the pod refuses to start unless that container also runs nonroot. It inherits the pod context, and fsGroup is what then lets both it and the app write to the mounted PVC. Without fsGroup this change breaks the deploy.
  • readOnlyRootFilesystem is left commented, not set. It should hold — every /tmp and tempfile use I found is test code or the gen-types binary, not the server — but I cannot prove it without a live deploy, and a dependency writing to /tmp would surface only at runtime. The guide says to enable it and watch one restart.

Also documented: the image runs as 65532, and the workaround for v1.6.2 and earlier, which is to set runAsUser: 65532 plus the same fsGroup on the existing image.

Verification

No docker in this environment, so I could not build the image. What I did check:

  • The nonroot image config, pulled from gcr.io (above), confirming uid and home.
  • The guide's Deployment manifest parses as YAML, and the resulting security contexts and command are the intended ones.

The image itself is unverified by a build. The remaining risk is the base-image swap, which CI will exercise on this PR.

Follow-up the issue asks for and this does not do

Cut a new release so the rebuilt image picks up the current Debian 12 package versions.

That is a tag push, not a code change, so it stays with whoever cuts releases. The OS-level CVEs the scanners report come from the base image and clear on a rebuild.

Closes #372

The runtime image was built on gcr.io/distroless/cc-debian12 with no USER,
so the container ran as uid 0. The binary never needed it: it binds 8080
and 9000, both above 1024, and writes only under its data directory.

Switches the base to the :nonroot tag, which carries uid/gid 65532 and a
home directory it owns, and states USER 65532:65532 explicitly so the
runtime identity is visible in this file rather than inherited.

DECPM_DIR moves from / to /home/nonroot. It is the ROOT dir and the app
writes $DECPM_DIR/data, so the old default put the database at /data, which
uid 65532 cannot create. Deployments override it anyway, but a plain
docker run now works instead of failing on the first write.

The deployment guide gains a pod securityContext with runAsNonRoot,
runAsUser/runAsGroup 65532 and fsGroup 65532 on the data volume, plus
allowPrivilegeEscalation false and all capabilities dropped on the
container. The init container inherits the pod context: busybox defaults to
root, and with runAsNonRoot set the pod would refuse to start otherwise.
fsGroup is what lets both it and the app write to the mounted PVC.

readOnlyRootFilesystem is left commented rather than set. It should hold,
but it has not been proven against a live deploy and a dependency writing
to /tmp would surface only at runtime.

Closes #372
@schronck
schronck requested review from a team and sosaucily August 25, 2026 09:38
@schronck schronck self-assigned this Aug 25, 2026
@schronck
schronck requested review from scolear and a lite review from Copilot August 25, 2026 09:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the runtime container by running the release image as a non-root user (uid/gid 65532) and updates the Kubernetes deployment documentation to match the tightened security posture and filesystem ownership requirements.

Changes:

  • Switch the runtime base image to gcr.io/distroless/cc-debian12:nonroot and explicitly set USER 65532:65532.
  • Change the runtime default DECPM_DIR to /home/nonroot so the non-root process can create its data/ directory by default.
  • Update docs/DEPLOYMENT_GUIDE.md to add pod/container securityContext settings (runAsNonRoot, runAsUser, fsGroup, drop caps, etc.) and document the init-container interaction.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
docs/DEPLOYMENT_GUIDE.md Documents non-root runtime behavior and updates the Deployment example to use pod/container security contexts appropriate for uid 65532.
Dockerfile Moves the runtime image to distroless :nonroot, sets USER 65532:65532, and updates DECPM_DIR defaults for non-root writes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Dockerfile
The example mounted ./data at /data, which worked only because DECPM_DIR
defaulted to / and the app writes $DECPM_DIR/data. With the nonroot default
at /home/nonroot the container would write inside its own filesystem and
the mount would sit unused, so the data disappeared with the container
without any error.

Also adds the chown the nonroot image needs: a bind mount keeps the host's
ownership, so uid 65532 cannot write to it otherwise.

Review finding from Copilot on #372.
Same problem as the USER_GUIDE example: ./data mounted at /data worked only
under the old DECPM_DIR=/ default. Copilot flagged the USER_GUIDE one;
grepping for the pattern found this one too.

Review finding from Copilot on #372.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish a hardened non-root runtime image

2 participants