fix: default to restricted OCI capability set (#786)

Changes the default Linux capability set for container processes from
`.allCapabilities` to `.defaultOCICapabilities`, making the library
secure-by-default. Callers that genuinely need elevated capabilities
must now opt in explicitly.

Signed-off-by: michael_crosby <michael_crosby@apple.com>
This commit is contained in:
Michael Crosby authored and GitHub committed 2026-07-02 12:45:15 -04:00
1 parent b438e97b93
commit bd709ba8cb
3 files changed
+33 -4

No files matched your search

@@ -376,8 +376,12 @@ public struct LinuxProcessConfiguration: Sendable {
/// process and its children cannot gain additional privileges via setuid/setgid binaries
/// or file capabilities.
public var noNewPrivileges: Bool = false
/// The Linux capabilities for the container process.
public var capabilities: LinuxCapabilities = .allCapabilities
/// The Linux capabilities for the container process. Defaults to
/// ``LinuxCapabilities/defaultOCICapabilities`` — the restricted baseline used by
/// runc/containerd, which excludes privileged capabilities such as `CAP_SYS_ADMIN`.
/// Callers that require additional capabilities (for example, privileged containers)
/// must opt in explicitly, e.g. by setting ``LinuxCapabilities/allCapabilities``.
public var capabilities: LinuxCapabilities = .defaultOCICapabilities
/// Whether to allocate a pseudo terminal for the process. If you'd like interactive
/// behavior and are planning to use a terminal for stdin/out/err on the client side,
/// this should likely be set to true.
@@ -398,7 +402,7 @@ public struct LinuxProcessConfiguration: Sendable {
user: ContainerizationOCI.User = .init(),
rlimits: [LinuxRLimit] = [],
noNewPrivileges: Bool = false,
capabilities: LinuxCapabilities = .allCapabilities,
capabilities: LinuxCapabilities = .defaultOCICapabilities,
terminal: Bool = false,
stdin: ReaderStream? = nil,
stdout: Writer? = nil,
-1
View File
@@ -114,7 +114,6 @@ extension Application {
config.process.setTerminalIO(terminal: current)
config.process.arguments = arguments
config.process.workingDirectory = cwd
config.process.capabilities = .allCapabilities
for mount in self.mounts {
let paths = mount.split(separator: ":")
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import ContainerizationOCI
import ContainerizationOS
import Foundation
import Testing
@@ -66,4 +67,29 @@ struct LinuxContainerTests {
#expect(process.arguments == ["/bin/sh", "-c", "echo 'hello'", "&&", "sleep 10"])
}
@Test func defaultCapabilitiesAreRestrictedOCISet() {
// Regression guard against shipping `.allCapabilities` as the default.
// A default container must not receive CAP_SYS_ADMIN, which would let it
// write /proc/sys/kernel/core_pattern and escape to guest-root. Cover both
// construction paths: the no-argument init (property default) and the full
// memberwise init (parameter default).
let viaProperty = LinuxProcessConfiguration()
let viaInit = LinuxProcessConfiguration(arguments: ["/bin/sh"])
for caps in [viaProperty.capabilities, viaInit.capabilities] {
for set in [caps.bounding, caps.effective, caps.permitted, caps.inheritable, caps.ambient] {
#expect(!set.contains(.sysAdmin), "default capabilities must not include CAP_SYS_ADMIN")
}
}
// The default must be exactly the documented OCI baseline.
let expected = LinuxCapabilities.defaultOCICapabilities
#expect(viaProperty.capabilities.bounding == expected.bounding)
#expect(viaProperty.capabilities.effective == expected.effective)
#expect(viaProperty.capabilities.permitted == expected.permitted)
#expect(viaProperty.capabilities.inheritable == expected.inheritable)
#expect(viaProperty.capabilities.ambient == expected.ambient)
#expect(viaInit.capabilities.bounding == expected.bounding)
}
}