๐ณ AppArmor and Seccomp for Docker
Intro: AppArmor and seccomp are two of the highest-value Linux containment controls available to Docker users. They do not make containers โsafe by magicโ, but they reduce the damage an attacker can do after compromise by constraining what a container can execute, read, write, or ask the kernel to do.
What this page includes
- what AppArmor and seccomp are and why they matter;
- Docker defaults versus custom profiles;
- host prerequisites and verification steps;
- practical examples, commands, files, and troubleshooting;
- where
dev-sec.iostill helps and where you should prefer newer runtime patterns.
Why this matters
When teams say โthe container is isolatedโ, they often mean only namespace and cgroup isolation. That is not enough.
Two common post-compromise paths are:
- abusing the containerโs allowed syscalls to interact with kernel features it never needed;
- abusing filesystem, process, or network permissions that should have been denied.
AppArmor and seccomp help reduce these paths.
What each control does
AppArmor
AppArmor is a Linux Security Module that associates a profile with a process and defines what that process can access or do.
In Docker terms, AppArmor is useful for:
- constraining file reads/writes;
- denying dangerous binaries or shell paths;
- restricting certain network activity;
- adding audit records when a container does something it should not do.
Docker documents that containers use the docker-default AppArmor profile by default when AppArmor is available, and that you can override it with --security-opt apparmor=<profile>. The docs also show that Docker expects the profile to be loaded into the kernel first with apparmor_parser. ๎cite๎turn208157search0๎
Seccomp
Seccomp constrains which syscalls the process may execute.
In Docker terms, seccomp is useful for:
- blocking dangerous or unnecessary kernel attack surface;
- enforcing a least-privilege syscall model;
- making privilege escalation and container escape techniques harder.
Dockerโs current docs say the default seccomp profile blocks about 44 syscalls out of 300+, and that it is an allowlist-based profile using SCMP_ACT_ERRNO by default. Docker also explicitly says it is not recommended to casually replace the default profile without understanding the consequences. ๎cite๎turn208157search1๎
Where dev-sec.io fits
The dev-sec.io ecosystem is still useful as a hardening automation baseline, especially if you want repeatable Linux / SSH / Docker / Kubernetes hardening in Ansible, Chef, Puppet, and InSpec-style validation. Their current site and GitHub org still show active hardening baselines and maintained hardening collections. ๎cite๎turn172423search1๎turn172423search2๎
That said, for AppArmor and seccomp specifically, the Docker docs should be treated as the primary source of truth for Docker runtime behavior. Use dev-sec.io as:
- baseline system hardening around Docker hosts;
- automation for Linux and SSH posture;
- testable compliance / benchmark alignment.
Use the Docker docs as the primary source for:
docker-defaultbehavior;- custom profile load/unload flow;
--security-optusage;- seccomp profile schema and default blocked syscalls.
Baseline principles before touching profiles
Before custom profiles, apply these easier controls first:
- run as non-root where possible;
- use a read-only root filesystem if practical;
- drop unnecessary Linux capabilities;
- avoid
--privileged; - avoid broad bind mounts and host socket mounts;
- keep the host and Docker Engine patched.
Profiles are strong, but they are not a substitute for a sane runtime posture.
Host-side prerequisites and checks
Verify AppArmor is available
sudo aa-status
Verify seccomp support in the kernel
grep CONFIG_SECCOMP= /boot/config-$(uname -r)
Docker documents this exact kernel check for seccomp support. ๎cite๎turn208157search1๎
Check Docker rootless mode as a safer local default
For developer or lab hosts, Dockerโs current docs continue to recommend considering rootless mode when the goal is to reduce daemon and runtime privilege exposure. Rootless mode runs both the daemon and containers inside a user namespace, unlike userns-remap, where the daemon still runs as root. ๎cite๎turn359551search0๎
Quick baseline tests
Confirm which AppArmor profile a container is using
docker run --rm -it --security-opt apparmor=docker-default hello-world
Confirm a custom seccomp profile is attached
docker run --rm -it \
--security-opt seccomp=/etc/docker/seccomp-restrictive.json \
alpine:3.20 sh
AppArmor โ default versus custom
Default behavior
Docker says docker-default is โmoderately protective while providing wide application compatibilityโ. That is a good default, but not always enough for high-trust workloads or regulated environments. ๎cite๎turn208157search0๎
When to write a custom AppArmor profile
Use a custom profile when you need one of these:
- deny execution of shells or package managers in a runtime-only image;
- restrict write access to a tight subset of directories;
- deny raw/packet network capabilities for a service that only needs TCP;
- make dangerous actions auditable in a more targeted way.
Example custom AppArmor profile for NGINX-like container
Save as /etc/apparmor.d/containers/docker-nginx-restricted.
#include <tunables/global>
profile docker-nginx-restricted flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
file,
umount,
network inet tcp,
deny network raw,
deny network packet,
/usr/sbin/nginx ix,
/var/run/nginx.pid w,
deny /bin/** mrwklx,
deny /sbin/** mrwklx,
deny /usr/bin/** mrwklx,
deny /usr/sbin/** mrwklx,
deny /tmp/** wl,
deny /proc/** w,
deny /sys/** rwklx,
capability net_bind_service,
deny mount,
}
This is intentionally stricter than Dockerโs general compatibility default. The goal is to show the pattern:
- explicitly allow the service binary;
- explicitly allow only the minimum file writes;
- deny shell escape and post-compromise convenience binaries;
- deny raw/packet networking;
- deny mount operations.
Load the AppArmor profile
sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx-restricted
Docker documents this exact load pattern. ๎cite๎turn208157search0๎
Run a container with the custom profile
docker run --rm -d \
--name apparmor-nginx \
--security-opt apparmor=docker-nginx-restricted \
-p 8080:80 \
nginx:1.27
Debug AppArmor denials
sudo dmesg | tail -n 100
sudo aa-status
Docker explicitly calls out both dmesg and aa-status for debugging profile behavior. ๎cite๎turn208157search0๎
Seccomp โ default versus custom
Why start with the default profile
Dockerโs current guidance is very clear: the default seccomp profile is already meaningful and it is not recommended to change it casually. The right pattern is:
- start with default seccomp;
- run the workload with reduced capabilities;
- only create a custom profile if the workload truly needs tighter or different syscall boundaries.
Good reasons to create a custom seccomp profile
- the container has a narrow runtime and you want to explicitly deny more than Dockerโs default profile already denies;
- you want a policy tailored to a controlled service type;
- you are validating a strong sandbox for semi-trusted or third-party code.
Example restrictive seccomp profile
Save as /etc/docker/seccomp-restrictive.json.
{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": [
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
]
}
],
"syscalls": [
{
"names": [
"accept", "accept4", "access", "arch_prctl", "bind", "brk",
"capget", "capset", "chdir", "close", "connect", "dup",
"dup2", "epoll_create1", "epoll_ctl", "epoll_pwait", "eventfd2",
"execve", "exit", "exit_group", "faccessat", "fchmod", "fchown",
"fcntl", "fdatasync", "fstat", "futex", "getcwd", "getdents64",
"getegid", "geteuid", "getgid", "getpid", "getppid", "getuid",
"ioctl", "listen", "lseek", "madvise", "mkdirat", "mmap",
"mprotect", "munmap", "newfstatat", "openat", "pipe2", "poll",
"ppoll", "pread64", "prlimit64", "pwrite64", "read", "readlinkat",
"recvfrom", "recvmsg", "rt_sigaction", "rt_sigprocmask", "sendmsg",
"sendto", "set_robust_list", "set_tid_address", "shutdown", "socket",
"statx", "uname", "write", "writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
This is a simplified example, not a universal production profile. The point is to show the pattern: deny by default and allow only what the workload demonstrably needs.
Run with the custom seccomp profile
docker run --rm -it \
--security-opt seccomp=/etc/docker/seccomp-restrictive.json \
alpine:3.20 sh
Example test
Try a syscall or behavior the profile does not permit.
docker run --rm -it \
--security-opt seccomp=/etc/docker/seccomp-restrictive.json \
alpine:3.20 unshare --map-root-user --user sh
If the profile blocks the required syscalls, you should see an error such as Operation not permitted or Permission denied.
Docker Compose example
services:
api:
image: ghcr.io/example/payment-api:1.4.3
read_only: true
security_opt:
- apparmor=docker-nginx-restricted
- seccomp=/etc/docker/seccomp-restrictive.json
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
tmpfs:
- /tmp:size=64m,noexec,nosuid
How AppArmor and seccomp fit together
Use them together, but remember they solve different things.
| Control | Best at | Weakness if used alone |
|---|---|---|
| AppArmor | file/process/network policy at process boundary | may still allow dangerous syscall surface |
| Seccomp | syscall-level reduction of kernel attack surface | does not model file/path intent |
| Capabilities drop | removing broad privilege classes | still leaves allowed syscalls and file access patterns |
| Read-only FS | reducing filesystem tampering | does not stop dangerous syscalls or process actions |
Suggested rollout order
- turn on host hardening and patching;
- remove
--privilegedand drop capabilities; - keep Docker default seccomp;
- verify AppArmor is active and containers are not unconfined;
- introduce targeted custom AppArmor profiles for high-value services;
- introduce custom seccomp only where the service profile is stable enough to maintain it.
Troubleshooting checklist
Container fails to start after applying AppArmor
Check:
sudo dmesg | tail -n 100
sudo aa-status
Container suddenly loses network or shell access
That often means the profile denied raw or packet networking, or denied /bin/sh / /bin/dash execution. In production this may be desired; in troubleshooting it can surprise operators.
Seccomp breaks the workload
Look for:
- missing syscall in the custom allowlist;
- hidden dependency like
ioctl,clone,setns, or language runtime behavior; - false assumption that the workload is as โsimpleโ as its top process suggests.
You are tempted to disable everything
Avoid:
--security-opt seccomp=unconfined
--security-opt apparmor=unconfined
Use these only as short-lived diagnostic steps, not as a convenience baseline.
Practical comments and cautionary notes
- Prefer default seccomp + custom AppArmor before jumping to highly custom seccomp for every workload.
- Keep the profiles in version control.
- Test profiles with the exact image and entrypoint used in production.
- Expect some troubleshooting effort when workloads rely on shells, package managers, or helper binaries.
- For developer workstations, rootless Docker is often a better default posture than trying to make a fully privileged local Docker daemon โsafe enough.โ ๎cite๎turn359551search0๎
Cross-links
- Dockerfile Security Best Practices
- Docker Top 10 Misconfigurations
- Linux Base Image and Host Security Baseline
- DevSecOps-Studio โ Virtual Lab Environment for Learning DevSecOps
Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.