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_WRITE
  • CAP_CHOWN
  • CAP_DAC_OVERRIDE
  • CAP_FOWNER
  • CAP_FSETID
  • CAP_KILL
  • CAP_MKNOD
  • CAP_NET_BIND_SERVICE
  • CAP_NET_RAW
  • CAP_SETFCAP
  • CAP_SETGID
  • CAP_SETPCAP
  • CAP_SETUID
  • CAP_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 ALL results in all capabilities being granted because the add operation overwrites the drop.
  • --cap-drop ALL --cap-add SETUID --cap-add SETGID leaves only CAP_SETUID and CAP_SETGID active, 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:

Summary

  • Apple Container implements Linux capabilities through --cap-add and --cap-drop flags defined in Flags.swift and processed by Parser.swift.
  • The parser normalizes capability names (e.g., NET_ADMINCAP_NET_ADMIN) and validates them against the CapabilityName enum.
  • 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 SETUID to create minimal privilege environments.
  • Accepted values include specific capability names (with or without CAP_ prefix) or ALL for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →