Skip to main content
RunBook Academy

← All labs in Docker & Containers

Lab · intermediate · ~20 min

Lab 19: Capabilities drop — minimum privilege for a service

B · Nested virtualisationC · Simulation

Objectives

  • Drop all capabilities
  • Confirm the container still works
  • Verify the capability set in /proc

Prerequisites

  • Lab 5: install Docker

Objective

Drop all Linux capabilities, then add back only what an nginx process needs. Confirm the container runs and serves traffic.

Tasks

Task 1: Without any capability changes

docker run -d --name nginx-default nginx:1.27
docker exec nginx-default cat /proc/1/status | grep ^Cap
# CapInh: 0000000000000000
# CapPrm: 00000000a80425fb  <- the default ~40 caps
# CapEff: 00000000a80425fb
# CapBnd: 00000000a80425fb

Decode the mask: capsh --decode=00000000a80425fb.

Task 2: Drop ALL

docker run -d --name nginx-no-caps --cap-drop=ALL nginx:1.27
docker exec nginx-no-caps cat /proc/1/status | grep ^Cap
# CapPrm: 0000000000000000
# CapEff: 0000000000000000

Nginx still listens on port 80 by default because the binary already had setcap applied by the image builder. So this works.

Task 3: Try to listen on a low port from a custom binary

cat > /srv/listen-80.c <<'EOF'
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
  int s = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in a = {.sin_family=AF_INET, .sin_port=htons(80), .sin_addr.s_addr=INADDR_ANY};
  bind(s, (struct sockaddr*)&a, sizeof(a));
  listen(s, 5);
  printf("listening on 80\n");
  pause();
}
EOF
gcc /srv/listen-80.c -o /srv/listen-80

docker run -d --name listen-80 -v /srv/listen-80:/listen-80 --cap-drop=ALL alpine /listen-80
docker logs listen-80
# bind: Operation not permitted

Task 4: Add CAP_NET_BIND_SERVICE back

docker run -d --name listen-80-ok --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
  -v /srv/listen-80:/listen-80 alpine /listen-80
docker logs listen-80-ok
# listening on 80

Step 5: Add read-only and no-new-privileges

docker run -d --name nginx-hardened \
  --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
  --read-only --tmpfs /var/cache/nginx --tmpfs /var/run \
  --security-opt no-new-privileges \
  nginx:1.27

docker exec nginx-hardened cat /proc/1/status | grep ^Cap
# CapPrm: 0000000000000400  <- NET_BIND_SERVICE only

Cleanup

docker rm -f nginx-default nginx-no-caps listen-80 listen-80-ok nginx-hardened
rm -f /srv/listen-80 /srv/listen-80.c

Verification status

Last reviewed
2026-08-09
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.