How to Use Linux Capabilities (`--cap-add` / `--cap-drop`) for Enhanced Container Security
Apple Container provides --cap-add and --cap-drop CLI flags that let you grant or remove specific Linux kernel capabilities, allowing you to run containers with minimal privileges instead of full root access.
Linux capabilities are fine-grained permissions that replace the traditional all-or-nothing root privilege model. By leveraging the capability system implemented in the apple/container repository, you can significantly reduce your container's attack surface while retaining only the specific privileges your workload requires.
How Capability Flags Are Parsed
When you execute commands like container run --cap-add NET_ADMIN --cap-drop CHOWN, the CLI forwards these strings to the ContainerAPIService. The parsing logic resides in Sources/Services/ContainerAPIService/Client/Parser.swift.
The capabilities() method normalizes each capability name to upper-case and prepends the CAP_ prefix when missing. It validates entries against the CapabilityName enum, which mirrors the kernel's official list, and returns two arrays—capAdd and capDrop—containing the normalized capability strings.
This validation occurs in the parser's capabilities method (lines 1023-1055 in Parser.swift), ensuring that only valid kernel capabilities are passed to the runtime.
Default Capability Set
Newly created containers start with a restricted whitelist of capabilities rather than the full root set. According to the implementation documentation, the default set includes:
CAP_AUDIT_WRITECAP_CHOWNCAP_DAC_OVERRIDECAP_FOWNERCAP_FSETIDCAP_KILLCAP_MKNODCAP_NET_BIND_SERVICECAP_NET_RAWCAP_SETFCAPCAP_SETGIDCAP_SETPCAPCAP_SETUIDCAP_SYS_CHROOT
This whitelist is defined in the how-to guide section on controlling Linux capabilities, ensuring containers start with minimal privileges.
Order of Operations: Drops Before Adds
The runtime processes capability modifications in a specific sequence: drops execute first, then adds. This ordering creates predictable security boundaries.
Consider these scenarios:
--cap-drop ALL --cap-add ALLresults in all capabilities being granted because the add operation overwrites the drop.--cap-drop ALL --cap-add SETUID --cap-add SETGIDleaves onlyCAP_SETUIDandCAP_SETGIDactive, creating a least-privilege environment.
This processing order is documented in the how-to guide and enforced by the runtime implementation.
Practical Security Patterns
Use these patterns to harden your containers based on specific workload requirements:
Least-Privilege Containers
Use --cap-drop ALL followed by explicit --cap-add flags for only the capabilities your application needs. This removes every default capability and grants minimal privileges.
Granting Single Extra Privileges
Add specific capabilities to the default set with --cap-add NET_ADMIN or similar flags when the default whitelist is insufficient but full privileges are unnecessary.
Debugging and Development
Temporarily use --cap-drop ALL --cap-add ALL to grant full capabilities, then progressively restrict the set using --cap-drop flags to identify minimum required permissions.
Privileged Workloads
For network appliances or system-level tools, --cap-add ALL provides the complete kernel capability set equivalent to Docker's --privileged flag.
Code Examples
Run these commands to implement capability-based security:
# Start with default capability set
container run --rm alpine:latest uname -a
# Remove a specific capability from defaults
container run --rm --cap-drop CHOWN alpine:latest touch /tmp/file
# Grant network administration privileges
container run --rm --cap-add NET_ADMIN alpine:latest ip link set eth0 up
# Minimal privilege container with only SETUID/SETGID
container run --rm \
--cap-drop ALL \
--cap-add SETUID \
--cap-add SETGID \
alpine:latest id
# Full privileges (equivalent to --privileged)
container run --rm --cap-add ALL alpine:latest sh -c "cat /proc/1/status"
Programmatic Usage (Swift)
When building tools that interact with the Container API, construct capability arrays as follows:
let capAdd = ["NET_ADMIN", "SYS_TIME"]
let capDrop = ["MKNOD"]
let (normAdd, normDrop) = try Parser.capabilities(capAdd: capAdd, capDrop: capDrop)
// normAdd contains: ["CAP_NET_ADMIN", "CAP_SYS_TIME"]
// normDrop contains: ["CAP_MKNOD"]
The Parser.capabilities() method in Parser.swift handles normalization automatically, converting shorthand names like NET_ADMIN to the canonical CAP_NET_ADMIN format required by the kernel.
Key Implementation Files
The capability system spans these source files in the apple/container repository:
Sources/Services/ContainerAPIService/Client/Parser.swift– Implements thecapabilities()method that validates and normalizes capability names (lines 1023-1055).Sources/Services/ContainerAPIService/Client/Flags.swift– Declares the CLI flag definitions that map user input to the parser.docs/how-to.md– Contains the default capability whitelist and explains ordering semantics.docs/command-reference.md– Documents accepted flag values includingALL,CAP_NET_RAW, and un-prefixed variants likeNET_RAW.
Summary
- Apple Container implements Linux capabilities through
--cap-addand--cap-dropflags defined inFlags.swiftand processed byParser.swift. - The parser normalizes capability names (e.g.,
NET_ADMIN→CAP_NET_ADMIN) and validates them against theCapabilityNameenum. - Containers start with a restricted default set of 14 capabilities rather than full root privileges.
- The runtime applies drops before adds, allowing patterns like
--cap-drop ALL --cap-add SETUIDto create minimal privilege environments. - Accepted values include specific capability names (with or without
CAP_prefix) orALLfor the complete set.
Frequently Asked Questions
What happens if I specify both --cap-add ALL and --cap-drop ALL?
When both flags are present, the drop operation executes first, removing all capabilities, then the add operation restores them. Because the runtime processes drops before adds, --cap-drop ALL --cap-add ALL results in a container with full capabilities, while the reverse order (if explicitly forced) would differ. According to the how-to documentation, this ordering allows you to start from zero privileges and build up specific permissions.
Can I use capability names without the CAP_ prefix?
Yes. The capabilities() method in Parser.swift automatically prepends CAP_ to any capability name that lacks it. You can specify either NET_ADMIN or CAP_NET_ADMIN; both normalize to the canonical CAP_NET_ADMIN string validated against the CapabilityName enum.
Which capabilities should I drop for a production web server?
For a standard web server, start with --cap-drop ALL then add only CAP_NET_BIND_SERVICE (to bind ports below 1024) and CAP_SETUID/CAP_SETGID if the process drops privileges after binding. Remove CAP_NET_ADMIN, CAP_SYS_MODULE, and CAP_MKNOD to prevent network reconfiguration, kernel module loading, and device node creation if the container is compromised.
How do capabilities differ from running as non-root?
Running as non-root (via --user) restricts the UID/GID, but the process may still retain Linux capabilities that allow privilege escalation. Combining non-root execution with --cap-drop ALL ensures the process cannot perform privileged operations even if exploited. Capabilities provide fine-grained control over what root (or any user) can do, while UID restrictions control identity.
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 →