Skip to content

ateom gvisor: drop privileged: true#496

Merged
Zoe Zhao (zoez7) merged 4 commits into
agent-substrate:mainfrom
BenTheElder:ateom-gvisor-less-privileged
Jul 24, 2026
Merged

ateom gvisor: drop privileged: true#496
Zoe Zhao (zoez7) merged 4 commits into
agent-substrate:mainfrom
BenTheElder:ateom-gvisor-less-privileged

Conversation

@BenTheElder

@BenTheElder Benjamin Elder (BenTheElder) commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes #288

x-ref #174

Also, by not running with privileged: true we should stop mounting all host devices, so we can start thinking about passing through specific devices. cc Davanum Srinivas (@dims) Omer Yahud (@omeryahud)

We do this by:

  • Dropping privileged: true, which also ensures we're in a private cgroupns (we assume cgroups v2 container ecosystem which does this, cgroup v1 is broadly deprecated).
  • Enabling a lot of capabilities and unconfining apparmor (which we had from privileged and still need)
  • Setting up interior leaf cgroups and moving ateom itself into one.
  • Configuring the gvisor containers to run in leaf cgroups.

To be clear: ateom-gvisor is still highly permissioned, it's just not literally privileged: true anymore, so we drop unwanted side-effects from that.

It's a good idea to open an issue first for discussion.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

This PR was AI assisted. I have tested everything on kind + GKE locally and reviewed the changes.

Actor packets enter the worker pod via the host-side veth and leave through
the pod's eth0, which requires IPv4 forwarding. Without privileged the
runtime bind-mounts /proc/sys read-only, so the worker (which holds
CAP_SYS_ADMIN with no user namespace, leaving the ro flag unlocked) clears
the flag with a bind-remount, writes the sysctl, and restores read-only.
Replace the --ignore-cgroups workaround with real cgroup delegation so runsc
creates a per-actor-container cgroup leaf (with cpu/memory/pids accounting)
nested under the worker pod's own cgroup, so those stats roll up to the pod and
kubelet pod metrics capture the actor.

At startup the ateom prepares the pod cgroup for delegation
(setupCgroupDelegation): the unprivileged worker runs in a private cgroup
namespace, so /sys/fs/cgroup is the pod's own scope. It remounts that scope
read-write (the runtime bind-mounts it read-only), moves the worker's own
processes into a dedicated "ateom" leaf to satisfy the cgroup v2
no-internal-processes rule, and enables the delegated controllers in
cgroup.subtree_control. This only runs inside a private cgroup namespace; a
privileged worker inherits the host cgroup namespace (the true root, full of
unmovable kernel threads), so inPrivateCgroupNamespace() detects that via
/proc/self/cgroup and skips delegation.

Each container's cgroupsPath is set to "/<containerName>" in the bundle's OCI
spec before runsc create/restore (ensureContainerCgroupsPath). atelet emits a
runtime-agnostic spec, so the gVisor ateom fills in its own convention here,
mirroring how the micro-VM ateom assigns /ateomchv/<id>.
Give the ateom container a per-sandbox-class security context instead of
always running privileged. gVisor workers now run unprivileged: drop ALL
capabilities and add only the set runsc needs (the gofer's user-namespace
identity map needs SETUID/SETGID/SETPCAP/SETFCAP; the sandbox needs
SYS_ADMIN/SYS_CHROOT/SYS_PTRACE; actor networking needs NET_ADMIN/NET_RAW;
OCI rootfs setup needs DAC_OVERRIDE/FOWNER/CHOWN/MKNOD). The default
seccomp profile is retained; AppArmor is set to Unconfined because runsc's
mounts and the worker's cgroup remount are denied by the default profile
(enforced on GKE COS/Ubuntu), which a privileged worker got implicitly.

Micro-VM workers stay privileged (kata + cloud-hypervisor needs broad host
access). A tailored AppArmor profile for the gVisor worker is left as a
follow-up.

@dims Davanum Srinivas (dims) 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.

LGTM in principle :)

@zoez7

Zoe Zhao (zoez7) commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

I tested the cgroups fix locally and got the output below

I'm wondering why is the "counter" cgroup empty with no process IDs in it? is this intended?

~ $ ps auxfww
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     3974326  0.0  0.0 1235380 12420 ?       Sl   Jul22   0:01  \_ /usr/bin/containerd-shim-runc-v2 -namespace [k8s.io](https://www.google.com/url?sa=D&q=http%3A%2F%2Fk8s.io) -id 18abab4318a358c4581b105fc6af5a3d6a77633dde91c5c40bba6989d56dd5ef -address /run/containerd/containerd.sock
root     3974350  0.0  0.0   1028   612 ?        Ss   Jul22   0:00      \_ /pause
root     3974401  0.0  0.1 1276052 19984 ?       Ssl  Jul22   0:00      \_ /ko-app/ateom-gvisor --pod-uid=2af1cdd8-6868-4038-9089-89c351341956
root     3974770  0.0  0.1 1371012 24920 ?       Ssl  Jul22   0:00          \_ runsc-gofer --alsologtostderr=true --root=/var/lib/ateom-gvisor/actors/84a73f10-2f4f-4f6c-a178-d684ba1ff31b/runsc-state --log-format=json gofer --bundle=/var/lib/ateom-gvisor/actors/84a73f10-2f4f-4f6c-a178-d684ba1ff31b/bundles/pause --gofer-mount-confs=lisafs:self,lisafs:none --io-fds=6,7 --mounts-fd=4 --rpc-fd=5 --spec-fd=3 --sync-chroot-fd=-1 --sync-userns-fd=-1 --proc-mount-sync-fd=16 --apply-caps=false --setup-root=false pause
root     3974776  0.0  0.3 5632636 54468 ?       Ssl  Jul22   0:01          \_ runsc-sandbox --alsologtostderr=true --root=/var/lib/ateom-gvisor/actors/84a73f10-2f4f-4f6c-a178-d684ba1ff31b/runsc-state --log-format=json boot --apply-caps=false --bundle=/var/lib/ateom-gvisor/actors/84a73f10-2f4f-4f6c-a178-d684ba1ff31b/bundles/pause --controller-fd=8 --cpu-num=4 --cpu-period=100000 --dev-io-fd=-1 --fs-restore-fds=10,11,12,13 --gofer-filestore-fds=5 --gofer-mount-confs=lisafs:self,lisafs:none --io-fds=3,4 --mounts-fd=6 --setup-root=false --spec-fd=9 --start-sync-fd=7 --stdio-fds=14,15,16 --total-host-memory=16773177344 --total-memory=16773177344 --host-thp-defrag=madvise --proc-mount-sync-fd=24 --product-name=Google Compute Engine --host-thp-shmem-enabled=never pause
root     3974841  0.0  0.0  16456    28 ?        Ss   Jul22   0:00          |   \_ [exe]
root     3974857  0.0  0.0  19400   712 ?        S    Jul22   0:00          |   \_ [exe]
root     3974858  0.0  0.0  19400   712 ?        SN   Jul22   0:00          |   |   \_ [exe]
root     3974887  0.0  0.0  46004 12000 ?        S    Jul22   0:00          |   \_ [exe]
root     3974888  0.0  0.0  46004 12000 ?        SN   Jul22   0:00          |       \_ [exe]
root     3974896  0.0  0.0  46004 12000 ?        SN   Jul22   0:00          |       \_ [exe]
root     3974897  0.0  0.0  46004 12000 ?        SN   Jul22   0:00          |       \_ [exe]
root     3974898  0.0  0.0  46004 12000 ?        SN   Jul22   0:00          |       \_ [exe]
root     3975123  0.0  0.0  46004 12000 ?        SN   Jul22   0:00          |       \_ [exe]
root     3976064  0.0  0.0  46004 12000 ?        SN   Jul22   0:00          |       \_ [exe]
root     3974871  0.0  0.1 1371012 24468 ?       Ssl  Jul22   0:00          \_ runsc-gofer --allow-connected-on-save=true --root=/var/lib/ateom-gvisor/actors/84a73f10-2f4f-4f6c-a178-d684ba1ff31b/runsc-state --log-format=json --alsologtostderr=true gofer --bundle=/var/lib/ateom-gvisor/actors/84a73f10-2f4f-4f6c-a178-d684ba1ff31b/bundles/counter --gofer-mount-confs=lisafs:self,lisafs:none,lisafs:none,lisafs:self --io-fds=6,7,8,9 --mounts-fd=4 --rpc-fd=5 --spec-fd=3 --sync-chroot-fd=-1 --sync-userns-fd=-1 --proc-mount-sync-fd=18 --apply-caps=false --setup-root=false counter

~ $ cat /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2af1cdd8_6868_4038_9089_89c351341956.slice/cri-containerd-75fd066b6522b7d481a96e61797bd7a83368aaa98f0d02f12111cf6bf975534e.scope/counter/cgroup.procs

~ $ cat /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2af1cdd8_6868_4038_9089_89c351341956.slice/cri-containerd-75fd066b6522b7d481a96e61797bd7a83368aaa98f0d02f12111cf6bf975534e.scope/pause/cgroup.procs
3974770
3974776
3974841
3974857
3974858
3974871
3974887
3974888
3974896
3974897
3974898
3975123
3976064
 ~ $ cat /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod2af1cdd8_6868_4038_9089_89c351341956.slice/cri-containerd-75fd066b6522b7d481a96e61797bd7a83368aaa98f0d02f12111cf6bf975534e.scope/ateom/cgroup.procs
3974401
 ~ $

@BenTheElder

Benjamin Elder (BenTheElder) commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Ugh, I guess I didn't actually submit my reply?

I'm wondering why is the "counter" cgroup empty with no process IDs in it? is this intended?

Yes. So the pause / sandbox container has the whole gvisor ~pod under it. The other leaf cgroups are ~harmless. I left it versus adding more branching/complexity to the code for now but I could see either way.

gvisor should internally enforce the resources etc.

@zoez7 Zoe Zhao (zoez7) 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.

Left some questions. To be honest I'm totally pretending to know what I'm reviewing in this PR :). Manual verification looked good. LGTM to ship this since this is a improvement from what we have now, and let Tim and Taahir complain when they are back from vacation.

Comment thread cmd/ateom-gvisor/main.go
// only ever lists processes that are not already in a child cgroup, and the list
// shrinks as we drain it, so loop until the source is empty.
func moveProcs(ctx context.Context, srcProcs, dstProcs string) error {
for range 100 {

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.

Nit: could you leave a comment to explain where the number "100" comes from?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point.

@BenTheElder Benjamin Elder (BenTheElder) Jul 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a somewhat arbitrary upper bound on attempts. Added a code comment.

Comment thread cmd/ateom-gvisor/main.go
if private, err := inPrivateCgroupNamespace(); err != nil {
return fmt.Errorf("while detecting cgroup namespace: %w", err)
} else if !private {
slog.InfoContext(ctx, "not in a private cgroup namespace; skipping cgroup delegation (worker is likely privileged)")

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.

When do we expect worker to be privileged when using gvisor sandbox class?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a skew guard.

Though technically we could also hit this on say, a cgroup v1 host.

Also very technically: cgroupv2 unprivileged container == private cgroupns is more of a convention across docker/podman/containerd/cri-o.

You could also explicitly opt into into it in cgroupv1, but there was some sort of agreement to make it default with the migration to v2. But they're not actually linked from the kernel perspective, only major container runtimes.

sc := corev1ac.SecurityContext().
WithRunAsUser(0).
WithRunAsGroup(0)
if class == atev1alpha1.SandboxClassMicroVM {

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.

Does this mean the "shape" of cgroup hierarchy will look different depending on microvm vs. givsor?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, though that's only mildly relevant, since the actual actor cgroups are inside the microVM guest environment anyhow. It's a different kernel etc.

I'm also looking at patching uVM for this, for overlapping reasons (the device passthrough angle), but it's tricky.

@zoez7

Copy link
Copy Markdown
Collaborator

Also fyi Haven Xia (@HavenXia) changes like this will rely on the upgrade strategy that you are working on :)

Review feedback on agent-substrate#496: document why the loop re-reads cgroup.procs and
where the 100-iteration cap comes from.
@LiorLieberman

Copy link
Copy Markdown
Collaborator

Can we hold this? for egress/ingress work - we probably going to need privileges for installing nftable rules for redirection from the actor to the tunneling component.

@BenTheElder

Copy link
Copy Markdown
Collaborator Author

Can we hold this? for egress/ingress work - we probably going to need privileges for installing nftable rules for redirection from the actor to the tunneling component.

We already install nftables rules, the actor has NET_ADMIN still after this PR, (and a dozen other capabilities) just not literal privileged which has other baggage.

@zoez7
Zoe Zhao (zoez7) merged commit 2c32717 into agent-substrate:main Jul 24, 2026
11 checks passed
@HavenXia

Copy link
Copy Markdown
Collaborator

Also fyi Haven Xia (Haven Xia (@HavenXia)) changes like this will rely on the upgrade strategy that you are working on :)

Yes - for system upgrade with such change we will need some batch suspend and resume mechanism to handle instead of dropping everything.

@BenTheElder
Benjamin Elder (BenTheElder) deleted the ateom-gvisor-less-privileged branch July 24, 2026 20:41
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.

All actors are under the pause cgroup

5 participants