This is a runtime-hardening lab, not an escape. Target is a Docker container I start with the default seccomp profile, then again with --privileged so I can see the extra surface and immediately destroy that container. Goal: capsh --print, docker inspect for Seccomp/AppArmor/Privileged, and a failed mount as evidence the default profile holds. I do not trigger CVE-2022-0185, I do not spray a heap, I do not nsenter into host PID 1.
1Figure 1. Namespaces are views. The kernel is shared. Caps, seccomp, LSM are the brakes.
2workload -> seccomp (mode 2) -> shared kernel
3no cap_sys_admin; mount/unshare => EPERM
Lab layout
1labs/ctr_lab/
2 run.sh # docker run --rm -it debian:bookworm-slim
3 inspect.json # redacted
4 capsh-default.txt
5 capsh-priv.txt
1# run.sh — default, then inspect
2docker run --rm --name labctr -d debian:bookworm-slim sleep 3600
3docker exec labctr bash -c 'apt-get -qq update && apt-get -qq install -y libcap2-bin >/dev/null'
Host: unprivileged user in group docker on a lab VM. I treat docker group as root-equivalent and do not pretend otherwise.
Artifact: capsh --print (default container)
1$ docker exec labctr capsh --print
2Current: 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
3Bounding set: 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
4Ambient set:
5Securebits: 00/0x0/1'b0
6Securebits: noroot no_noroot nosuid-lock no_nosuid-lock
7uid=0(root) euid=0(root)
8gid=0(root)
9groups=0(root)
UID 0 inside the user namespace / container, but the bounding set is not full. Missing (among others): cap_sys_admin, cap_sys_module, cap_sys_ptrace, cap_dac_read_search, cap_net_admin, cap_sys_rawio. Those absences are the lab.
--privileged container I started for ten seconds:
1$ docker run --rm --privileged --name labpriv debian:bookworm-slim capsh --print
2Current: =ep
3Bounding set =ep
4# =ep means full cap set, effective+permitted
5uid=0(root)
=ep is “this is a VM with extra steps”. I docker rm -f labpriv immediately. The dump exists so a review comment can say “do not pass --privileged”.
Artifact: docker inspect seccomp / AppArmor / mounts
1$ docker inspect labctr --format \
2 'Privileged={{.HostConfig.Privileged}}
3CapAdd={{.HostConfig.CapAdd}}
4CapDrop={{.HostConfig.CapDrop}}
5SecurityOpt={{.HostConfig.SecurityOpt}}
6ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}}
7PidMode={{.HostConfig.PidMode}}
8NetworkMode={{.HostConfig.NetworkMode}}
9Seccomp={{.HostConfig.SecurityOpt}}'
10Privileged=false
11CapAdd=[]
12CapDrop=[]
13SecurityOpt=[]
14ReadonlyRootfs=false
15PidMode=
16NetworkMode=bridge
17Seccomp=[]
Empty SecurityOpt on this Docker still applies the default seccomp profile. Confirm:
1$ docker inspect labctr --format '{{.AppArmorProfile}} {{.HostConfig.Privileged}}'
2docker-default false
3
4$ cat /sys/firmware/acpi 2>/dev/null
5# from inside:
6$ docker exec labctr cat /proc/self/status | grep -i seccomp
7Seccomp: 2
8Seccomp_filters: 1
9# 2 = SECCOMP_MODE_FILTER
Seccomp: 2 is the filter. 0 would be off (finding). 1 strict (rare).
Default profile blocks a pile of syscalls including mount, reboot, unshare in some versions, bpf, kexec_*. I do not list the full JSON; I test one call.
Sanitized reproduction (failed syscall / crash only)
1$ docker exec labctr mount -t tmpfs tmpfs /mnt
2mount: /mnt: permission denied.
3# dmesg on host (sometimes):
4# audit: type=1326 ... syscall=165 exit=-1 a0=... comm="mount"
5# 165 = mount on this arch; seccomp or missing CAP_SYS_ADMIN
6
7$ docker exec labctr capsh --print | grep sys_admin
8# no output — cap missing, even if seccomp were off this mount should fail
Unshare of user+mount (often the first line of escape write-ups) — I run it to watch it fail:
1$ docker exec labctr unshare -Um --mount-proc true
2unshare: unshare failed: Operation not permitted
3# or: Invalid argument depending on Debian/kernel/seccomp
I do not add --security-opt seccomp=unconfined to make it work.
Privileged container mount (the anti-pattern, then destroy):
1$ docker run --rm --privileged debian:bookworm-slim mount -t tmpfs tmpfs /mnt
2# succeeds — that is why privileged is banned
ASAN analog: a tiny C file that calls unshare and is compiled with ASAN in userland, not as an exploit:
1/* unshare_lab.c — expect EPERM */
2#define _GNU_SOURCE
3#include <sched.h>
4#include <stdio.h>
5#include <errno.h>
6int main(void) {
7 if (unshare(CLONE_NEWUSER | CLONE_NEWNS) != 0)
8 perror("unshare");
9 return 0;
10}
1$ docker exec labctr gcc -fsanitize=address -o /tmp/u unshare_lab.c
2$ docker exec labctr /tmp/u
3unshare: Operation not permitted
4# ASAN silent — no memory bug, just EPERM. Good.
If someone runs this on the host as an unprivileged user with kernel.unprivileged_userns_clone=0:
1$ sysctl kernel.unprivileged_userns_clone
2kernel.unprivileged_userns_clone = 0
3$ ./u
4unshare: Operation not permitted
CVE-2022-0185-era notes: a kernel bug in filesystem context parameter size handling, reachable via a syscall that containers might still have. Theme, not a trigger: seccomp reduce the syscall set; still patch the kernel. I do not include the size value that hit the bug.
Analysis steps
Privileged=true? Stop. No other finding matters.capsh --printbounding set: issys_admin/sys_module/sys_ptrace/dac_read_searchpresent?Seccomp:in/proc/self/statusmust be 2. Inspect custom profiles ifSecurityOptnames one.- AppArmor/SELinux profile not empty/unconfined.
- Host kernel:
uname -rvs CVE list. Shared kernel is the invariant. - User namespace:
--userns-remap/ rootless. UID 0 in the container mapped to a high host UID.
1$ docker exec labctr cat /proc/self/uid_map
2 0 0 4[REDACTED]
3# default: container 0 == host 0 ← finding for a hardened runtime
Rootless / remap would show a non-zero host UID in column 2. I file “container root is host root” even when caps are dropped.
/proc and sysctls that should stay read-only
Even with caps dropped, a writable /proc/sys is a class of bug. Default Docker does not mount host /proc/sys writable into the container. I check:
1$ docker exec labctr ls -ld /proc/sys
2dr-xr-xr-x ... /proc/sys
3$ docker exec labctr sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
4sh: /proc/sys/net/ipv4/ip_forward: Read-only file system
Read-only is the artifact. --sysctl net.ipv4.ip_forward=1 on docker run is an explicit host sysctl from the daemon, not from the workload. I treat workload writes to sysctl as a failed escape attempt and keep the EPERM/EROFS.
/proc/kcore and /dev/mem should be absent:
1$ docker exec labctr ls /dev/mem /proc/kcore
2ls: cannot access '/dev/mem': No such file or directory
Presence under --privileged is expected and is another reason that flag is banned.
Mitigation
1docker run --rm \
2 --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
3 --security-opt no-new-privileges \
4 --read-only \
5 --tmpfs /tmp \
6 debian:bookworm-slim capsh --print
1Current: cap_net_bind_service
2Bounding set: cap_net_bind_service
- Never
--privilegedin compose for apps. - Drop ALL, add back the minimum.
- Keep default seccomp; do not
seccomp=unconfinedto “make a binary work” without a named syscall. - Patch host kernel; 2022-0185 is one of many.
kernel.unprivileged_userns_clone=0on hosts that do not need it (breaks some rootless; document).- Kubernetes: drop
securityContext.privileged, setallowPrivilegeEscalation: false, seccompRuntimeDefault, read-only root FS.
Failed-auth analog at the API:
1$ docker -H tcp://127.0.0.1:2375 ps
2error during connect: ... # we do not expose 2375
3# if someone did:
4# GET /containers/json without TLS → treat as host root
What I file after this lab
- Default ctr:
Seccomp: 2, AppArmordocker-default, Privileged false, nocap_sys_admin mount→ permission denied;unshare -Um→ EPERM--privileged:Current: =ep,mountsucceeds — banneduid_mapcolumn 2 is 0 (container root = host root) — finding- Hardened run:
--cap-drop=ALL,no-new-privileges, read-only, onlycap_net_bind_service - Out of scope: CVE-2022-0185 trigger, host PID 1 nsenter, /proc/sys write gadgets
Commands appendix
1docker exec labctr capsh --print
2docker exec labctr cat /proc/self/status | grep -i seccomp
3docker inspect labctr --format '{{.HostConfig.Privileged}} {{.AppArmorProfile}}'
4docker exec labctr mount -t tmpfs tmpfs /mnt
5docker exec labctr cat /proc/self/uid_map