Skip to content

Honor a container image's USER and WORKDIR - #1918

Open
Davanum Srinivas (dims) wants to merge 5 commits into
agent-substrate:mainfrom
dims:pr/image-user
Open

Davanum Srinivas (dims) wants to merge 5 commits into
agent-substrate:mainfrom
dims:pr/image-user

Conversation

@dims

@dims Davanum Srinivas (dims) commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

USER resolves as Docker does, against the image's own /etc/passwd and /etc/group: uid:gid as is, a bare uid with its login group (gid 0 without an entry), names looked up or Run/Restore fails, group memberships as supplementary groups. No USER means root. atelet resolves it into the worker's ContainerSpec; the pause container follows the same rule through WorkloadSpec.pause_user. The process starts in the image's WORKDIR (default /), created in the actor's upper when the image lacks it.

Behavior change: images with a USER stop running as root; images with a WORKDIR stop starting in /. On gVisor no snapshot from before this restores: runsc restore checks Process.User and Process.Cwd of every container, and the pause image sets USER 65535. Micro-VM snapshots resume as they were.

Merge after #2392 (image layer owners): without it a non-root process cannot write the files its image gives it. On the micro-VM class, images that serve on a privileged port also need #1904.

Live run on kind, both classes, main + #1906 + #2392 + this PR: a node image (USER 1000, WORKDIR /app, durable dir) and the jupyter demo run as their user, write to their workdir and durable dir, and resume on demand after a suspend. On micro-VM uid 1000 cannot bind port 80 (#1904); gVisor allows it.

  • Tests pass: go test -race on Linux for atelet, imagecache, ocispec, ateworker-gvisor and kata.
  • Appropriate changes to documentation are included in the PR

@dims

Copy link
Copy Markdown
Collaborator Author

Comment thread cmd/atelet/oci.go Outdated
Comment thread internal/ocispec/ocispec.go Outdated
Comment thread cmd/atelet/oci_test.go

@mayawang Maya Wang (mayawang) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The USER and WORKDIR resolution looks right to me, and moving to moby/sys/user covers Ben's gid point. My main ask is a live run before this merges, since it changes who real images run as. It also needs a rebase: internal/ocispec/parity_test.go conflicts with main.

Comment thread cmd/atelet/oci.go Outdated
Comment thread docs/api-guide.md
Comment thread internal/imagecache/imagefile.go Outdated
Quentin Bisson (QuentinBisson) added a commit to giantswarm/substrate that referenced this pull request Oct 1, 2026
… root dependency of kagent-upstream#125 (#94)

docs(fork): the egress default's upstream exit is agent-substrate#1558; note the rootfs root dependency of kagent-upstream#125 (agent-substrate#1918)
@dims Davanum Srinivas (dims) changed the title atelet: honor a container image's own USER atelet: honor a container image's USER and WORKDIR Oct 1, 2026
Quentin Bisson (QuentinBisson) added a commit to giantswarm/substrate that referenced this pull request Oct 1, 2026
@mayawang

Copy link
Copy Markdown
Collaborator

Two gVisor issues from Preston Holmes (@ptone) that every image this change moves to non-root will hit: #2035 (the actor rootfs / is mode 0700, so a non-root user can't resolve any path) and #2036 (files the image gives to a non-root user show up as owned by uid 0). There's a fork fix for #2036 in giantswarm#115. Neither is covered by this stack, and I think we need a plan for both before this lands, otherwise images with a USER go from running as root to not starting. Davanum Srinivas (@dims) Benjamin Elder (@BenTheElder) let me know what you think.

@BenTheElder

Copy link
Copy Markdown
Collaborator

I agree that we should fix #2036 and probably need to fix it first.

Comment thread docs/upgrade.md Outdated
@dims

Copy link
Copy Markdown
Collaborator Author

Maya Wang (@mayawang) Plan is roughly spreadh between #1906 / #2392 (fixes #2036) and this PR.

#2035 is fixed in this PR's first commit (merged root 0755, layerless case included); #2430 fixes the upper only. Whichever lands first, I drop the other.

Ran the three together on kind, both classes: USER images start, write their own files and resume after suspend. Details in the live-run thread above. Privileged ports on microVM need #1904.

Nishanth Kotla (Nishanth29) pushed a commit to Nishanth29/substrate that referenced this pull request Oct 16, 2026
…nt-substrate#1906)

Durable-dir volumes were created `0700`, so a container running as a
non-root user could not write its own volume. Now `0777`, as Kubernetes
creates an emptyDir: one actor's containers can run as different users,
and the `0700` actor directory above keeps other actors out.

A restore keeps the mode the snapshot recorded, as emptyDir keeps a
pod's own chmod. Volumes created earlier stay `0700`, fine while every
container runs as root. No effect until an image's `USER` is honored
(agent-substrate#1918). Cleanup of files a non-root container leaves behind landed in
agent-substrate#1910.

Tests: `TestPrepareDurableDirVolume`. Exercised on kind with the
micro-VM class: uid 65532 writes a durable dir, then suspend and resume.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
@BenTheElder

Copy link
Copy Markdown
Collaborator

#1906 is in.
#2392 is close.

@dims
Davanum Srinivas (dims) force-pushed the pr/image-user branch 3 times, most recently from 4d5bcaf to 6361a58 Compare October 17, 2026 01:48
rootfs and upper were 0700. On gVisor the container's / takes that mode,
so a non-root process could not search its own root. 0755 is what
containerd and every image root use; the 0700 actor directory above
keeps other actors out. The micro-VM guest already sees 0755.
atelet writes the OCI spec before the worker mounts the rootfs, so it
cannot read the image's /etc/passwd from a mount. Image.ReadFile reads a
file from the cached layers top-down, honoring recorded whiteouts and
opaque dirs. Capped at 1 MiB.
Resolve USER as Docker does, against the image's own /etc/passwd and
/etc/group: uid:gid as is, a bare uid with its login group (gid 0 without
an entry), names looked up or Run/Restore fails, group memberships as
supplementary groups. No USER means root. atelet resolves it into the
worker's ContainerSpec; the pause container follows the same rule through
WorkloadSpec.pause_user.

On gVisor every snapshot taken before this needs re-taking, whatever the
image: runsc restore checks Process.User, and the pause image sets
USER 65535.
Start the process in the image's WORKDIR instead of "/", and create the
directory in the actor's upper when the image lacks it. atelet resolves
it into the worker's ContainerSpec. runsc restore checks Process.Cwd as
well as Process.User, so gVisor snapshots from before this need
re-taking.
Substrate makes no compatibility promise before v1.0.0, so a note about
one release's snapshots is noise in the upgrade procedure. The API guide
describes how the image's USER and WORKDIR apply.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/node area/security Security related issue/pr kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants