Repository navigation
Run containers as the image's USER and WORKDIR, with layer file owners kept - #2392
Conversation
Benjamin Elder (BenTheElder)
left a comment
There was a problem hiding this comment.
mostly LGTM, needs a rebase, many lingering ateom refs to cleanup and the import path changes / vanity URL
a147a98 to
73db1c9
Compare
|
Benjamin Elder (@BenTheElder) this is ready for another review! |
| // the target is a regular file it could read and write, which | ||
| // CAP_DAC_OVERRIDE grants (a regular file another user owns never keeps | ||
| // setuid or setgid in atelet, see restoreSetIDBits). Other targets | ||
| // (symlinks, fifos) are given to root before the link is made. |
There was a problem hiding this comment.
🤖 nit 🟢 – fifos hit the default case below and fail the unpack, so this should probably only mention symlinks?
There was a problem hiding this comment.
Right, fifos fail the unpack as before, so a symlink is the only non-regular hardlink target; the comment now says that and the check tests the symlink mode directly.
| dirs := make([]string, 0, len(dirModes)) | ||
| for name := range dirModes { | ||
| dirs = append(dirs, name) | ||
| // Restore the image's intended directory modes, mtimes and owners now that |
There was a problem hiding this comment.
🤖 nit 🟢 – This comment is 22 lines, and the ones above dirHdrs and restoreSetIDBits are close to that. Can we trim these to the ordering constraints (deepest first, chmod and mtime before chown) and drop the rest?
There was a problem hiding this comment.
Trimmed all three to deepest first and chmod/mtime before chown, plus one clause that chown keeps setgid and sticky bits; the protected_hardlinks note moved to the hardlink case.
| // with the path of each, without following symlinks. | ||
| func dirPaths(root *os.Root, dir string) (map[dirInode]string, error) { | ||
| paths := map[dirInode]string{} | ||
| err := fs.WalkDir(root.FS(), dir, func(name string, d fs.DirEntry, err error) error { |
There was a problem hiding this comment.
🤖 nit 🟢 – root.FS() rejects any path that isn't valid UTF-8, so this walk fails when the directory, or one below it, has a Latin-1 name, which tar allows. Replacing such a directory, or the fallback walk in the restore pass, then fails the whole unpack where root.RemoveAll used to cope. Walking with root.Open and ReadDir directly would avoid it.
There was a problem hiding this comment.
Good catch: dirPaths now walks with root.Open and ReadDir, and TestUnpackLayer_NonUTF8DirectoryNames covers a Latin-1 directory on the replace path and the restore fallback walk; it fails before and passes after.
| // permission bits plus setuid, setgid and sticky, as containerd and moby apply | ||
| // them. Without the sticky bit /tmp would be 0777 instead of 1777 in the | ||
| // actor, letting any user delete another's files there. | ||
| func tarMode(hdr *tar.Header) os.FileMode { |
There was a problem hiding this comment.
🤖 nit 🟢 – This is the same mask as tarutil.restoredMode. Maybe move that into owner.go next to Lchown, which already builds everywhere, and use it here?
There was a problem hiding this comment.
Moved to tarutil.Mode and tarutil.ModeBits in owner.go; snapshot restore and image unpack both use them.
73db1c9 to
aab7662
Compare
|
Should atelet get FOWNER and FSETID? Without them, Debian-style setgid binaries such as unix_chkpwd, chage, crontab and wall lose the bit. restoreSetIDBits also logs a warning per file on every pull. Adding them would also remove the ordering workarounds. DAC_OVERRIDE already lets atelet rewrite any file, so they wouldn't widen much. |
|
though x-ref #2428 |
|
Yes, added both to atelet in 77e2e6f. With them the unpack keeps the set-id bits (checked on unix_chkpwd, chage and expiry), and the two chowns that stood in for FOWNER are gone. The chmod after the chown stays, since chown clears setuid and setgid even for root. Re #2428: it changes the worker cap sets, not atelet's, but both PRs edit the same atelet/worker paragraph in |
77e2e6f to
a43f209
Compare
…gent-substrate#2979) Follow-up to agent-substrate#2279. @BenTheElder's last review asked for these, and they were pushed to agent-substrate#2279 before it merged, but main got the earlier commit. - Layout version moves to 2. Layers in a version 1 pool may not have been hashed against their diffID, and eviction never retires a layer that images keep using, so atelet refuses the old pool. - `pullWith` logs a diffID mismatch at WARN with ref, digest, and diffID. The RPC error was its only trace. ## Breaking change After an upgrade or rollback, atelet refuses the existing image cache. Before 1.0 a minor upgrade is a reinstall, so the upgrade runbook does not change. agent-substrate#2392 also moves the layout version to 2. Whichever lands second takes 3. Written with Claude Code.
Benjamin Elder (BenTheElder)
left a comment
There was a problem hiding this comment.
Lot of commits, did a review pass and tested some new review assistance agents on this one ...
I think this is something we clearly need to do but we have to be careful and I have a few concerns.
Some of these code comments could do with humanizing (not a new problem ... plenty of my earlier PRs especially too).
For sequencing we'll want to add correct user quickly after this (#1918). Eitan Yarmush (@EItanya) Tim Hockin (@thockin) Dmitry Berkovich (@dberkov) this would've been nice to land pre-GA, IMHO this is in the security direction, but we also probably need an API addition to allow overriding the image user + workdir on top of both of these ...
a43f209 to
41ee18f
Compare
atelet unpacked image layers as root and dropped each entry's owner and set-id and sticky bits. The unpack now applies both, and atelet gets CHOWN, FOWNER and FSETID to do so. The worker restores a directory's owner after its mode so gVisor keeps setgid. atelet refuses a version 2 cache (layout version 3), whose layers are root-owned. Signed-off-by: Anish Ramasekar <[email protected]>
4b6a50d to
97bd5d9
Compare
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. Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Anish Ramasekar <[email protected]>
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. Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Anish Ramasekar <[email protected]>
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. runsc restore checks Process.User, so a gVisor snapshot taken before this of an image that sets USER needs re-taking. Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Anish Ramasekar <[email protected]>
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. Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Anish Ramasekar <[email protected]>
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. Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Anish Ramasekar <[email protected]>
The actor images built from this repository write at the root of their filesystem and into the durable-dir volumes atelet creates as root. With the default base now nonroot and a container running as its image USER, they would start as uid 65532 and lose those writes, so the e2e demos break. These images stay on the root variant of the same distroless build. Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Anish Ramasekar <[email protected]>
Signed-off-by: Anish Ramasekar <[email protected]>
Tim Hockin (thockin)
left a comment
There was a problem hiding this comment.
I am OK with the state of this - ping me when you merge 1918 into it so I can look at the rest
The imagefs suite starts an actor from a probe built with USER nonroot:nonroot on the distroless nonroot base. It checks the uid, gid, supplementary groups, effective capabilities and working directory, and that the user can create a file in its WORKDIR. Signed-off-by: Anish Ramasekar <[email protected]>
Tim Hockin (@thockin) #1918 is merged into this PR now. PTAL! |
Tim Hockin (thockin)
left a comment
There was a problem hiding this comment.
I tried but all I could find were inconsequential nits which were not worth sending comments on
🙃 thanks! |
…nd WORKDIR, with layer file owners kept (#3473) Cherry-pick of #2392 onto `release-1.0` (`git cherry-pick -x` of the squash commit), it applied cleanly with no changes. Containers run as the image's `USER` and start in its `WORKDIR`, and atelet keeps the file owners recorded in image layers. Fixes #2036 for 1.0. AI use: AI-assisted. Clean pick. This carries the same breaking changes as #2392: - After an upgrade or rollback, atelet refuses the existing image cache. Delete `/var/lib/ate/image-cache` on each node once no actor runs on it, as atelet's error says. After an upgrade, a node reboot also works. - A custom atelet securityContext needs `CHOWN`, `DAC_OVERRIDE`, `FOWNER` and `FSETID`. - Images with a `USER` stop running as root, and images with a `WORKDIR` stop starting in `/`. A non-root `USER` has no effective capabilities, so `securityContext.capabilities.add` no longer helps it. - gVisor snapshots of such images don't restore, because `runsc restore` checks each container's user and working directory. Recreate those templates. MicroVM snapshots resume as they were. - Files an earlier run wrote as root into a durable dir stay root-owned. - [x] Tests pass - [x] Docs updated Signed-off-by: Anish Ramasekar <[email protected]> Signed-off-by: Davanum Srinivas <[email protected]> Signed-off-by: Benjamin Elder <[email protected]> Co-authored-by: Anish Ramasekar <[email protected]> Co-authored-by: Davanum Srinivas <[email protected]>
Containers ignored the image's
USERandWORKDIRand ran as root in/, and atelet's unpack dropped the file owners image layers record. So an image that hands a directory to its user, such as jupyter's/home/jovyan, didn't work as built.CHOWN,FOWNERandFSETIDto itsDAC_OVERRIDE. The image cache layout moves to version 3.USER, resolved as Docker does against the image's own/etc/passwdand/etc/group, and starts in itsWORKDIR(default/).DAC_OVERRIDEand--allow-root.The
USERandWORKDIRcommits are Davanum Srinivas (@dims)'s from #1918, kept as written. This PR supersedes #1918.Fixes #2036
Breaking change
Please add the
breaking-changelabel./var/lib/ate/image-cacheon each node once no actor runs on it, as atelet's error says. After an upgrade, a node reboot also works.CHOWN,DAC_OVERRIDE,FOWNERandFSETID.USERstop running as root, and images with aWORKDIRstop starting in/. A non-rootUSERis expected to have no effective capabilities, sosecurityContext.capabilities.addno longer helps it. This is checked under runc and in the gVisor source, not yet inside a gVisor or microVM sandbox.runsc restorechecks each container's user and working directory. Recreate those templates. MicroVM snapshots resume as they were.Tested:
go test -raceon atelet, imagecache, ocispec, tarutil, kata and the probe fixture, on macOS and on Linux as non-root, and imagecache with-count=3 -shuffle=on. The root-only imagecache and tarutil tests pass as root on a privileged Linux container.hack/verify-all.shpasses. The jupyter image serves/apiunder Docker as 1000:100 with every capability dropped. Davanum Srinivas (@dims) ran the jupyter demo and a non-root node image as their users on kind, on both runtimes, before #1904 merged; on microVM uid 1000 couldn't bind :80 then. A new imagefs e2e starts an actor from a probe image withUSER nonroot:nonrootandWORKDIR /home/nonroot. It checks uid and gid 65532, no supplementary group besides 65532, no effective capabilities, the working directory, and that the user can create a file there. CI runs it on gVisor and on microVM. At this head nothing ran in a gVisor or microVM sandbox yet: not the jupyter demo and not the imagefs e2e.