| |

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.

CapabilityGrants
CAP_CHOWNChange file ownership
CAP_DAC_OVERRIDEBypass file read/write/execute checks
CAP_FOWNERBypass ownership checks on files
CAP_FSETIDSet setuid/setgid bits
CAP_KILLSend signals to any process
CAP_SETGIDChange group ID
CAP_SETUIDChange user ID
CAP_SETPCAPModify capability sets
CAP_NET_BIND_SERVICEBind ports below 1024
CAP_NET_RAWUse raw and packet sockets
CAP_SYS_CHROOTUse chroot
CAP_MKNODCreate special files
CAP_AUDIT_WRITEWrite to kernel audit log
CAP_SETFCAPSet 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:

CapabilityWhen Needed
CAP_NET_BIND_SERVICEBinding ports below 1024
CAP_CHOWNChanging file ownership at runtime
CAP_SETUID / CAP_SETGIDDropping privileges inside the container
CAP_SYS_CHROOTUsing chroot
CAP_DAC_OVERRIDEBypassing file permissions (rare, usually a sign of a workaround)
CAP_SYS_ADMINMounting, 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:

  1. Run with --cap-drop=ALL.
  2. Exercise the application’s full functionality, not just startup.
  3. Record every failure.
  4. Add back the capability that resolves each failure.
  5. 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

CapabilityGrants
CAP_CHOWNChange file ownership
CAP_DAC_OVERRIDEBypass file permission checks
CAP_FOWNERBypass ownership checks
CAP_FSETIDSet setuid/setgid bits
CAP_KILLSignal any process
CAP_SETGIDChange GID
CAP_SETUIDChange UID
CAP_SETPCAPModify capability sets
CAP_NET_BIND_SERVICEBind ports below 1024
CAP_NET_RAWRaw sockets
CAP_SYS_CHROOTUse chroot
CAP_MKNODCreate special files
CAP_AUDIT_WRITEWrite audit log
CAP_SETFCAPSet file capabilities

Capabilities in Practice

CapabilityTypical Need
NoneMost applications
CAP_NET_BIND_SERVICEPorts below 1024
CAP_CHOWNRuntime ownership changes
CAP_SETUID / CAP_SETGIDPrivilege dropping
CAP_SYS_CHROOTchroot
CAP_SYS_ADMINAvoid; escape risk

Hardening Flags

FlagPurpose
--cap-drop=ALLRemove all capabilities
--cap-add=XAdd back one capability
--security-opt=no-new-privilegesBlock setuid escalation
--read-onlyRoot filesystem read-only
--tmpfs /tmpWritable temp directory
--user UID:GIDRun as non-root
--memoryMemory limit
--cpusCPU limit

Docker Compose Equivalent

CLI FlagCompose Key
--cap-drop=ALLcap_drop: [ALL]
--cap-add=Xcap_add: [X]
--security-opt=no-new-privilegessecurity_opt: [no-new-privileges:true]
--read-onlyread_only: true
--tmpfs /tmptmpfs: [/tmp]
--user 1000:1000user: "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

PitfallWhy It HappensFix
Application fails to startMissing capabilityAdd back the specific one
Port 80 bind failsCAP_NET_BIND_SERVICE droppedAdd it back or use a port above 1024
File write failsRead-only rootMount a tmpfs or volume
setuid failsno-new-privilegesRemove the setuid dependency
Ownership change failsCAP_CHOWN droppedAdd it back or avoid the operation
Container escapeCAP_SYS_ADMIN presentRemove it; redesign the application
Permission denied on socketsCAP_NET_RAW droppedAdd it back or use a non-raw socket
Documentation missingCapabilities added ad hocRecord 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

ItemValue
CapabilityA single privileged kernel operation
--cap-drop=ALLRemove all capabilities
--cap-add=XAdd back one capability
Default Docker set14 capabilities
Typical needZero to two capabilities
--security-opt=no-new-privilegesBlock setuid escalation
--read-onlyRead-only root filesystem
--tmpfsWritable temporary directory
--userRun as non-root
CAP_SYS_ADMINMost powerful; avoid
capsh --decodeDecode 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=ALL is 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_SERVICE is the most common requirement. Applications that bind ports below 1024 need it. Applications that bind higher ports do not.
  • CAP_SYS_ADMIN is 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-privileges blocks 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!