Implement container security with AppArmor and seccomp profiles

Intermediate 45 min Apr 29, 2026 1,311 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Secure your containers with AppArmor mandatory access controls and seccomp system call filtering. Learn to create custom security profiles, implement runtime policies, and monitor container security violations in production environments.

Prerequisites

  • Root or sudo access
  • Docker or Podman installed
  • Basic understanding of Linux security concepts

What this solves

Container security relies on multiple layers of protection beyond basic isolation. AppArmor provides mandatory access control by restricting what files and capabilities containers can access, while seccomp filters limit which system calls containers can make. This tutorial shows you how to implement both security mechanisms to harden your containerized applications against privilege escalation and system compromise.

Understanding AppArmor and seccomp security mechanisms

AppArmor is a Linux Security Module that confines programs to a limited set of resources through mandatory access control policies. For containers, AppArmor profiles define which files, network resources, and Linux capabilities a container can access. Seccomp (secure computing mode) filters system calls at the kernel level, blocking potentially dangerous operations before they reach the kernel.

Docker and Podman automatically apply default profiles, but production environments need custom profiles tailored to specific application requirements. The default Docker seccomp profile blocks about 44 of the 300+ available system calls, while AppArmor provides file system and capability restrictions.

Note: AppArmor is available on Ubuntu and Debian systems by default. RHEL-based systems like AlmaLinux and Rocky Linux use SELinux instead, which provides similar functionality through different mechanisms.

Step-by-step installation

Install and enable AppArmor utilities

Install the AppArmor userspace utilities needed to create and manage security profiles.

sudo apt update
sudo apt install -y apparmor-utils apparmor-profiles apparmor-profiles-extra
sudo systemctl enable apparmor
sudo systemctl start apparmor
# SELinux is used instead of AppArmor on RHEL-based systems
sudo dnf install -y container-selinux selinux-policy-targeted
sudo setsebool -P container_manage_cgroup on

Verify AppArmor status

Check that AppArmor is running and can enforce security policies.

sudo aa-status
sudo apparmor_status

The output should show AppArmor is loaded with profiles in enforce mode. You'll see the default Docker profile listed as docker-default.

Install Docker with security features

Install Docker with AppArmor and seccomp support enabled.

sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER

Log out and back in for group changes to take effect, or use newgrp docker.

Test default security profiles

Run a container to verify that default AppArmor and seccomp profiles are active.

docker run --rm alpine:latest grep -i apparmor /proc/self/attr/current
docker run --rm alpine:latest cat /proc/self/status | grep Seccomp

The first command should show the AppArmor profile name, while the second should show seccomp mode as 2 (filtered).

Creating custom AppArmor profiles for containers

Create a custom AppArmor profile directory

Set up a workspace for custom container profiles.

sudo mkdir -p /etc/apparmor.d/containers
cd /etc/apparmor.d/containers

Generate a restrictive web application profile

Create a custom AppArmor profile for a web application container that needs limited file access.

#include 

Load and test the custom profile

Parse and load the custom AppArmor profile into the kernel.

sudo apparmor_parser -r /etc/apparmor.d/containers/docker-webapp
sudo aa-status | grep docker-webapp

Create a database container profile

Create a more restrictive profile for database containers that don't need network access.

#include 

Load the database profile:

sudo apparmor_parser -r /etc/apparmor.d/containers/docker-database

Creating custom seccomp profiles for containers

Create seccomp profile directory

Set up a directory structure for custom seccomp profiles.

mkdir -p ~/seccomp-profiles
cd ~/seccomp-profiles

Create a restrictive seccomp profile

Create a custom seccomp profile that blocks dangerous system calls while allowing necessary ones for web applications.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "accept",
        "accept4",
        "access",
        "adjtimex",
        "alarm",
        "bind",
        "brk",
        "capget",
        "capset",
        "chdir",
        "chmod",
        "chown",
        "chown32",
        "clock_getres",
        "clock_gettime",
        "clock_nanosleep",
        "close",
        "connect",
        "copy_file_range",
        "creat",
        "dup",
        "dup2",
        "dup3",
        "epoll_create",
        "epoll_create1",
        "epoll_ctl",
        "epoll_pwait",
        "epoll_wait",
        "eventfd",
        "eventfd2",
        "execve",
        "exit",
        "exit_group",
        "faccessat",
        "fadvise64",
        "fchdir",
        "fchmod",
        "fchmodat",
        "fchown",
        "fchown32",
        "fchownat",
        "fcntl",
        "fcntl64",
        "fdatasync",
        "fgetxattr",
        "flistxattr",
        "flock",
        "fork",
        "fstat",
        "fstat64",
        "fstatfs",
        "fstatfs64",
        "fsync",
        "ftruncate",
        "ftruncate64",
        "futex",
        "getcwd",
        "getdents",
        "getdents64",
        "getegid",
        "geteuid",
        "getgid",
        "getgroups",
        "getpeername",
        "getpgid",
        "getpgrp",
        "getpid",
        "getppid",
        "getpriority",
        "getrandom",
        "getresgid",
        "getresuid",
        "getrlimit",
        "get_robust_list",
        "getrusage",
        "getsid",
        "getsockname",
        "getsockopt",
        "get_thread_area",
        "gettid",
        "gettimeofday",
        "getuid",
        "getxattr",
        "inotify_add_watch",
        "inotify_init",
        "inotify_init1",
        "inotify_rm_watch",
        "io_cancel",
        "ioctl",
        "io_destroy",
        "io_getevents",
        "ioprio_get",
        "ioprio_set",
        "io_setup",
        "io_submit",
        "ipc",
        "kill",
        "lchown",
        "lchown32",
        "lgetxattr",
        "link",
        "linkat",
        "listen",
        "listxattr",
        "llistxattr",
        "_llseek",
        "lseek",
        "lsetxattr",
        "lstat",
        "lstat64",
        "madvise",
        "memfd_create",
        "mincore",
        "mkdir",
        "mkdirat",
        "mknod",
        "mknodat",
        "mlock",
        "mlock2",
        "mlockall",
        "mmap",
        "mmap2",
        "mprotect",
        "mq_getsetattr",
        "mq_notify",
        "mq_open",
        "mq_timedreceive",
        "mq_timedsend",
        "mq_unlink",
        "mremap",
        "msgctl",
        "msgget",
        "msgrcv",
        "msgsnd",
        "msync",
        "munlock",
        "munlockall",
        "munmap",
        "nanosleep",
        "newfstatat",
        "_newselect",
        "open",
        "openat",
        "pause",
        "pipe",
        "pipe2",
        "poll",
        "ppoll",
        "prctl",
        "pread64",
        "preadv",
        "prlimit64",
        "pselect6",
        "pwrite64",
        "pwritev",
        "read",
        "readahead",
        "readlink",
        "readlinkat",
        "readv",
        "recv",
        "recvfrom",
        "recvmsg",
        "recvmmsg",
        "rename",
        "renameat",
        "renameat2",
        "restart_syscall",
        "rmdir",
        "rt_sigaction",
        "rt_sigpending",
        "rt_sigprocmask",
        "rt_sigqueueinfo",
        "rt_sigreturn",
        "rt_sigsuspend",
        "rt_sigtimedwait",
        "rt_tgsigqueueinfo",
        "sched_getaffinity",
        "sched_getattr",
        "sched_getparam",
        "sched_get_priority_max",
        "sched_get_priority_min",
        "sched_getscheduler",
        "sched_rr_get_interval",
        "sched_setaffinity",
        "sched_setattr",
        "sched_setparam",
        "sched_setscheduler",
        "sched_yield",
        "seccomp",
        "select",
        "semctl",
        "semget",
        "semop",
        "semtimedop",
        "send",
        "sendfile",
        "sendfile64",
        "sendmmsg",
        "sendmsg",
        "sendto",
        "setfsgid",
        "setfsgid32",
        "setfsuid",
        "setfsuid32",
        "setgid",
        "setgid32",
        "setgroups",
        "setgroups32",
        "setitimer",
        "setpgid",
        "setpriority",
        "setregid",
        "setregid32",
        "setresgid",
        "setresgid32",
        "setresuid",
        "setresuid32",
        "setreuid",
        "setreuid32",
        "setrlimit",
        "set_robust_list",
        "setsid",
        "setsockopt",
        "set_thread_area",
        "set_tid_address",
        "setuid",
        "setuid32",
        "setxattr",
        "shmat",
        "shmctl",
        "shmdt",
        "shmget",
        "shutdown",
        "sigaltstack",
        "signalfd",
        "signalfd4",
        "sigreturn",
        "socket",
        "socketcall",
        "socketpair",
        "splice",
        "stat",
        "stat64",
        "statfs",
        "statfs64",
        "statx",
        "symlink",
        "symlinkat",
        "sync",
        "sync_file_range",
        "syncfs",
        "sysinfo",
        "tee",
        "tgkill",
        "time",
        "timer_create",
        "timer_delete",
        "timer_getoverrun",
        "timer_gettime",
        "timer_settime",
        "times",
        "tkill",
        "truncate",
        "truncate64",
        "ugetrlimit",
        "umask",
        "uname",
        "unlink",
        "unlinkat",
        "utime",
        "utimensat",
        "utimes",
        "vfork",
        "vmsplice",
        "wait4",
        "waitid",
        "waitpid",
        "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Create a minimal seccomp profile for databases

Create an even more restrictive seccomp profile for database containers that don't need network system calls.

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "access",
        "brk",
        "chdir",
        "chmod",
        "chown",
        "close",
        "creat",
        "dup",
        "dup2",
        "execve",
        "exit",
        "exit_group",
        "fchmod",
        "fchown",
        "fcntl",
        "fdatasync",
        "fork",
        "fstat",
        "fsync",
        "ftruncate",
        "getcwd",
        "getegid",
        "geteuid",
        "getgid",
        "getpid",
        "getuid",
        "lseek",
        "lstat",
        "mkdir",
        "mmap",
        "mprotect",
        "munmap",
        "open",
        "openat",
        "read",
        "readv",
        "rename",
        "rmdir",
        "stat",
        "sync",
        "truncate",
        "unlink",
        "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Implementing runtime security policies and monitoring

Test containers with custom profiles

Run containers using the custom AppArmor and seccomp profiles to verify they work correctly.

# Test web application with custom profiles
docker run --rm \
  --security-opt apparmor=docker-webapp \
  --security-opt seccomp=~/seccomp-profiles/webapp-seccomp.json \
  nginx:alpine echo "Web app security test passed"

# Test database container with restrictive profiles
docker run --rm \
  --security-opt apparmor=docker-database \
  --security-opt seccomp=~/seccomp-profiles/database-seccomp.json \
  alpine:latest echo "Database security test passed"

Set up AppArmor logging for monitoring

Configure system logging to capture AppArmor violations for security monitoring.

# AppArmor logging configuration
:msg,contains,"apparmor" /var/log/apparmor.log
& stop

Restart rsyslog to apply the configuration:

sudo systemctl restart rsyslog

Create a security monitoring script

Create a script to monitor and alert on security violations.

#!/bin/bash

# Container Security Monitor
# Monitors AppArmor and audit logs for security violations

LOGFILE="/var/log/container-security.log"
ALERT_EMAIL="admin@example.com"

log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOGFILE"
}

check_apparmor_violations() {
    local violations
    violations=$(grep "apparmor.*DENIED" /var/log/syslog | tail -n 20)
    
    if [[ -n "$violations" ]]; then
        log_message "AppArmor violations detected:"
        echo "$violations" >> "$LOGFILE"
        
        # Send alert email (requires mail command)
        if command -v mail >/dev/null 2>&1; then
            echo "$violations" | mail -s "Container Security Alert: AppArmor Violations" "$ALERT_EMAIL"
        fi
    fi
}

check_seccomp_violations() {
    local violations
    violations=$(grep "audit.*seccomp" /var/log/audit/audit.log 2>/dev/null | tail -n 20)
    
    if [[ -n "$violations" ]]; then
        log_message "Seccomp violations detected:"
        echo "$violations" >> "$LOGFILE"
        
        # Send alert email
        if command -v mail >/dev/null 2>&1; then
            echo "$violations" | mail -s "Container Security Alert: Seccomp Violations" "$ALERT_EMAIL"
        fi
    fi
}

check_container_escapes() {
    # Check for common container escape attempts
    local escape_patterns=("docker.*breakout" "runc.*escape" "privileged.*container")
    
    for pattern in "${escape_patterns[@]}"; do
        local matches
        matches=$(grep -i "$pattern" /var/log/syslog | tail -n 10)
        
        if [[ -n "$matches" ]]; then
            log_message "Potential container escape attempt detected: $pattern"
            echo "$matches" >> "$LOGFILE"
        fi
    done
}

# Main monitoring loop
log_message "Starting container security monitoring"

while true; do
    check_apparmor_violations
    check_seccomp_violations
    check_container_escapes
    
    # Wait 60 seconds between checks
    sleep 60
done

Make the script executable and create a systemd service:

sudo chmod +x /usr/local/bin/container-security-monitor.sh

Create systemd service for security monitoring

Set up the monitoring script as a systemd service for automatic startup.

[Unit]
Description=Container Security Monitor
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/container-security-monitor.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Enable and start the monitoring service:

sudo systemctl daemon-reload
sudo systemctl enable container-security-monitor.service
sudo systemctl start container-security-monitor.service

Configure Docker daemon security defaults

Configure Docker to use your

Don't want to manage this yourself?

We handle infrastructure for businesses that depend on uptime. Fully managed, with one fixed contact who knows your setup.

You get one fixed contact who knows your setup

Rotterdam 01:25 · reachable in a message, no ticket form