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

  1. Privileged=true? Stop. No other finding matters.
  2. capsh --print bounding set: is sys_admin / sys_module / sys_ptrace / dac_read_search present?
  3. Seccomp: in /proc/self/status must be 2. Inspect custom profiles if SecurityOpt names one.
  4. AppArmor/SELinux profile not empty/unconfined.
  5. Host kernel: uname -r vs CVE list. Shared kernel is the invariant.
  6. 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 --privileged in compose for apps.
  • Drop ALL, add back the minimum.
  • Keep default seccomp; do not seccomp=unconfined to “make a binary work” without a named syscall.
  • Patch host kernel; 2022-0185 is one of many.
  • kernel.unprivileged_userns_clone=0 on hosts that do not need it (breaks some rootless; document).
  • Kubernetes: drop securityContext.privileged, set allowPrivilegeEscalation: false, seccomp RuntimeDefault, 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, AppArmor docker-default, Privileged false, no cap_sys_admin
  • mount → permission denied; unshare -Um → EPERM
  • --privileged: Current: =ep, mount succeeds — banned
  • uid_map column 2 is 0 (container root = host root) — finding
  • Hardened run: --cap-drop=ALL, no-new-privileges, read-only, only cap_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