PS Product SecurityKnowledge Base

๐Ÿณ 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.io still 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-default behavior;
  • custom profile load/unload flow;
  • --security-opt usage;
  • 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:

  1. start with default seccomp;
  2. run the workload with reduced capabilities;
  3. 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

  1. turn on host hardening and patching;
  2. remove --privileged and drop capabilities;
  3. keep Docker default seccomp;
  4. verify AppArmor is active and containers are not unconfined;
  5. introduce targeted custom AppArmor profiles for high-value services;
  6. 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๎ˆ

Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.