Docker 52 🐳 Container Security Hardening: Dropping Linux Capabilities (–cap-drop=ALL)
A container is a process, not a virtual machine. It runs directly on the host kernel, and the kernel does not distinguish between a process running in a container and a process running on the host. The only thing separating them is a set of kernel features: namespaces, cgroups, and capabilities. Capabilities are the most granular of the three. They split the traditional root privilege into roughly forty distinct permissions, each granting a specific kernel operation. A default Docker container starts with fourteen of them, and most applications need fewer than five. Dropping the unnecessary ones is the single highest-leverage hardening step available, and it costs nothing in performance.
Key point: Capabilities are not a permission check on files. They are a permission check on operations. Dropping a capability removes the ability to perform an operation entirely, regardless of the user ID running the process. A root process without CAP_NET_ADMIN cannot modify network interfaces, and the kernel does not care that it is root.
Why capabilities exist
The all-or-nothing root problem. Historically, UNIX had two privilege levels: root and everyone else. Root could do anything. A process that needed one privileged operation, such as binding to a low-numbered port, had to run as root and therefore had access to every privileged operation. A compromise of that process was a compromise of the entire system.
The principle of least privilege. Capabilities decompose root into individual permissions. A process can be granted only the capabilities it needs, so a compromise yields only the operations those capabilities permit. The web server that binds port 80 needs CAP_NET_BIND_SERVICE and nothing else. It does not need CAP_SYS_ADMIN to mount filesystems or CAP_SYS_PTRACE to inspect other processes.
The container default problem. Docker grants a default set of fourteen capabilities to every container. The set was chosen to make typical workloads work without configuration, not to minimize privilege. A Python web application inherits CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FOWNER, CAP_KILL, CAP_SETGID, CAP_SETUID, CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE, CAP_NET_RAW, CAP_SYS_CHROOT, CAP_MKNOD, CAP_AUDIT_WRITE, and CAP_SYS_PTRACE. Most of these are unnecessary for most applications .
The privilege escalation path. Each capability is a potential escalation vector. CAP_DAC_OVERRIDE bypasses file permission checks. CAP_SYS_ADMIN is effectively root inside the container. CAP_SYS_PTRACE allows inspecting and modifying other processes. A container that does not need these should not have them, because a compromised process inside the container can use any capability it holds.
The defense-in-depth principle. Container isolation depends on layers: namespaces, cgroups, seccomp profiles, AppArmor or SELinux, and capabilities. Each layer is independent. A bug in one layer does not automatically defeat the others. Dropping capabilities is one of the cheapest layers to add and one of the most effective against container escapes.
The compliance requirement. Security frameworks such as CIS Docker Benchmark and NIST SP 800-190 recommend dropping all capabilities and adding back only those required. Auditors increasingly look for this configuration as a baseline.
a. The capability model
Capabilities are per-thread attributes in the Linux kernel. A process may hold capabilities in five sets, but the two that matter for containers are the permitted set and the effective set.
The capability sets
The permitted set is the maximum set of capabilities a thread may have. The effective set is the subset currently in effect. A thread can drop capabilities from its effective set and later restore them from the permitted set, but it cannot add capabilities to the permitted set that were not granted at exec time.
# View capabilities of a running process
cat /proc/$$/status | grep Cap
# Decode the hex mask
capsh --decode=0000003fffffffff
The hex masks in /proc/<pid>/status list the capabilities in each set. capsh decodes them to names .
The default Docker set
Docker grants fourteen capabilities by default. The list is documented in the Docker run reference and has been stable for years.
| Capability | Grants |
|---|---|
| CAP_CHOWN | Change file ownership |
| CAP_DAC_OVERRIDE | Bypass file read/write/execute checks |
| CAP_FOWNER | Bypass ownership checks on files |
| CAP_FSETID | Set setuid/setgid bits |
| CAP_KILL | Send signals to any process |
| CAP_SETGID | Change group ID |
| CAP_SETUID | Change user ID |
| CAP_SETPCAP | Modify capability sets |
| CAP_NET_BIND_SERVICE | Bind ports below 1024 |
| CAP_NET_RAW | Use raw and packet sockets |
| CAP_SYS_CHROOT | Use chroot |
| CAP_MKNOD | Create special files |
| CAP_AUDIT_WRITE | Write to kernel audit log |
| CAP_SETFCAP | Set file capabilities |
The list is generous. A typical web application needs none of them except possibly CAP_NET_BIND_SERVICE.
Dropping capabilities
The --cap-drop flag removes capabilities from the container. The value can be a single capability or ALL to drop everything .
docker run --cap-drop=ALL nginx
After --cap-drop=ALL, the container has no capabilities. Any operation requiring a capability fails, even if the process runs as root.
Adding back specific capabilities
The --cap-add flag adds a capability back after --cap-drop. The order matters: drop first, then add.
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
The container has exactly one capability. It can bind port 80, and it can do nothing else privileged.
The all-or-nothing baseline
The recommended baseline is --cap-drop=ALL followed by the minimum set of --cap-add flags the application actually needs. Most applications need zero. Some need one or two. A container that needs more than a handful is a signal that either the application is doing something unusual or it should run without containers.
b. Determining what an application needs
The hard part is not dropping capabilities. It is knowing which ones the application actually requires. The approach is empirical: start with everything dropped, run the application, observe what fails, and add back only what is necessary.
Start with –cap-drop=ALL
docker run --cap-drop=ALL myapp:latest
If the application works, it needs no capabilities. Many applications do. A Go binary that binds a high port, reads a config file, and talks to a database over TCP needs nothing beyond what the kernel grants to any unprivileged user.
Observe the failures
If the application fails, the failure mode identifies the missing capability. A permission denied on a network bind points to CAP_NET_BIND_SERVICE. A failure to change file ownership points to CAP_CHOWN. The kernel audit log may record the denied operation.
# Check the audit log
dmesg | grep -i cap
The common capabilities
Most applications need zero. The small set that appears in practice:
| Capability | When Needed |
|---|---|
| CAP_NET_BIND_SERVICE | Binding ports below 1024 |
| CAP_CHOWN | Changing file ownership at runtime |
| CAP_SETUID / CAP_SETGID | Dropping privileges inside the container |
| CAP_SYS_CHROOT | Using chroot |
| CAP_DAC_OVERRIDE | Bypassing file permissions (rare, usually a sign of a workaround) |
| CAP_SYS_ADMIN | Mounting, namespaces, many kernel operations (avoid) |
CAP_SYS_ADMIN deserves special mention. It is the most powerful capability and the one most likely to enable a container escape. It is almost never needed by an application. If the application requires it, the design should be reconsidered.
The test procedure
The empirical procedure is:
- Run with
--cap-drop=ALL. - Exercise the application’s full functionality, not just startup.
- Record every failure.
- Add back the capability that resolves each failure.
- Repeat until the application works.
The resulting --cap-add list is the minimum set. It should be documented alongside the application and reviewed when the application changes.
The seccomp interaction
Capabilities and seccomp profiles are independent layers. A capability grants permission to perform an operation; a seccomp profile controls whether the syscall that performs the operation is allowed at all. A process with CAP_NET_BIND_SERVICE can bind port 80 only if the seccomp profile permits the bind syscall. Dropping capabilities and restricting syscalls together is stronger than either alone.
c. Other hardening flags alongside capabilities
Capabilities are the highest-leverage hardening step, but they are not the only one. Several other flags complement them.
The no-new-privileges flag
--security-opt=no-new-privileges prevents the process from gaining additional privileges through setuid binaries or file capabilities.
docker run --security-opt=no-new-privileges --cap-drop=ALL myapp
Without this flag, a setuid binary inside the container could escalate privileges, even if the container’s process starts unprivileged. With it, the escalation is blocked.
The read-only filesystem
--read-only mounts the container’s root filesystem as read-only. Writes to the filesystem fail unless a specific path is mounted as a writable volume.
docker run --read-only --tmpfs /tmp myapp
The combination of read-only root and a writable /tmp is the standard pattern for applications that need temporary storage but should not modify their own image.
The non-root user
Running the process as a non-root user inside the container limits what the process can do even if a capability is present. The USER directive in the Dockerfile sets the user.
FROM alpine
RUN adduser -D app
USER app
A non-root user cannot exercise capabilities that require root, and the kernel’s capability checks fail at the user ID check before the capability check.
The user namespace
--userns-remap maps the container’s root user to an unprivileged user on the host. A process that appears to be root inside the container is a high-numbered UID on the host, with no privileges.
docker run --userns=host myapp
The remapping is configured at the daemon level and applies to all containers when enabled.
The seccomp profile
Docker’s default seccomp profile blocks roughly forty-four syscalls that are not needed by most applications. A custom profile can block more.
docker run --security-opt seccomp=profile.json myapp
The profile is a JSON document listing the allowed syscalls and their arguments.
The AppArmor or SELinux profile
AppArmor and SELinux provide mandatory access control beyond capabilities. Docker’s default AppArmor profile restricts filesystem access, network access, and mount operations.
docker run --security-opt apparmor=docker-default myapp
The profile is applied automatically on systems where AppArmor is enabled.
The resource limits
--memory and --cpus limit the container’s resource consumption. They do not directly harden against exploits, but they prevent a compromised container from consuming all host resources.
docker run --memory=512m --cpus=1.0 myapp
The combination
The hardened baseline combines several flags:
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
--user 1000:1000 \
myapp:latest
Each flag addresses a different attack surface. The combination is what makes the container meaningfully harder to compromise than the default.
Complete Example Session
# ============================================
# PART 1: THE DEFAULT CONTAINER
# ============================================
docker run -d --name default-nginx nginx
# The container has 14 capabilities.
docker exec default-nginx sh -c 'cat /proc/1/status | grep Cap'
# CapInh: 0000000000000000
# CapPrm: 00000000a80425fb
# CapEff: 00000000a80425fb
# CapBnd: 00000000a80425fb
# Decode the mask:
capsh --decode=00000000a80425fb
# 0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,
# cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,
# cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,
# cap_audit_write,cap_setfcap
# ============================================
# PART 2: DROP ALL CAPABILITIES
# ============================================
docker run -d --name hardened-nginx \
--cap-drop=ALL \
nginx
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep CapEff'
# CapEff: 0000000000000000
# The container has no capabilities.
# The nginx process runs as root but cannot perform privileged operations.
# ============================================
# PART 3: ADD BACK ONE CAPABILITY
# ============================================
docker run -d --name nginx-port80 \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
nginx
# nginx binds port 80.
# The container has exactly one capability.
# ============================================
# PART 4: THE NO-NEW-PRIVILEGES FLAG
# ============================================
docker run -d --name no-priv-nginx \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
nginx
# The process cannot gain privileges from setuid binaries.
# Escalation inside the container is blocked.
# ============================================
# PART 5: THE READ-ONLY ROOT FILESYSTEM
# ============================================
docker run -d --name ro-nginx \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
--tmpfs /var/cache/nginx \
--tmpfs /var/run \
nginx
# The root filesystem is read-only.
# nginx writes only to /tmp, /var/cache/nginx, and /var/run.
# A compromise cannot persist by writing to the image.
# ============================================
# PART 6: THE NON-ROOT USER
# ============================================
# In the Dockerfile:
# FROM nginx
# RUN chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx
# USER nginx
docker run -d --name nonroot-nginx \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
my-nginx:nonroot
# The process runs as UID 101 (nginx).
# Even with CAP_NET_BIND_SERVICE, most privileged operations fail
# because the user ID check fails first.
# ============================================
# PART 7: DETERMINING REQUIRED CAPABILITIES
# ============================================
# Start with everything dropped.
docker run --cap-drop=ALL myapp:latest
# Observe the failure.
# If the application cannot bind port 80:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp:latest
# If the application cannot change file ownership:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE --cap-add=CHOWN myapp:latest
# Each --cap-add is a documented requirement.
# ============================================
# PART 8: THE AUDIT LOG
# ============================================
# Check the kernel audit log for capability denials.
dmesg | grep -i 'cap'
# audit: type=1400 audit(...): apparmor="DENIED" ...
# audit: type=1326 audit(...): auid=... cap_fowner=1 ...
# ============================================
# PART 9: DOCKER COMPOSE
# ============================================
# docker-compose.yml
services:
web:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
user: "1000:1000"
# ============================================
# PART 10: THE HARDENED BASELINE
# ============================================
docker run -d \
--name hardened \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
--user 1000:1000 \
--memory=512m \
--cpus=1.0 \
myapp:latest
# Capabilities dropped.
# Privilege escalation blocked.
# Filesystem read-only.
# Non-root user.
# Resource limits.
# Each flag addresses a different attack surface.
The ten parts progressed from the default container through dropping all capabilities, adding back the minimum, the no-new-privileges flag, the read-only filesystem, the non-root user, the empirical process for determining required capabilities, the audit log, Docker Compose configuration, and the hardened baseline. The same nginx image ran through every part, showing how each flag contributes to the overall posture.
Quick Reference
Docker Default Capabilities
| Capability | Grants |
|---|---|
| CAP_CHOWN | Change file ownership |
| CAP_DAC_OVERRIDE | Bypass file permission checks |
| CAP_FOWNER | Bypass ownership checks |
| CAP_FSETID | Set setuid/setgid bits |
| CAP_KILL | Signal any process |
| CAP_SETGID | Change GID |
| CAP_SETUID | Change UID |
| CAP_SETPCAP | Modify capability sets |
| CAP_NET_BIND_SERVICE | Bind ports below 1024 |
| CAP_NET_RAW | Raw sockets |
| CAP_SYS_CHROOT | Use chroot |
| CAP_MKNOD | Create special files |
| CAP_AUDIT_WRITE | Write audit log |
| CAP_SETFCAP | Set file capabilities |
Capabilities in Practice
| Capability | Typical Need |
|---|---|
| None | Most applications |
| CAP_NET_BIND_SERVICE | Ports below 1024 |
| CAP_CHOWN | Runtime ownership changes |
| CAP_SETUID / CAP_SETGID | Privilege dropping |
| CAP_SYS_CHROOT | chroot |
| CAP_SYS_ADMIN | Avoid; escape risk |
Hardening Flags
| Flag | Purpose |
|---|---|
--cap-drop=ALL | Remove all capabilities |
--cap-add=X | Add back one capability |
--security-opt=no-new-privileges | Block setuid escalation |
--read-only | Root filesystem read-only |
--tmpfs /tmp | Writable temp directory |
--user UID:GID | Run as non-root |
--memory | Memory limit |
--cpus | CPU limit |
Docker Compose Equivalent
| CLI Flag | Compose Key |
|---|---|
--cap-drop=ALL | cap_drop: [ALL] |
--cap-add=X | cap_add: [X] |
--security-opt=no-new-privileges | security_opt: [no-new-privileges:true] |
--read-only | read_only: true |
--tmpfs /tmp | tmpfs: [/tmp] |
--user 1000:1000 | user: "1000:1000" |
Best Practices
✅ Do This:
# Drop all capabilities and add back only what is needed.
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp # ✅
# Block privilege escalation.
docker run --security-opt=no-new-privileges myapp # ✅
# Run as a non-root user.
docker run --user 1000:1000 myapp # ✅
# Make the root filesystem read-only.
docker run --read-only --tmpfs /tmp myapp # ✅
# Test with everything dropped first.
docker run --cap-drop=ALL myapp # ✅
# Document which capabilities the application needs and why.
# Each --cap-add is a conscious decision. # ✅
# Use Docker Compose or Kubernetes manifests to encode the policy.
# The flags belong in version control. # ✅
❌ Don’t Do This:
# Don't run with default capabilities when none are needed.
docker run myapp # ❌ 14 unnecessary capabilities
# Don't add CAP_SYS_ADMIN without a specific reason.
docker run --cap-add=SYS_ADMIN myapp # ❌ Escape risk
# Don't skip --security-opt=no-new-privileges.
docker run --cap-drop=ALL myapp # ❌ Setuid escalation still possible
# Don't run as root inside the container.
# USER root in the Dockerfile. # ❌
# Don't leave the root filesystem writable unless needed.
# A compromised process can persist. # ❌
# Don't assume a capability is harmless.
# CAP_DAC_OVERRIDE bypasses file permissions. # ❌
# Don't forget to test the full application path.
# Startup may work with fewer capabilities than the main workload. # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Application fails to start | Missing capability | Add back the specific one |
| Port 80 bind fails | CAP_NET_BIND_SERVICE dropped | Add it back or use a port above 1024 |
| File write fails | Read-only root | Mount a tmpfs or volume |
| setuid fails | no-new-privileges | Remove the setuid dependency |
| Ownership change fails | CAP_CHOWN dropped | Add it back or avoid the operation |
| Container escape | CAP_SYS_ADMIN present | Remove it; redesign the application |
| Permission denied on sockets | CAP_NET_RAW dropped | Add it back or use a non-raw socket |
| Documentation missing | Capabilities added ad hoc | Record each one with a reason |
Real-World Examples
1. Drop All Capabilities
docker run --cap-drop=ALL nginx
2. Add Back NET_BIND_SERVICE
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
3. Block Escalation
docker run --security-opt=no-new-privileges myapp
4. Read-Only Root
docker run --read-only --tmpfs /tmp myapp
5. Non-Root User
docker run --user 1000:1000 myapp
6. Docker Compose
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]
security_opt: [no-new-privileges:true]
read_only: true
7. Check Capabilities
docker exec myapp cat /proc/1/status | grep CapEff
8. Decode Capability Mask
capsh --decode=00000000a80425fb
9. Audit Log
dmesg | grep -i cap
10. The Hardened Baseline
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges --read-only --tmpfs /tmp \
--user 1000:1000 myapp
Visual
Capability Model
┌────────────────────────────────────────────────────────────┐
│ CAPABILITIES DECOMPOSE ROOT │
│ │
│ TRADITIONAL UNIX │
│ ┌──────────────────────────────────────────────────┐ │
│ │ root │ │
│ │ All-or-nothing. │ │
│ │ Can do everything or nothing. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ CAPABILITIES │
│ ┌──────────────────────────────────────────────────┐ │
│ │ root │ │
│ │ ├── CAP_NET_BIND_SERVICE (bind port 80) │ │
│ │ ├── CAP_CHOWN (change ownership) │ │
│ │ ├── CAP_SYS_ADMIN (mount, namespaces) │ │
│ │ ├── CAP_NET_RAW (raw sockets) │ │
│ │ ├── CAP_SYS_PTRACE (inspect processes) │ │
│ │ └── ... roughly 40 total │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ A process can hold some capabilities and not others. │
│ Dropping the unneeded ones limits what a compromise │
│ can do. │
│ │
└────────────────────────────────────────────────────────────┘
Docker Defaults vs Hardened
┌────────────────────────────────────────────────────────────┐
│ DEFAULT vs HARDENED CONTAINER │
│ │
│ DEFAULT │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 14 capabilities │ │
│ │ Root user │ │
│ │ Writable filesystem │ │
│ │ setuid escalation allowed │ │
│ │ │ │
│ │ A compromise can: │ │
│ │ - Bypass file permissions │ │
│ │ - Change ownership │ │
│ │ - Signal other processes │ │
│ │ - Use raw sockets │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ HARDENED │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1 capability (NET_BIND_SERVICE) │ │
│ │ Non-root user (UID 1000) │ │
│ │ Read-only root, tmpfs for /tmp │ │
│ │ no-new-privileges │ │
│ │ │ │
│ │ A compromise can: │ │
│ │ - Bind port 80 │ │
│ │ - Nothing else privileged │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Same application. Same performance. │
│ Different blast radius. │
│ │
└────────────────────────────────────────────────────────────┘
The Layered Hardening
┌────────────────────────────────────────────────────────────┐
│ LAYERED CONTAINER HARDENING │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ NAMESPACES │ │
│ │ PID, network, mount, user, UTS, IPC │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CGROUPS │ │
│ │ Memory, CPU, I/O limits │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CAPABILITIES │ │
│ │ Per-operation permissions │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ SECCOMP │ │
│ │ Allowed syscalls │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ APPARMOR / SELINUX │ │
│ │ Mandatory access control │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ FILESYSTEM │ │
│ │ Read-only root, tmpfs, volumes │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Each layer is independent. A bug in one does not │
│ automatically defeat the others. │
│ │
└────────────────────────────────────────────────────────────┘
The Empirical Process
┌────────────────────────────────────────────────────────────┐
│ FINDING THE MINIMUM CAPABILITY SET │
│ │
│ STEP 1 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker run --cap-drop=ALL myapp │ │
│ │ Result: fails to bind port 80. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ STEP 2 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker run --cap-drop=ALL \ │ │
│ │ --cap-add=NET_BIND_SERVICE myapp │ │
│ │ Result: starts, but fails to write to /var/log. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ STEP 3 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ docker run --cap-drop=ALL \ │ │
│ │ --cap-add=NET_BIND_SERVICE \ │ │
│ │ --cap-add=CHOWN myapp │ │
│ │ Result: works fully. │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ MINIMUM SET: NET_BIND_SERVICE, CHOWN │
│ Document both. Review when the application changes. │
│ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Capability | A single privileged kernel operation |
--cap-drop=ALL | Remove all capabilities |
--cap-add=X | Add back one capability |
| Default Docker set | 14 capabilities |
| Typical need | Zero to two capabilities |
--security-opt=no-new-privileges | Block setuid escalation |
--read-only | Read-only root filesystem |
--tmpfs | Writable temporary directory |
--user | Run as non-root |
CAP_SYS_ADMIN | Most powerful; avoid |
capsh --decode | Decode capability hex masks |
Key takeaways:
- Capabilities decompose root into individual permissions. A process can hold some and not others. Dropping the unneeded ones limits what a compromise can do without affecting what the application can do.
- Docker grants fourteen capabilities by default. The set was chosen for compatibility, not security. Most applications need none of them.
--cap-drop=ALLis the baseline. Start with everything dropped, then add back only what the application provably needs. The empirical process is: run, observe failures, add the specific capability, repeat.CAP_NET_BIND_SERVICEis the most common requirement. Applications that bind ports below 1024 need it. Applications that bind higher ports do not.CAP_SYS_ADMINis the escape risk. It is the most powerful capability and almost never needed by an application. If it is required, the design should be reconsidered.--security-opt=no-new-privilegesblocks setuid escalation. Without it, a setuid binary inside the container can gain privileges even when the container started unprivileged.- Running as a non-root user is complementary. A non-root user cannot exercise capabilities that require root, and the kernel’s user ID check fails before the capability check.
- The read-only root filesystem prevents persistence. A compromised process cannot modify the image or write a backdoor into a binary. Writable paths are mounted explicitly as tmpfs or volumes.
- Layer the hardening. Capabilities, seccomp, AppArmor, namespaces, cgroups, and the filesystem all address different attack surfaces. Each is independent, and the combination is stronger than any single layer.
- Encode the policy in version control. The flags belong in the Docker Compose file, the Kubernetes manifest, or the deployment pipeline, not in an operator’s memory.
Remember: Container hardening is a matter of reducing what a compromised process can do. The highest-leverage step is dropping Linux capabilities. Docker’s defaults grant fourteen permissions that most applications never use. Dropping all of them and adding back only what the application provably requires reduces the container’s privileged surface to its actual needs. Combine that with --security-opt=no-new-privileges, a non-root user, a read-only filesystem, and resource limits, and the container becomes meaningfully harder to compromise. None of these flags costs performance. The empirical process is to start with everything dropped, run the application, observe what fails, and add back the specific capability that resolves each failure. The result is a documented minimum set that is reviewed when the application changes. The container runs the same workload with the same performance, but a compromise yields far less.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!