Repository navigation
Restrict actor capabilities.add and make mounts nodev - #3580
Benjamin Elder (BenTheElder) merged 3 commits into
Conversation
securityContext.capabilities.add accepted any Linux capability. In a microVM, MKNOD lets a root container create a node for the guest's virtio-pmem root disk and write to the guest rootfs, and SYS_ADMIN, SYS_RAWIO and similar reach the guest kernel directly. add now accepts only AUDIT_WRITE, CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, FOWNER, FSETID, KILL, NET_BIND_SERVICE, SETFCAP, SETGID, SETPCAP, SETUID and SYS_CHROOT. ateapi and atelet reject anything else, and atelet ignores it when resolving the container's capabilities. drop still accepts any name. MicroVM guests also mount volumes nosuid,nodev, and set net.ipv4.ping_group_range so ping works without NET_RAW. Signed-off-by: Greg Castle <[email protected]>
|
|
Not that familiar with the repo here, so take this with a pinch of salt, but: substrate/internal/e2e/fixtures/probe/filesystem.go Lines 146 to 148 in ac8c621 EPERM now that MKNOD has been dropped.
|
Benjamin Elder (BenTheElder)
left a comment
There was a problem hiding this comment.
agent-assisted. thanks for working on this at the last minute.
| }, | ||
| SecurityContext: &ateapi.SecurityContext{Capabilities: &ateapi.Capabilities{ | ||
| Add: []string{"CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "SETUID", "SETGID", "MKNOD"}, | ||
| Add: []string{"CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "SETUID", "SETGID"}, |
There was a problem hiding this comment.
blocking (this is the CI failure): internal/e2e/fixtures/probe/filesystem.go:146-148 still calls syscall.Mknod in checkNonRootFilesystem and expects it to succeed.
We should flip that check to assert errors.Is(err, syscall.EPERM), which also gives us e2e coverage that mknod is denied inside the microVM guest.
There was a problem hiding this comment.
🤖 Done. The probe now requires errors.Is(err, syscall.EPERM), so the microVM lane asserts that mknod is denied inside the guest. Thanks Fabiano Fidêncio (@fidencio) for the matching suggestion.
| // The guest must not honor a device node or setuid bit on a volume. | ||
| for _, opt := range []string{"nosuid", "nodev"} { | ||
| if !slices.Contains(m.Options, opt) { | ||
| m.Options = append(m.Options, opt) | ||
| } | ||
| } |
There was a problem hiding this comment.
I don't think we should add "nosuid" here (blocking for runtime parity).
Neither containerd CRI nor gVisor (internal/ocispec/ocispec.go:172) sets nosuid on volume mounts. MS_NOSUID disables both setuid/setgid bits and security.capability xattrs (SETFCAP, which we allow here), breaking parity when e.g. /home is a durableDir or tools come from an imageVolume (docs/reference/api-guide.md:379). Inside the guest, setuid and file capabilities on a volume are still bounded by the container's CapBnd just like on rootfs, and setNosuidNodev (cmd/ateworker-microvm/internal/kata/overlay_linux.go:376-382) already sets nosuid on the host mount.
https://github.com/containerd/containerd/blob/c507a0257ea6462fbd6f5ba4f5c74facb04021f4/internal/cri/opts/spec_linux_opts.go#L126
https://github.com/google/gvisor/blob/760b80785dc53311d4c87bd2de167525a99709c1/runsc/boot/vfs.go#L1183-L1184
"nodev" makes sense so the guest won't honor a pre-existing device node on an external volume. Two non-blocking notes on "nodev":
ShapeGVisoralready strips"nodev"(internal/ocispec/gvisor.go:78-80), so"nodev"could live onbindininternal/ocispec/ocispec.go:172alongsidesystemMounts().rustjailapplies"nodev"viaMS_BIND | MS_REMOUNT, which in Linux (do_reconfigure_mnt) ignoresMS_RECand only setsMNT_NODEVon the top mount, not--announce-submountssubmounts. PassingOptions: []string{"nodev"}onkataSharedincmd/ateworker-microvm/internal/kata/overlay_linux.go:433-438would setMNT_NODEVon the virtiofs mount and its automounted submounts (finish_automountinheritsmnt_flags).
https://github.com/kata-containers/kata-containers/blob/03cf2ec06a5d302f23af811744f6bcea49988bfb/src/agent/rustjail/src/mount.rs#L866-L884
https://github.com/kata-containers/kata-containers/blob/03cf2ec06a5d302f23af811744f6bcea49988bfb/src/agent/src/storage/mod.rs#L509-L534
https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/fs/namespace.c#L3560
https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/fs/namespace.c#L3823-L3824
There was a problem hiding this comment.
🤖 Agreed, dropped nosuid so setuid binaries and file capabilities on volumes behave the same as under containerd and gVisor. Adopted both nodev notes:
nodevnow lives onbindinocispec.Build.ShapeGVisorstrips it, so gVisor specs remain byte-for-byte identical and existing gVisor snapshots restore without spec drift.CreateSandboxForActorpassesOptions: ["nodev"]on thekataSharedstorage, applyingMNT_NODEVto the virtio-fs mount and its automounted submounts. This also covers container rootfs trees on the share, which is safe because/devis a separate tmpfs. Note that this applies on fresh boots; memory restores resume the existing guest mount.
| var AllowedAddCapabilities = sets.New( | ||
| "AUDIT_WRITE", "CHOWN", "DAC_OVERRIDE", "DAC_READ_SEARCH", "FOWNER", | ||
| "FSETID", "KILL", "NET_BIND_SERVICE", "SETFCAP", "SETGID", "SETPCAP", | ||
| "SETUID", "SYS_CHROOT", | ||
| ) |
There was a problem hiding this comment.
blocking: should we exclude DAC_READ_SEARCH here (and in api/ateapi/v1/ateapi.proto and docs/reference/api-guide.md)?
Kubernetes Pod Security Standards Baseline excludes it, and in Linux CAP_DAC_READ_SEARCH also gates open_by_handle_at(2), which can escape a mounted subdirectory on a shared CSI filesystem. DAC_OVERRIDE already covers file read and directory search permission checks, and starting stricter lets us relax later without a breaking change if we ever need to.
https://github.com/kubernetes/kubernetes/blob/70d3cc986aa8221cd1dfb1121852688902d3bf53/staging/src/k8s.io/pod-security-admission/policy/check_capabilities_baseline.go#L62-L76
https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/fs/fhandle.c#L296-L299
https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/fs/namei.c#L418-L446
There was a problem hiding this comment.
🤖 Agreed, removed DAC_READ_SEARCH from the allowlist, the proto definition, and the API guide.
It was originally included because the "Image files owned by other users" section from PR #2392 recommended adding it for read-only access. That documentation bullet now points to DAC_OVERRIDE instead.
Anish Ramasekar (@aramase) FYI since this updates that guidance: DAC_OVERRIDE is broader for file permission bypasses, but avoids exposing open_by_handle_at. If read-only bypass without write bypass is needed later, we can add DAC_READ_SEARCH back without a breaking change.
| for _, a := range caps.GetAdd() { | ||
| effective[a] = struct{}{} | ||
| if resources.AllowedAddCapabilities.Has(a) { | ||
| effective[a] = struct{}{} | ||
| } | ||
| } |
There was a problem hiding this comment.
non-blocking: apivalidation.ValidateRunRequest and ValidateRestoreRequest already run ValidateCapabilities before prepareOCIBundles calls resolveCapabilities, so we don't need to silently filter unallowed entries here (per AGENTS.md, fail loudly instead of guarding against unreachable states or quietly dropping bad input).
There was a problem hiding this comment.
🤖 Removed. resolveCapabilities is back to default - drop + add, leaving admission validation as the single enforcement gate.
| | Minimum `limits.memoryBytes` | None enforced by worker | `128 MiB` (`134217728` bytes) | | ||
| | Per-container limits (`containers[].resources.limits`) | Accepted in the API, ignored at runtime | Enforced via guest cgroups (`cpuMillicores` floored at 10) | | ||
| | `NET_RAW` capability | Stripped (`runsc` default `--net-raw=false`) | Granted when listed in `securityContext.capabilities.add` | | ||
| | Unprivileged `ping` | Not configured | `net.ipv4.ping_group_range=0 2147483647` | |
There was a problem hiding this comment.
non-blocking: gVisor's netstack already allows unprivileged SOCK_DGRAM IPPROTO_ICMP and IPPROTO_ICMPV6 sockets for all users, so setting ping_group_range on microVM brings both classes to parity here (like ip_unprivileged_port_start=0 did for low ports).
We should probably drop this row and ## Capabilities (NET_RAW) below (and update the link in docs/reference/api-guide.md:378 to note both runtimes allow ICMP echo sockets without NET_RAW).
https://github.com/google/gvisor/blob/760b80785dc53311d4c87bd2de167525a99709c1/pkg/sentry/socket/netstack/provider.go#L56-L65
There was a problem hiding this comment.
🤖 Dropped the table row and the Capabilities (NET_RAW) section from sandbox-classes.md. The API guide note now clarifies that both runtimes permit ICMP echo sockets without NET_RAW.
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| assertWorkerFilesystem(t, raw) |
|
|
||
| // ValidateCapabilities checks each capability name. allowAll is set for drop, | ||
| // which accepts "ALL" and any capability name. Add accepts only | ||
| // AllowedAddCapabilities. |
There was a problem hiding this comment.
below in the switch, where we throw error name the individual capabilities the container needs, do we want to specify which ones are allowed?
There was a problem hiding this comment.
🤖 Unsupported capability names already return field.NotSupported with the full allowed list. The error for ALL in add now lists them as well:
'ALL' is not accepted; name the individual capabilities the container needs, from: AUDIT_WRITE, CHOWN, ...
Eitan Yarmush (EItanya)
left a comment
There was a problem hiding this comment.
🤖 AI-generated review. I looked through them to make sure they weren't crazy and they all seem worth looking at. (Comment left by Claude on behalf of Eitan Yarmush (@EItanya))
These are all optional, on top of what Ben already raised.
Optional follow-up, outside this diff: workerMicroVMCapabilities in cmd/atecontroller/internal/controllers/workerpool_apply.go keeps MKNOD "for guest device-node creation". With actors unable to add MKNOD and volumes mounted nodev, that worker grant may no longer be needed. FIFOs and sockets don't need CAP_MKNOD. I didn't check whether anything else in the guest creates device nodes over virtiofs, so this is a question for a separate PR.
| - **`NET_RAW`.** gVisor removes it even when `add` lists it, and microVM grants it. See [Capabilities (`NET_RAW`)](sandbox-classes.md#capabilities-net_raw). | ||
| - **`ping` without `NET_RAW`.** `NET_RAW` can't be added. The microVM guest sets `net.ipv4.ping_group_range=0 2147483647`, so any user can open ICMP echo sockets. See [Capabilities (`NET_RAW`)](sandbox-classes.md#capabilities-net_raw). | ||
| - **Setuid binaries work.** Neither runtime sets `no_new_privileges`, as for a Kubernetes container with `allowPrivilegeEscalation: true`; gVisor runs with `--allow-suid`. | ||
| - **Image files owned by other users.** Image files keep the owner their image's layers record, as under containerd. A container running as its image's `USER` can use what the image gave that user, for example after `RUN chown -R app:app /app` and `USER app`. A container running as root, because its image sets no `USER` or `USER root`, has no `DAC_OVERRIDE` or `DAC_READ_SEARCH` by default. On a file or directory another user owns, it gets only what the permission bits grant to others. Add `DAC_OVERRIDE` if it writes to such files, or `DAC_READ_SEARCH` if it only reads them. |
There was a problem hiding this comment.
Optional: if DAC_READ_SEARCH comes out of the allowlist per Ben's comment, this bullet still tells users to add it for read-only access. It should point to DAC_OVERRIDE instead.
There was a problem hiding this comment.
🤖 Fixed. The bullet now directs users to add DAC_OVERRIDE when reading or writing files owned by other image users.
| // Containers cannot add NET_RAW, so let every group open ICMP echo | ||
| // sockets for ping. | ||
| if _, ok := spec.Linux.Sysctl["net.ipv4.ping_group_range"]; !ok { | ||
| spec.Linux.Sysctl["net.ipv4.ping_group_range"] = "0 2147483647" |
There was a problem hiding this comment.
Optional: nothing in e2e checks that ping still works without NET_RAW. A small probe check that opens a SOCK_DGRAM/IPPROTO_ICMP socket as a non-root user would cover both runtimes, and would catch a later change to this sysctl.
There was a problem hiding this comment.
🤖 Added. On both runtimes, /filesystem?icmp=1 re-execs the probe as UID 1000 and verifies opening a SOCK_DGRAM/IPPROTO_ICMP socket. On microVM this guards the ping_group_range sysctl, and on gVisor it covers netstack's default behavior.
| // ValidateCapabilities checks each capability name. allowAll is set for drop, | ||
| // which accepts "ALL" and any capability name. Add accepts only | ||
| // AllowedAddCapabilities. | ||
| func ValidateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { |
There was a problem hiding this comment.
Optional: allowAll now also picks between the add allowlist and the full set of known names, so it really means "this is drop". All four callers pass a literal. Splitting this into separate add and drop functions, or renaming the flag, would make the rules clearer at the call sites.
There was a problem hiding this comment.
🤖 Split into ValidateAddCapabilities and ValidateDropCapabilities with a shared helper for syntactic validation (length and CAP_ prefix checks).
🤖 Agreed that dropping Because modifying |
| // when allowAll is set, which is the case for drop but not add. | ||
| func ValidateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { | ||
| // AllowedAddCapabilities are the capabilities a container may add. | ||
| var AllowedAddCapabilities = sets.New( |
There was a problem hiding this comment.
Thanks, updated PR description
| // that nest under one another. A volume at a system mount path, such as | ||
| // /tmp, replaces that mount in place. | ||
| // nodev: the runtime must not honor a device node on a volume. ShapeGVisor | ||
| // strips it, since runsc ignores it. |
There was a problem hiding this comment.
runsc does not ignore it. nodev (and other options like nosuid) are applied to the host bind mount and should be used for both gVisor and microVM.
Some options are not yet supported inside gVisor's kernel, so they are not exposed to the sandboxed application, but the host bind mount does get the options.
There was a problem hiding this comment.
Ack, I'll update this comment to drop that sentence. LMK if there's anything else you see in review.
There was a problem hiding this comment.
Ah, please also adjust the code that is removing nodev in ShapeGVisor. It should not do that:
substrate/internal/ocispec/gvisor.go
Line 78 in fc931c4
There was a problem hiding this comment.
Yeah the problem is that changing the gvisor mount options breaks restore for every existing gVisor snapshot. We could decide to do that as a pre-GA break, but it's pretty bad impact.
ShapeGVisor runs on restore as well, and runsc rejects a restore when the spec's mounts differ from the checkpoint's ("Mounts" does not match across checkpoint restore). The system mounts (/proc, /dev/shm, /sys, /tmp) already list nodev and lose it at that line, so dropping the strip changes the mounts of every gVisor actor, not just ones with volumes. I hit this on a test cluster when I first added nosuid,nodev on gVisor: actors suspended before the upgrade, and new actors from existing templates, all went to ACTOR_STATE_CRASHED.
Hence my question about how valuable it is to change in gVisor. It would have to be worth it to inflict that pain. To do this in a less breaking way we can support resuming with the previous mount options so the change isn't breaking. But that won't happen before GA.
Here's the AI description of how you'd need to recover if we made the breaking change:
Recovery / Migration Steps for Customers
- Pre-Upgrade Drain (Recommended to avoid state loss):
- Resume all suspended gVisor actors before upgrading.
- Allow active workloads to complete or flush state to durable external storage.
- Template Recreation:
- Because
ActorTemplateis immutable and cannot regenerate its golden snapshot in place, customers must recreate all gVisor templates. - Either deploy a new template version (e.g.,
my-template-v2), or delete and recreate the template to trigger a fresh golden snapshot run under the new mount configuration. - Update client workloads to reference the new template UID.
- Because
- Recovering Crashed / Suspended Actors:
RevertActordoes not recover crashed actors: it reverts to the latest external snapshot, which still contains the old checkpoint mounts and will crash again on the next resume attempt.- Repointing an existing actor via
UpdateActor --templatealso does not resolve it, because resume restores the actor's own checkpoint rather than the template's golden snapshot. - Customers must delete the affected actors (
DeleteActor) and recreate them against the new template. Any in-memory state or local rootfs delta is lost. - The only way to recover in-memory state from an un-drained, pre-upgrade snapshot is to temporarily roll back the cluster to the previous Substrate release, resume the actors, and drain them cleanly.
There was a problem hiding this comment.
OK since:
a) we're going to break snapshots whenever we make this change; and
b) we've already broken restores with another change
We should do this now to get more security and breaking changes out of the way before 1.0 final release. Updated code to pass through nodev.
DAC_READ_SEARCH also gates open_by_handle_at(2), which can escape a mounted subdirectory on a shared filesystem, and DAC_OVERRIDE already covers reads. Remove it from the add allowlist. MicroVM volumes no longer get nosuid, matching containerd and gVisor, so setuid binaries and file capabilities on volumes keep working. nodev moves to the shared bind options (ShapeGVisor strips it, so gVisor specs are unchanged), and the guest mounts the virtio-fs share nodev so its submounts are covered too. Split ValidateCapabilities into ValidateAddCapabilities and ValidateDropCapabilities, list the allowed names when add has ALL, and stop atelet from silently filtering adds that validation already rejects. The e2e probe now expects mknod to fail with EPERM in the guest and checks on both runtimes that a non-root user can open an ICMP echo socket. Signed-off-by: Greg Castle <[email protected]>
c7710e2 to
ab35273
Compare
ShapeGVisor stripped nodev from every mount on the assumption that runsc ignores it. runsc applies nodev to the host bind mount, so keep it, and gVisor and microVM now get the same nodev volumes and system mounts. This changes the mounts in every gVisor spec, and runsc rejects a MEMORY restore whose mounts differ from the checkpoint's. Signed-off-by: Greg Castle <[email protected]>
Benjamin Elder (BenTheElder)
left a comment
There was a problem hiding this comment.
I have two tiny nits that can wait:
| for i := range spec.Mounts { | ||
| spec.Mounts[i].Options = slices.DeleteFunc(slices.Clone(spec.Mounts[i].Options), func(opt string) bool { | ||
| return opt == "nodev" || opt == "strictatime" | ||
| return opt == "strictatime" | ||
| }) | ||
| } |
There was a problem hiding this comment.
nit: now that nodev stays, this only strips strictatime, which only /dev sets (internal/ocispec/ocispec.go:114) and /dev is dropped just above, so this loop can go?
| // ValidateCapabilities checks each capability name. "ALL" is accepted only | ||
| // when allowAll is set, which is the case for drop but not add. | ||
| func ValidateCapabilities(fldPath *field.Path, caps []string, allowAll bool) field.ErrorList { | ||
| // AllowedAddCapabilities are the capabilities a container may add. |
There was a problem hiding this comment.
nit: can we note this is the Pod Security Standards Baseline set minus MKNOD? It tells the next person where the list came from before they add something back.
…e#3580) Fixes [agent-substrate#3579](agent-substrate#3579) ### Summary Restricts `securityContext.capabilities.add` to a hardened allowlist of 12 Linux capabilities and blocks dangerous capabilities such as `MKNOD`, `SYS_ADMIN`, and `SYS_RAWIO` before 1.0. In microVM sandboxes, granting `MKNOD` to a container running as root allows it to create block device nodes matching the guest's `virtio-pmem` root disk and directly write to the guest filesystem. Other capabilities like `SYS_ADMIN` and `SYS_RAWIO` expose dangerous guest kernel attack surface. ### Changes * **Capability Allowlist:** `securityContext.capabilities.add` now only permits 12 capabilities required for package installs, non-root user switches, and standard container workflows: * `AUDIT_WRITE` * `CHOWN` * `DAC_OVERRIDE` * `FOWNER` * `FSETID` * `KILL` * `NET_BIND_SERVICE` * `SETFCAP` * `SETGID` * `SETPCAP` * `SETUID` * `SYS_CHROOT` * **Admission Enforcement:** `ateapi` and `atelet` reject actor template creations and updates specifying capabilities outside this allowlist. * **Drop Unrestricted:** `securityContext.capabilities.drop` continues to accept any capability name. * **MicroVM Hardening:** * Volume bind mounts are now mounted with `nodev`, and the microVM guest mounts the virtio-fs share with `nodev` (covering automounted submounts and rootfs). `ShapeGVisor` strips `nodev`, keeping gVisor specs identical and preserving checkpoint/restore compatibility. * MicroVM guest sysctl `net.ipv4.ping_group_range = "0 2147483647"` is configured so standard unprivileged `ping` continues to work without requiring `NET_RAW`. * **Documentation & Schemas:** Updated `api-guide.md`, `sandbox-classes.md`, and regenerated protobuf bindings (`ateapi.pb.go`, `atelet.pb.go`). ### Breaking change: gVisor `MEMORY` snapshots taken before this release won't restore gVisor now gets `nodev` on volume and system mounts (`/proc`, `/dev/shm`, `/sys`, `/tmp`), matching microVM. runsc applies it to the host bind mount. `runsc restore` rejects a checkpoint whose mounts differ from the new spec (`"Mounts" does not match across checkpoint restore`), so gVisor `MEMORY` snapshots taken before the upgrade can't be restored. rc.2 already breaks these snapshots with [agent-substrate#3533](agent-substrate#3533) (tmpfs `/dev/shm` and `/tmp`), so this adds no new upgrade step on top of that. MicroVM actors are unaffected. **What breaks:** - Suspended gVisor actors whose latest snapshot is `MEMORY` fidelity (the default) move to `ACTOR_STATE_CRASHED` on `ResumeActor`. - New actors from gVisor templates created before the upgrade crash on first resume, because the template's golden snapshot is an old `MEMORY` checkpoint. **Upgrade steps:** 1. **Before upgrading,** resume any suspended gVisor actor whose in-memory state you need, and save that state to durable storage or let its work finish. 2. **Re-create gVisor templates.** `ActorTemplate` is immutable, so create a new template (e.g. `my-template-v2`), or delete and re-create it, to build a new golden snapshot. Point clients at the new template. 3. **Replace crashed actors.** `RevertActor` won't help: it reverts to the same pre-upgrade checkpoint, which crashes again. `UpdateActor --template` won't either, because resume restores the actor's own snapshot. Delete the actor and create it again from the new template. Its in-memory state and its snapshot's rootfs and volume contents are lost. ### Testing * **Unit Tests:** Updated resource validation tests, parity tests, and microVM spec tests (`go test ./...` passes in container). * **Live GKE Testing (`sgke-rc1`):** * Verified template rejection when adding `MKNOD` or `SYS_ADMIN`. * Verified unprivileged ICMP `ping` works in microVM guests without `NET_RAW`. * Verified that pre-existing gVisor actor snapshots restore cleanly. * **e2e Probe Coverage:** * Verified inside the guest that `mknod` fails with `EPERM`. * Verified that unprivileged users can open `SOCK_DGRAM`/`IPPROTO_ICMP` sockets across both runtimes. Thanks to [Fabiano Fidêncio (@fidencio)](https://github.com/fidencio) for the report on MKNOD that led us to make this change. --------- Signed-off-by: Greg Castle <[email protected]> (cherry picked from commit 17fa858) Signed-off-by: dberkov <[email protected]>
Fixes [#3579](#3579) ### Summary Restricts `securityContext.capabilities.add` to a hardened allowlist of 12 Linux capabilities and blocks dangerous capabilities such as `MKNOD`, `SYS_ADMIN`, and `SYS_RAWIO` before 1.0. In microVM sandboxes, granting `MKNOD` to a container running as root allows it to create block device nodes matching the guest's `virtio-pmem` root disk and directly write to the guest filesystem. Other capabilities like `SYS_ADMIN` and `SYS_RAWIO` expose dangerous guest kernel attack surface. ### Changes * **Capability Allowlist:** `securityContext.capabilities.add` now only permits 12 capabilities required for package installs, non-root user switches, and standard container workflows: * `AUDIT_WRITE` * `CHOWN` * `DAC_OVERRIDE` * `FOWNER` * `FSETID` * `KILL` * `NET_BIND_SERVICE` * `SETFCAP` * `SETGID` * `SETPCAP` * `SETUID` * `SYS_CHROOT` * **Admission Enforcement:** `ateapi` and `atelet` reject actor template creations and updates specifying capabilities outside this allowlist. * **Drop Unrestricted:** `securityContext.capabilities.drop` continues to accept any capability name. * **MicroVM Hardening:** * Volume bind mounts are now mounted with `nodev`, and the microVM guest mounts the virtio-fs share with `nodev` (covering automounted submounts and rootfs). `ShapeGVisor` strips `nodev`, keeping gVisor specs identical and preserving checkpoint/restore compatibility. * MicroVM guest sysctl `net.ipv4.ping_group_range = "0 2147483647"` is configured so standard unprivileged `ping` continues to work without requiring `NET_RAW`. * **Documentation & Schemas:** Updated `api-guide.md`, `sandbox-classes.md`, and regenerated protobuf bindings (`ateapi.pb.go`, `atelet.pb.go`). ### Breaking change: gVisor `MEMORY` snapshots taken before this release won't restore gVisor now gets `nodev` on volume and system mounts (`/proc`, `/dev/shm`, `/sys`, `/tmp`), matching microVM. runsc applies it to the host bind mount. `runsc restore` rejects a checkpoint whose mounts differ from the new spec (`"Mounts" does not match across checkpoint restore`), so gVisor `MEMORY` snapshots taken before the upgrade can't be restored. rc.2 already breaks these snapshots with [#3533](#3533) (tmpfs `/dev/shm` and `/tmp`), so this adds no new upgrade step on top of that. MicroVM actors are unaffected. **What breaks:** - Suspended gVisor actors whose latest snapshot is `MEMORY` fidelity (the default) move to `ACTOR_STATE_CRASHED` on `ResumeActor`. - New actors from gVisor templates created before the upgrade crash on first resume, because the template's golden snapshot is an old `MEMORY` checkpoint. **Upgrade steps:** 1. **Before upgrading,** resume any suspended gVisor actor whose in-memory state you need, and save that state to durable storage or let its work finish. 2. **Re-create gVisor templates.** `ActorTemplate` is immutable, so create a new template (e.g. `my-template-v2`), or delete and re-create it, to build a new golden snapshot. Point clients at the new template. 3. **Replace crashed actors.** `RevertActor` won't help: it reverts to the same pre-upgrade checkpoint, which crashes again. `UpdateActor --template` won't either, because resume restores the actor's own snapshot. Delete the actor and create it again from the new template. Its in-memory state and its snapshot's rootfs and volume contents are lost. ### Testing * **Unit Tests:** Updated resource validation tests, parity tests, and microVM spec tests (`go test ./...` passes in container). * **Live GKE Testing (`sgke-rc1`):** * Verified template rejection when adding `MKNOD` or `SYS_ADMIN`. * Verified unprivileged ICMP `ping` works in microVM guests without `NET_RAW`. * Verified that pre-existing gVisor actor snapshots restore cleanly. * **e2e Probe Coverage:** * Verified inside the guest that `mknod` fails with `EPERM`. * Verified that unprivileged users can open `SOCK_DGRAM`/`IPPROTO_ICMP` sockets across both runtimes. Thanks to [Fabiano Fidêncio (@fidencio)](https://github.com/fidencio) for the report on MKNOD that led us to make this change. --------- Signed-off-by: Greg Castle <[email protected]> (cherry picked from commit 17fa858) Signed-off-by: dberkov <[email protected]>
Fixes #3579
Summary
Restricts
securityContext.capabilities.addto a hardened allowlist of 12 Linux capabilities and blocks dangerous capabilities such asMKNOD,SYS_ADMIN, andSYS_RAWIObefore 1.0.In microVM sandboxes, granting
MKNODto a container running as root allows it to create block device nodes matching the guest'svirtio-pmemroot disk and directly write to the guest filesystem. Other capabilities likeSYS_ADMINandSYS_RAWIOexpose dangerous guest kernel attack surface.Changes
securityContext.capabilities.addnow only permits 12 capabilities required for package installs, non-root user switches, and standard container workflows:AUDIT_WRITECHOWNDAC_OVERRIDEFOWNERFSETIDKILLNET_BIND_SERVICESETFCAPSETGIDSETPCAPSETUIDSYS_CHROOTateapiandateletreject actor template creations and updates specifying capabilities outside this allowlist.securityContext.capabilities.dropcontinues to accept any capability name.nodev, and the microVM guest mounts the virtio-fs share withnodev(covering automounted submounts and rootfs).ShapeGVisorstripsnodev, keeping gVisor specs identical and preserving checkpoint/restore compatibility.net.ipv4.ping_group_range = "0 2147483647"is configured so standard unprivilegedpingcontinues to work without requiringNET_RAW.api-guide.md,sandbox-classes.md, and regenerated protobuf bindings (ateapi.pb.go,atelet.pb.go).Breaking change: gVisor
MEMORYsnapshots taken before this release won't restoregVisor now gets
nodevon volume and system mounts (/proc,/dev/shm,/sys,/tmp), matching microVM. runsc applies it to the host bind mount.runsc restorerejects a checkpoint whose mounts differ from the new spec ("Mounts" does not match across checkpoint restore), so gVisorMEMORYsnapshots taken before the upgrade can't be restored. rc.2 already breaks these snapshots with #3533 (tmpfs/dev/shmand/tmp), so this adds no new upgrade step on top of that. MicroVM actors are unaffected.What breaks:
MEMORYfidelity (the default) move toACTOR_STATE_CRASHEDonResumeActor.MEMORYcheckpoint.Upgrade steps:
ActorTemplateis immutable, so create a new template (e.g.my-template-v2), or delete and re-create it, to build a new golden snapshot. Point clients at the new template.RevertActorwon't help: it reverts to the same pre-upgrade checkpoint, which crashes again.UpdateActor --templatewon't either, because resume restores the actor's own snapshot. Delete the actor and create it again from the new template. Its in-memory state and its snapshot's rootfs and volume contents are lost.Testing
go test ./...passes in container).sgke-rc1):MKNODorSYS_ADMIN.pingworks in microVM guests withoutNET_RAW.mknodfails withEPERM.SOCK_DGRAM/IPPROTO_ICMPsockets across both runtimes.Thanks to Fabiano Fidêncio (@fidencio) for the report on MKNOD that led us to make this change.