Repository navigation
Description
Split out of #2428, where Benjamin Elder (@BenTheElder) asked for worker seccomp to be handled as a separate change.
Current state
Both worker classes run with seccompProfile: Unconfined. RuntimeDefault is not an option: it denies pivot_root(2) whatever capabilities the worker holds, and runsc's gofer and virtiofsd both call it while building their own sandboxes (#1368, #1369). AppArmor is Unconfined too, because the default AppArmor profile denies mount.
With #2428 each worker keeps a fixed capability set, 9 for gVisor and 11 for micro-VM, including SYS_ADMIN and NET_ADMIN, and runs with no_new_privs. Nothing limits which syscalls it can make. A probe started in a gVisor worker container on EKS with the #2428 defaults reached the kernel with bpf, perf_event_open, io_uring_setup, keyctl, add_key, request_key and open_by_handle_at, and opened AF_VSOCK and AF_ALG sockets. With SYS_ADMIN held, the worker can load BPF programs.
The inner sandboxes already filter themselves: runsc's gofer and sentry, virtiofsd (--seccomp=kill), and cloud-hypervisor's VMM, vCPU and device threads all install their own filters. The gap is the worker process itself, the cloud-hypervisor main thread, and everything before those filters are installed.
A profile that works
Draft #2750 adds generated amd64 and arm64 Localhost profiles: moby's default allow list (seccomp/v0.2.4) plus the namespace and mount syscalls the workers need (clone, clone3, unshare, setns, mount, umount2, pivot_root, chroot and the new mount API). Every other syscall returns EPERM. chroot stays even though the workers no longer hold SYS_CHROOT, because runsc's gofer calls it inside its own user namespace.
Tested on EKS (Kubernetes 1.37, AL2023, kernel 6.18, containerd 2.2.7) on top of #2428 rebased onto c4518a13, with the amd64 profile applied to every worker through a test-only controller change:
TestWorkerSecurityFilesystempassed on gVisor (m6i.xlarge) and micro-VM (i4i.metal): golden creation, pause/resume and suspend/resume, with the ownership, mode and contents checks.- On the nodes, the worker ran in seccomp mode 2, and runsc's gofer and boot processes and the virtiofsd serving child still added their own filters on top.
- The same probe got
EPERMforbpf,perf_event_open,io_uring_setup,keyctl,add_key,request_key,userfaultfd,open_by_handle_at,init_module,kexec_load,sethostname,AF_VSOCKandAF_ALG.AF_PACKETand netlink sockets,unshare,mount,fsopen,pivot_rootandsetnsstill reached the kernel.
The arm64 profile is generated from the same lists but has not been run live.
Open question: getting the profile onto nodes
A Localhost profile has to be under the kubelet's seccomp directory on a node before a worker can start there. The options I can see:
- atelet installs it. atelet already runs on every node that hosts workers. It writes the profile for its architecture to a versioned name under the kubelet's seccomp directory, and the controller sets
Localhostwith that name. This keeps the fixed-defaults approach from Tighten worker permissions with runtime-specific defaults #2428 and adds no API. Costs: atelet needs a hostPath into the kubelet directory, whose location varies by distribution and so has to be configurable; a worker cannot start on a node until atelet has written the file; and when the profile changes, atelet has to roll out before the controller, with older versions left in place for workers that still use them. - The worker applies the filter itself at startup, before it starts a sandbox, as runsc and virtiofsd do. The filter is inherited by runsc, virtiofsd and cloud-hypervisor. There are no files on nodes, it ships with the binaries it confines, and the pod spec stays
Unconfined. Costs: Substrate needs a BPF builder (none is vendored, and libseccomp needs cgo), and processes started withkubectl execin the worker container are not covered. - Security Profiles Operator distributes a
SeccompProfile. Least code in Substrate, but every install needs another operator.
I'd go with option 1. It is the standard Kubernetes mechanism, keeps the profile visible in the pod spec, and atelet is already the per-node component. Happy to go another way.
Also to decide:
- One profile for both runtimes, as drafted, or one per runtime.
- How to keep the base list in step with moby.
go generate ./hack/seccompregenerates the profiles from the lists inhack/seccomp/syscalls.go. - Whether AppArmor should follow the same route. A custom AppArmor profile has the same distribution problem, with the extra step of loading it into each node's kernel.
Activity
Tighten worker permissions with runtime-specific defaults
destijl commentedon Oct 28, 2026
My read is this can wait until after 1.0 because we don't need to worry about customer workloads being broken by clawing back permissions and it's a defense-in-depth measure. Here's gemini's summary of the state:
The only software constrained by the worker's seccomp profile is Substrate's own code (ateworker-*) and the pinned runtime binaries (runsc, virtiofsd, cloud-hypervisor) that Substrate builds, tests, and distributes. Customer container images and their application code are completely decoupled from the host worker's seccomp profile.
Residual Security Risk of Shipping GA with
UnconfinedShipping GA with
seccompProfile: {type: Unconfined}on worker pods leaves the following security posture:runsc-sandbox(Seccomp: 2),runsc-gofer(Seccomp: 2),virtiofsd's serving child (Seccomp: 2), andcloud-hypervisor's VMM/vCPU/device threads (Seccomp: 2) all install their own seccomp filters at runtime.ateworker-*daemon itself, or insidecloud-hypervisor's main HTTP API thread (Seccomp: 0), they run in the host user namespace withCAP_SYS_ADMIN+CAP_NET_ADMINand an unconstrained syscall table. This leaves high-risk kernel attack surfaces that standard container runtimes (RuntimeDefault) typically block even forSYS_ADMINreachable:bpf(2): Loading arbitrary BPF programs into the host kernel (reachable becauseCAP_SYS_ADMINis held without a seccomp block).perf_event_open(2)andio_uring_setup(2): High-frequency kernel CVE attack surfaces.keyctl(2),add_key(2),request_key(2): Host kernel keyring manipulation.open_by_handle_at(2): Host filesystem traversal bypass on micro-VM workers whereDAC_READ_SEARCHis held.Tighten worker permissions with runtime-specific defaults
Tighten worker permissions with runtime-specific defaults
Tighten worker permissions with runtime-specific defaults
Tighten worker permissions with runtime-specific defaults (agent-subs…
Tighten worker permissions with runtime-specific defaults (#2428)