Guide to Configuring Linux Capabilities (cap-drop and cap-add) in Docker to Harden Security

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Context: Why You Need to Restrict Linux Capabilities

At exactly 2:00 AM, the SOC system triggered a red alert. A Node.js web service was compromised via an RCE vulnerability in a third-party npm package, allowing attackers to inject a webshell. Immediately, they attempted to scan internal networks, modify routing tables, and dump host packets.

Fortunately, the worst-case scenario—a container escape granting control over the host server—did not happen. The reason was simple: all kernel-level capabilities had been stripped from this container right at startup.

By default, Docker does not grant full host root privileges to containers. However, it still assigns 14 default Linux Capabilities (such as CAP_NET_RAW, CAP_CHOWN, CAP_MKNOD, CAP_SYS_CHROOT…).

If hackers gain root access inside a container, they will immediately leverage these redundant capabilities to sniff internal traffic, spoof ARP caches, or exploit unpatched kernel vulnerabilities to escalate privileges onto the host server.

The golden rule in production is the Principle of Least Privilege. Alongside fundamental hardening steps like blocking malicious images with Docker Content Trust, the most secure approach is to strip all capabilities using --cap-drop=ALL, and then grant back only the specific 1–2 capabilities the service actually needs via --cap-add.

Preparing Tools to Inspect Capabilities

To inspect exactly which capabilities a container holds, you need to install the libcap utilities on your Linux host.

Run the installation command on Ubuntu/Debian:

sudo apt-get update && sudo apt-get install -y libcap2-bin libcap-ng-utils

Now, let’s run a test Alpine container to inspect the default capabilities granted by Docker:

# Start a test Alpine container
docker run -d --name test-cap alpine sleep 3600

# Get the container process PID on the host
PID=$(docker inspect --format '{{.State.Pid}}' test-cap)

# View the capabilities held by the process
getpcaps $PID

# Clean up after inspection
docker rm -f test-cap

The output will include several capabilities like cap_chown, cap_net_raw, and cap_sys_chroot. A standard API microservice has no legitimate need for CAP_NET_RAW or CAP_MKNOD.

Practical Configuration: Combining cap-drop and cap-add

The best practice is a whitelist approach: Drop everything first, Add necessary capabilities later. You can also run Docker build checks to optimize Dockerfiles during development to eliminate potential security flaws early.

1. Configuring Directly via Docker CLI

Consider an Nginx container that needs to bind to ports 80/443 and manage file permissions during startup. You only need to grant 3 capabilities: NET_BIND_SERVICE, SETUID, and SETGID:

docker run -d \
  --name secure-web \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --cap-add=SETUID \
  --cap-add=SETGID \
  -p 80:80 \
  nginx:alpine

Once locked down, even if attackers gain a shell, they cannot run ping or scan networks with nmap because CAP_NET_RAW is missing. They also cannot create virtual block devices to access host storage due to the absence of CAP_MKNOD.

2. Standardizing Configuration with Docker Compose

When managing dozens of microservices, manually running CLI commands makes it easy to miss security flags. All configurations should be standardized directly in the docker-compose.yml file:

version: '3.8'

services:
  api-backend:
    image: node:20-alpine
    container_name: production-api
    working_dir: /app
    command: node server.js
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
    ports:
      - "80:80"
    restart: always

The no-new-privileges:true configuration is a critical safeguard. It prevents child processes from gaining elevated privileges through binaries with the setuid/setgid bit, such as sudo or suid. Similarly, ensure you protect sensitive build arguments by preventing leaked API keys in image layers.

Key Linux Capabilities Reference

  • CAP_NET_BIND_SERVICE: Allows binding to ports below 1024 (80, 443). Essential when running non-root web servers that listen on privileged ports.
  • CAP_NET_RAW: Used to generate raw packets or execute ICMP commands (ping). Should be DROPPED on all web/API containers to prevent port scanning and ARP spoofing.
  • CAP_SYS_ADMIN: Grants capabilities roughly equivalent to 90% of full root. Strictly avoid enabling this flag in production unless running Docker-in-Docker or kernel tracing.
  • CAP_CHOWN: Allows modifying file ownership (UID/GID). Grant only if the entrypoint script needs to chown data directories at startup.

Verifying and Monitoring Privileges Post-Deployment

After restricting capabilities, verify that the container runs reliably and that the unnecessary privileges have indeed been revoked. For continuous runtime health inspection, consider leveraging the Docker Events API for real-time container monitoring.

Inspecting Capabilities Directly from Within the Container

Read the process status via the /proc/1/status file:

docker exec -it secure-web sh -c "grep Cap /proc/1/status"

The system returns hex strings representing capability bitmaps:

CapInh: 0000000000000000
CapPrm: 00000000000004c0
CapEff: 00000000000004c0
CapBnd: 00000000000004c0

Decode these hex values on the host machine using the capsh command:

capsh --decode=00000000000004c0

The output displays: cap_setgid,cap_setuid,cap_net_bind_service. All superfluous capabilities have been completely eliminated.

Troubleshooting Permission Denied Errors

If your application crashes after applying --cap-drop=ALL, avoid blindly re-enabling all permissions. Check the container logs and kernel audit logs on the host:

# Check application logs
docker logs --tail 50 secure-web

# Find capabilities blocked by the kernel
sudo dmesg -T | grep -i "apparmor\|audit\|denied"

When you encounter an Operation not permitted error, identify the failing operation and add only the required capability to cap_add. Maintaining this discipline safeguards your entire Docker infrastructure against container escape attacks.

Share: