# 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

```bash
sudo aa-status
```

### Verify seccomp support in the kernel

```bash
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

```bash
docker run --rm -it --security-opt apparmor=docker-default hello-world
```

### Confirm a custom seccomp profile is attached

```bash
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

```bash
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

```bash
docker run --rm -d \
  --name apparmor-nginx \
  --security-opt apparmor=docker-nginx-restricted \
  -p 8080:80 \
  nginx:1.27
```

### Debug AppArmor denials

```bash
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`.

```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

```bash
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.

```bash
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

```yaml
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:

```bash
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:

```bash
--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](/cloud-kubernetes-and-infrastructure-security/index-1/dockerfile-security-best-practices.md)
* [Docker Top 10 Misconfigurations](/cloud-kubernetes-and-infrastructure-security/index-1/docker-top-10-misconfigurations.md)
* [Linux Base Image and Host Security Baseline](/cloud-kubernetes-and-infrastructure-security/index/linux-base-image-and-host-security-baseline.md)
* [DevSecOps-Studio — Virtual Lab Environment for Learning DevSecOps](/learning-labs-interview-and-templates/index-2/devsecops-studio-virtual-lab-environment.md)

***

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.product-security.expert/cloud-kubernetes-and-infrastructure-security/index-1/apparmor-and-seccomp-for-docker.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
