Managing Linux Capabilities and Capability Dropping in Containers
The container CLI provides --cap-add and --cap-drop flags that let you fine-tune Linux capabilities by modifying the default OCI capability set through a three-step algorithm implemented in RuntimeService.swift.
The apple/container repository implements a secure-by-default approach to managing Linux capabilities and capability dropping in containers through a deterministic flag-processing system. When you launch a container, the runtime calculates an effective capability set by combining default OCI capabilities with user-specified additions and removals. This calculation happens before the container starts, ensuring the kernel enforces only the permissions you explicitly grant.
How Capability Flags Work in container
The capability management system translates CLI arguments into kernel-enforced permission sets through two main phases: parsing and calculation.
CLI Parsing in Flags.swift
In Sources/Services/ContainerAPIService/Client/Flags.swift, the --cap-add and --cap-drop options are defined as string arrays that accept individual capability names or the special ALL sentinel:
@Option(
name: .customLong("cap-add"),
help: .init("Add a Linux capability (e.g. CAP_NET_RAW, or ALL)", valueName: "cap")
)
public var capAdd: [String] = []
@Option(
name: .customLong("cap-drop"),
help: .init("Drop a Linux capability (e.g. CAP_NET_RAW, or ALL)", valueName: "cap")
)
public var capDrop: [String] = []
These arrays are passed to the runtime service where the effective capability set is computed.
The Effective Capability Algorithm
The core logic resides in Sources/Services/RuntimeLinux/Server/RuntimeService.swift within the effectiveCapabilities function. This implementation follows Docker-compatible semantics using a three-step ordered process.
Capability Calculation Logic
The effectiveCapabilities function processes capabilities in strict sequence to determine the final set:
- Base set initialization – If
capDropcontains"ALL", start with an empty set; otherwise inherit the default OCI capabilities (Containerization.LinuxCapabilities.defaultOCICapabilities). - Process additions – If
capAddcontains"ALL", replace the set with all possible capabilities (CapabilityName.allCases). Otherwise validate and insert each specified capability. - Process drops – Remove every capability listed in
capDrop(except the"ALL"sentinel) from the current set.
private static func effectiveCapabilities(capAdd: [String],
capDrop: [String]) throws -> Containerization.LinuxCapabilities {
// 1. Base set
var caps: Set<CapabilityName>
if capDrop.contains("ALL") {
caps = []
} else {
caps = Set(Containerization.LinuxCapabilities.defaultOCICapabilities.effective)
}
// 2. Process adds
if capAdd.contains("ALL") {
caps = Set(CapabilityName.allCases)
} else {
for name in capAdd {
caps.insert(try CapabilityName(rawValue: name))
}
}
// 3. Process individual drops
for name in capDrop where name != "ALL" {
caps.remove(try CapabilityName(rawValue: name))
}
return Containerization.LinuxCapabilities(capabilities: Array(caps))
}
Because steps are processed sequentially, later operations override earlier ones. For example, --cap-drop ALL --cap-add SETGID results in a container possessing only CAP_SETGID.
Validation and Enforcement
Capability names undergo strict validation before reaching the kernel.
Name Validation
The parser validates every capability name against the CapabilityName enum from the Containerization Swift package. Invalid names like CHWOWZERS trigger immediate user-visible errors, which the test suite in Tests/CLITests/Subcommands/Run/TestCLIRunCapabilities.swift verifies.
Kernel Enforcement
After validation, the runtime passes the computed capability bitmask to the Linux VM. The kernel enforces these restrictions natively inside the container. The test suite confirms this by attempting to execute mknod after dropping CAP_MKNOD; the operation fails with a permission error, demonstrating that the capability drop was effective at the kernel level.
Practical Examples
Use these patterns to configure container capabilities for common scenarios:
Add a single capability to enable network raw sockets:
container run --cap-add NET_RAW alpine sh -c 'ping -c 1 8.8.8.8'
Drop a specific capability to prevent device node creation:
container run --cap-drop MKNOD alpine sh -c 'mknod /tmp/sda b 8 0'
Start from a clean slate and add only required capabilities:
container run \
--cap-drop ALL \
--cap-add SETGID \
--cap-add NET_RAW \
alpine sh
Grant all capabilities for debugging:
container run --cap-add ALL alpine sh
Combine drops and adds to create a minimal permission set:
container run \
--cap-drop ALL \
--cap-add NET_ADMIN \
--cap-add CHOWN \
alpine sh
Summary
- The
apple/containerrepository implements Linux capability management through--cap-addand--cap-dropCLI flags defined inFlags.swift. - The
effectiveCapabilitiesfunction inRuntimeService.swiftcalculates the final set using a three-step algorithm: initialize base, process adds, process drops. - Using
--cap-drop ALLfollowed by specific--cap-addentries creates a secure whitelist approach to container permissions. - All capability names are validated against the
CapabilityNameenum before enforcement, with invalid names producing immediate errors. - The kernel enforces the final capability set inside the container, as verified by the test suite in
TestCLIRunCapabilities.swift.
Frequently Asked Questions
What is the default capability set when running a container?
If you do not specify --cap-drop ALL, the container starts with the default OCI capability list (Containerization.LinuxCapabilities.defaultOCICapabilities). This provides a standard set of safe capabilities while removing dangerous privileges like CAP_SYS_ADMIN or CAP_NET_ADMIN.
How does the ordering of flags affect the final capability set?
The algorithm processes flags in a fixed sequence regardless of their CLI order: first it establishes the base set, then applies all additions, then applies all drops. This means --cap-drop ALL --cap-add NET_RAW results in only CAP_NET_RAW being present, because the drop clears the set before the add occurs.
Can I add all capabilities at once?
Yes. Pass --cap-add ALL to grant every possible Linux capability to the container. This is useful for debugging but should be avoided in production due to security implications. The implementation uses CapabilityName.allCases to expand the ALL sentinel into the complete capability set.
How do I verify that capabilities are actually dropped inside the container?
You can test enforcement by attempting an operation that requires the dropped capability. For example, after dropping CAP_MKNOD, running mknod /tmp/device b 8 0 will fail with "Operation not permitted". The repository's test suite uses this exact method in TestCLIRunCapabilities.swift to verify that the kernel correctly enforces the computed capability restrictions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →