Lab 06 โ Linux Capabilities: Dropping Root, Principle of Least Privilege¶
| Course | SCIA-360 OS Security |
| Topic | Access Control Models |
| Chapter | 6 |
| Difficulty | โญโญ Intermediate |
| Estimated Time | 45โ60 minutes |
| Prerequisites | Lab 05 completed; Docker installed and running |
Overview¶
Traditionally, Linux had a binary privilege model: either a process runs as root (UID 0) and can do anything, or it runs as an unprivileged user and can do almost nothing system-level. Linux capabilities break that monolith into fine-grained privilege tokens โ a process can hold exactly the capabilities it needs and nothing more.
In this lab you will:
- Inspect the full capability set granted to a default Docker container
- Decode raw capability bitmasks from
/proc/self/status - Drop all capabilities and observe which operations fail
- Add back only
CAP_NET_BIND_SERVICEand verify least-privilege behaviour - Map real-world services to their minimum required capabilities
Grading Rubric
| Component | Points |
|---|---|
| Screenshots (06a โ 06g) | 40 pts |
| Service-to-capability table (completed and submitted) | 20 pts |
| Reflection questions (4 ร 10 pts) | 40 pts |
| Total | 100 pts |
Part 1 โ Understanding Capabilities¶
Step 1.1 โ Full Capability Set (Default Container)¶
docker run --rm ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
capsh --print"
Expected output: Three sets are displayed:
- Current (effective): capabilities the process can currently exercise
- Bounding: the ceiling โ capabilities can never be added above this set
- Ambient: inherited automatically by child processes without
setuid
Look for names like cap_chown, cap_net_bind_service, cap_kill, cap_setuid, cap_setgid in the Current set.
๐ธ Screenshot checkpoint 06a: Full capsh --print output showing all three capability sets.
Step 1.2 โ Decode the Raw Capability Bitmask¶
docker run --rm ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
HEX=\$(cat /proc/self/status | grep CapEff | awk '{print \$2}')
echo \"CapEff hex: \$HEX\"
capsh --decode=\$HEX"
Expected output: A hex value like 00000000a80425fb followed by a comma-separated list of named capabilities. The kernel stores capability sets as 64-bit bitmasks โ each bit represents one capability. capsh --decode translates the bitmask to human-readable names.
๐ธ Screenshot checkpoint 06b: The CapEff hex: line and the decoded capability names beneath it.
Step 1.3 โ Per-Binary File Capabilities¶
docker run --rm ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
getcap /usr/bin/ping 2>/dev/null || echo 'ping: no file capabilities'
ls -la /usr/bin/ping"
Note: In Ubuntu 22.04 containers ping may use cap_net_raw as a file capability, or it may rely on setuid permissions. Either approach avoids requiring a full root shell just to send ICMP packets โ a classic capability use case.
๐ธ Screenshot checkpoint 06c: Output of getcap /usr/bin/ping and ls -la /usr/bin/ping.
Part 2 โ Dropping All Capabilities¶
Step 2.1 โ Default vs. --cap-drop ALL¶
Run both comparisons in the same terminal so the contrast is visible:
echo '=== Default capabilities ==='
docker run --rm ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
capsh --print | grep '^Current:'"
echo ''
echo '=== After --cap-drop ALL ==='
docker run --rm --cap-drop ALL ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
capsh --print | grep '^Current:'"
Expected output:
=== Default capabilities ===
Current: = cap_chown,cap_dac_override,...
=== After --cap-drop ALL ===
Current: =
Current: = (with nothing after the equals sign) means the process holds zero capabilities โ it cannot perform any privileged kernel operation.
๐ธ Screenshot checkpoint 06c: Both Current: lines visible in the same terminal window, demonstrating the empty set after --cap-drop ALL.
Package installation still works
Even with --cap-drop ALL, apt-get succeeds here because installing packages does not require kernel capabilities โ it only writes files and runs scripts as root. Capabilities govern kernel-level privilege, not filesystem write access controlled by UID 0.
Step 2.2 โ CAP_CHOWN Removed: chown Fails¶
docker run --rm --cap-drop CHOWN ubuntu:22.04 bash -c "
touch /tmp/test.txt
chown daemon:daemon /tmp/test.txt 2>&1 || echo 'chown FAILED: no cap_chown'"
Expected output: chown: changing ownership of '/tmp/test.txt': Operation not permitted
Even though the process runs as root (UID 0), without CAP_CHOWN the kernel rejects the chown() system call.
Step 2.3 โ CAP_CHOWN Present: chown Succeeds¶
docker run --rm ubuntu:22.04 bash -c "
touch /tmp/test.txt
chown daemon:daemon /tmp/test.txt && ls -la /tmp/test.txt"
Expected output: -rw-r--r-- 1 daemon daemon โ the ownership change succeeded.
๐ธ Screenshot checkpoint 06d: Steps 2.2 and 2.3 results in the same terminal: chown failure without CAP_CHOWN and chown success with it.
Part 3 โ Adding Only the Capabilities You Need¶
Step 3.1 โ Single-Capability Container¶
docker run --rm \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
echo 'Capabilities (only NET_BIND_SERVICE):'
capsh --print | grep '^Current:'"
Expected output:
One capability. Nothing else. This is the principle of least privilege in machine-readable form.
๐ธ Screenshot checkpoint 06e: Current: = cap_net_bind_service clearly visible.
Step 3.2 โ Port Binding Allowed, File Ownership Still Blocked¶
docker run --rm \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq netcat-openbsd 2>/dev/null
nc -l -p 1024 &
sleep 0.5 && kill %1 2>/dev/null
echo 'Port 1024: allowed with NET_BIND_SERVICE'
nc -l -p 80 &
sleep 0.5 && kill %1 2>/dev/null
echo 'Port 80: also allowed with NET_BIND_SERVICE'
touch /tmp/t && chown daemon:daemon /tmp/t 2>&1 || echo 'chown: still blocked (no CAP_CHOWN)'"
Expected output:
Port 1024: allowed with NET_BIND_SERVICE
Port 80: also allowed with NET_BIND_SERVICE
chown: still blocked (no CAP_CHOWN)
CAP_NET_BIND_SERVICE grants the right to bind to any port (including privileged ports < 1024) but grants nothing else.
๐ธ Screenshot checkpoint 06f: All three output lines visible โ two port-binding successes and the chown failure.
Part 4 โ Capabilities vs. Root: The Key Insight¶
Step 4.1 โ What Docker Drops From Full Root by Default¶
docker run --rm ubuntu:22.04 bash -c "
apt-get update -qq && apt-get install -y -qq libcap2-bin 2>/dev/null
echo '=== Dropped from full root in Docker default: ==='
echo 'CAP_SYS_ADMIN - mount, ioctl, kernel params (most dangerous)'
echo 'CAP_SYS_BOOT - reboot system'
echo 'CAP_NET_ADMIN - iptables, change network interfaces'
echo 'CAP_SYS_MODULE - load/unload kernel modules'
echo ''
echo '=== Present in Docker default container: ==='
capsh --print | grep '^Current:'"
๐ธ Screenshot checkpoint 06g: The dropped capabilities list and the Current: line of Docker's default set โ both visible together.
Step 4.2 โ Service-to-Capability Reference Table¶
Study the table below. You will reference it in your reflection and may be asked to extend it on a quiz.
| Service | Minimum Required Capability | Why It Is Needed |
|---|---|---|
| nginx (HTTPS on port 443) | CAP_NET_BIND_SERVICE | Bind to privileged port < 1024 |
| syslog daemon | CAP_SYS_ADMIN (or none with modern API) | Read kernel ring buffer log |
| cron daemon | CAP_SETUID, CAP_SETGID | Switch effective UID/GID before running user jobs |
| NTP client (chronyd) | CAP_SYS_TIME | Adjust the hardware and system clock |
| Container runtime (runc) | CAP_SYS_ADMIN + many | Create namespaces, mount filesystems, pivot_root |
Lab deliverable
Submit this table with your screenshots. On the assessment you may be asked to add two additional services and justify their capability requirements.
Cleanup¶
Assessment¶
Screenshot Checklist¶
| ID | Required Content |
|---|---|
| 06a | capsh --print showing full Current, Bounding, and Ambient sets |
| 06b | CapEff hex: value and decoded capability names |
| 06c | Current: = (empty) after --cap-drop ALL alongside default Current: |
| 06d | chown failing without CAP_CHOWN and succeeding with it |
| 06e | Current: = cap_net_bind_service โ single capability container |
| 06f | Port 1024 and port 80 allowed; chown still blocked |
| 06g | Dropped capabilities list and Docker default Current: line |
Reflection Questions¶
Submission requirement
Answer each question in complete paragraphs (minimum 4โ6 sentences each). Bullet-point-only answers will not receive full credit.
Q1. Before Linux capabilities, programs like ping and web servers needed to run as full root (UID 0) to perform privileged operations such as binding to port 80 or sending raw packets. Explain how Linux capabilities improve security compared to that binary root/non-root model. What specific risk does each individual capability grant, and why does granularity matter?
Q2. CAP_SYS_ADMIN is sometimes called the root of capabilities โ it grants so many kernel privileges that possessing it is nearly equivalent to having full root. Why does Docker drop CAP_SYS_ADMIN from containers by default? Describe at least two concrete attacks an adversary could execute inside a container if CAP_SYS_ADMIN were available.
Q3. In Step 3.2, a container holding only CAP_NET_BIND_SERVICE could listen on port 80 but could not change file ownership. How does this outcome demonstrate the principle of least privilege? Describe a realistic production web-server deployment that would benefit from this capability isolation โ what would be in the container, what capabilities would it hold, and what would be explicitly dropped?
Q4. An attacker exploits a buffer overflow in an nginx process running in a container launched with --cap-drop ALL --cap-add NET_BIND_SERVICE. Using your knowledge from this lab, list and explain three specific privileged actions the attacker cannot perform โ actions that would be available to them if the container were running with the default Docker capability set or full root.