Docker & ContainersII Β· Linux InternalsSeccomp and LSMs
Seccomp and AppArmor β restricting what a container can do
What you'll learn
- Explain seccomp, AppArmor, and SELinux and when each applies to Docker
- Customize seccomp profiles safely
- Diagnose why a container fails after a seccomp or AppArmor profile is applied
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-09
Capabilities narrow what a container can do at the kernel-authority level. Seccomp narrows it further by restricting which syscalls a process can make. AppArmor and SELinux go further still, applying mandatory access control to file, capability, and network operations.
Seccomp
Seccomp (secure computing mode) is a Linux kernel feature that filters syscalls. A seccomp profile is an allowlist of syscalls plus a default action for everything not on it.
Docker applies a default seccomp profile whose allowlist names over
400 syscalls; everything else is denied. The denied ones are
those that have caused real-world container escapes: unshare and
clone with namespace flags, bpf, keyctl, add_key,
perf_event_open, mount and umount2, pivot_root, setns,
kexec_load, and the kernel-module family.
IMAGE=debian:bookworm-slim
docker run --rm "$IMAGE" sh -c 'unshare --user --map-root-user true; echo "default profile: exit $?"'
docker run --rm --security-opt seccomp=unconfined "$IMAGE" \
sh -c 'unshare --user --map-root-user true; echo "unconfined: exit $?"'unshare: unshare failed: Operation not permitted
default profile: exit 1
unconfined: exit 0Illustrative output
Two different exit codes for the same command is the signature. If both runs fail identically, seccomp is not involved and you are back to ownership, mode bits, or an LSM.
CONTAINER=web
docker inspect --format '{{.HostConfig.SecurityOpt}}' "$CONTAINER"
docker exec "$CONTAINER" grep Seccomp /proc/1/status[]
Seccomp: 2
Seccomp_filters: 1Illustrative output
Seccomp: 2 in /proc/<pid>/status means filter mode is active.
Seccomp: 0 means the process has no filter at all β which, in a
container, means somebody set seccomp=unconfined and probably did
not write it down.
Custom seccomp profiles
docker run --security-opt seccomp=./seccomp.json nginxURL=https://raw.githubusercontent.com/moby/profiles/main/seccomp/default.json
curl -fsS "$URL" \
| jq '{defaultAction, defaultErrnoRet, groups: (.syscalls | length), allowed: ([.syscalls[].names[]] | length)}'{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"groups": 33,
"allowed": 442
}defaultErrnoRet: 1 is EPERM. Read that output again before you
write a custom profile: whatever you deny, the container will keep
running and the failure will surface inside the application.
URL=https://raw.githubusercontent.com/moby/profiles/main/seccomp/default.json
curl -fsS "$URL" \
| jq -r '.syscalls[] | select(.action != "SCMP_ACT_ALLOW") | "\(.action) errno=\(.errnoRet) \(.names | join(","))"'SCMP_ACT_ERRNO errno=38 clone3Errno 38 is ENOSYS. clone3 is the one syscall the profile lies
about on purpose, so that glibcβs runtime probe concludes βthis
kernel is too oldβ and takes its clone fallback path instead of
treating the denial as fatal.
Custom profiles are most commonly used to:
- Tighten the default profile (remove syscalls your workload does not use).
- Loosen the default profile (re-enable a syscall the workload needs but Docker blocks).
- Comply with a specific security baseline (CIS Docker Benchmark, PSP, etc.).
The cost of a custom profile is operational, and it is deferred.
When the workload upgrades to a library version that reaches for a
syscall you did not allow, the profile has to be updated β but the
bill does not arrive as a crash at deploy time. It arrives as
EPERM on some code path that only runs during a failover, a
backup, or a certificate rotation, weeks later, to whoever is on
call. Every custom profile you own is a promise to re-test it on
every dependency bump.
AppArmor
AppArmor is a Linux Security Module (LSM) that confines programs to a set of file, capability, and network permissions. It is path- based (Ubuntuβs preferred LSM) and ships with templates for many applications.
Docker applies a default AppArmor profile named
docker-default. To use a custom profile:
sudo apparmor_parser -r /etc/apparmor.d/docker-custom
docker run --security-opt apparmor=docker-custom nginxAppArmor profiles are especially useful for:
- Filesystem containment (deny writes to /proc, /sys outside expected paths).
- Network containment (deny outbound except to specific peers).
- Capability containment (override the capability set with capability rules).
Finding an AppArmor denial, and why dmesg lies about how many there were
An AppArmor denial produces a kernel audit record, not an
application-visible error. The application sees EACCES or EPERM;
the reason lives in the audit stream. Where that stream goes depends
on whether auditd is running, and the two cases look completely
different:
- No
auditd. Records go to the kernel ring buffer and are readable withdmesg. The ring buffer is rate-limited: under a burst the kernel printsaudit: N callbacks suppressedand drops the rest. So a tight retry loop that trips one denial per request shows a handful of lines indmesgand hides thousands. Counting denials indmesgand concluding βit only happened twiceβ is a wrong conclusion drawn from a correct command. auditdrunning. The kernel hands records to the daemon instead, and they stop appearing indmesgaltogether.dmesggoes silent, which reads exactly like βno denialsβ β but the records are in/var/log/audit/audit.log.
Check both, and always check whether auditd is running before you
trust an empty result from either.
systemctl is-active auditd || echo 'auditd not running: denials go to dmesg'
dmesg | grep -E 'apparmor=("DENIED"|"AUDIT")|callbacks suppressed' | tail -20
sudo ausearch -m AVC -ts recent 2>/dev/null | tail -20inactive
[172834.119284] audit: type=1400 audit(1754899201.882:412): apparmor="DENIED" operation="mount" class="mount" profile="docker-default" name="/mnt/remote/" pid=48821 comm="mount" fstype="fuse.sshfs"
[172834.119401] audit: 37 callbacks suppressedIllustrative output
The callbacks suppressed line is the important one. It is the
kernel telling you the count you just read is not the count that
happened.
CONTAINER=web
docker inspect --format '{{.AppArmorProfile}}' "$CONTAINER"
docker exec "$CONTAINER" cat /proc/1/attr/current 2>/dev/null || echo 'no AppArmor label'
aa-status --profiled 2>/dev/null || sudo aa-status | head -5docker-default
docker-default (enforce)Illustrative output
docker-default (enforce) is the working state. (complain) means
the profile logs but does not block β useful while developing a
profile, and a silent hole if it reaches production by accident.
SELinux
SELinux is the other major LSM. It is label-based (RHELβs preferred LSM) and significantly more expressive than AppArmor.
Docker integrates with SELinux via the --security-opt label=... flag. By default, containers run with the
container_t type and limited MCS labels. To customise:
docker run --security-opt label=type:container_myapp_t \
-v /srv/data:/srv/data:z \
nginxThe :z and :Z mount options tell Docker to relabel the host
volume for the container. Misconfiguration here is one of the most
common SELinux + Docker issues.
What to do in practice
- Start with the default Docker security profile. Verify the workload runs without customisation.
- If a syscall or capability is missing, identify the specific call. Add only what is needed.
- Test the workload under the custom profile. A profile that breaks the workload is worse than no profile.
- Document the rationale for each customisation. Auditors will ask.
- Re-test after every dependency upgrade. New libraries introduce new syscalls.
Knowledge check
Knowledge check Β· 5 questions
Q1. Under Docker's default seccomp profile, what happens when a process in a container makes a denied syscall?
Q2. A seccomp profile that is too restrictive causes the container to fail noisily, with the cause visible in `docker logs` and the exit code.
Q3. You suspect a seccomp denial. Which single test distinguishes it from a genuine file-permission problem?
Q4. An empty `dmesg` is sufficient evidence that no AppArmor denials occurred.
Q5. Why might a workload suddenly fail after a library upgrade under a custom seccomp profile?
Passing score: 75%. Answers are checked in this browser.