Implement container security with AppArmor and seccomp profiles

Intermediate 45 min Apr 29, 2026 299 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 

profile docker-webapp flags=(attach_disconnected,mediate_deleted) {
  #include 
  
  # Deny dangerous capabilities
  deny capability sys_admin,
  deny capability sys_ptrace,
  deny capability sys_module,
  deny capability sys_rawio,
  
  # Allow basic capabilities needed for web apps
  capability setuid,
  capability setgid,
  capability chown,
  capability dac_override,
  capability fowner,
  capability fsetid,
  capability kill,
  capability net_bind_service,
  
  # Network access
  network inet tcp,
  network inet udp,
  network unix stream,
  
  # File system access - be very specific
  /app/** r,
  /app/public/** rw,
  /tmp/** rw,
  /var/log/app/** rw,
  /dev/null rw,
  /dev/zero r,
  /dev/random r,
  /dev/urandom r,
  
  # Deny access to sensitive areas
  deny /etc/passwd r,
  deny /etc/shadow r,
  deny /proc/sys/** rw,
  deny /sys/** rw,
  deny mount,
  deny umount,
  
  # Standard library access
  /lib{,32,64}/** mr,
  /usr/lib{,32,64}/** mr,
  
  # Allow execution of application binaries
  /usr/bin/node ix,
  /usr/bin/python3 ix,
  /bin/sh ix,
  
  # Prevent privilege escalation
  deny /bin/su x,
  deny /usr/bin/sudo x,
  deny /usr/bin/passwd x
}

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 

profile docker-database flags=(attach_disconnected,mediate_deleted) {
  #include 
  
  # Very limited capabilities for database
  capability setuid,
  capability setgid,
  capability chown,
  capability dac_override,
  
  # No network access (internal communication only)
  network unix stream,
  deny network inet,
  
  # Restricted file access for database files
  /var/lib/mysql/** rw,
  /var/lib/postgresql/** rw,
  /tmp/** rw,
  /var/log/mysql/** rw,
  /var/log/postgresql/** rw,
  
  # System files - read only
  /etc/mysql/** r,
  /etc/postgresql/** r,
  
  # Block dangerous areas completely
  deny /boot/** rwklx,
  deny /proc/sys/** rwklx,
  deny /sys/** rwklx,
  deny capability sys_admin,
  deny capability sys_ptrace,
  deny capability sys_module,
  
  # Essential system access
  /lib{,32,64}/** mr,
  /usr/lib{,32,64}/** mr,
  /usr/bin/mysql* ix,
  /usr/bin/postgres* ix
}

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 custom profiles by default for enhanced security.

{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "security-opts": [
    "apparmor=docker-webapp"
  ],
  "no-new-privileges": true,
  "userns-remap": "default"
}

Restart Docker to apply the configuration:

sudo systemctl restart docker

Create container security assessment tool

Create a tool to assess the security posture of running containers.

#!/bin/bash

Container Security Assessment Tool

Checks security settings of running containers

echo "Container Security Assessment Report" echo "=====================================\n" echo "Active AppArmor Profiles:" sudo aa-status | grep -E "profiles.*in enforce mode|docker" echo echo "Running Container Security Status:" echo "Container ID | Image | AppArmor Profile | Seccomp | Privileged" echo "--------------------------------------------------------" for container in $(docker ps -q); do container_info=$(docker inspect "$container" --format '{{.Id}}|{{.Config.Image}}|{{.AppArmorProfile}}|{{.HostConfig.SecurityOpt}}|{{.HostConfig.Privileged}}') echo "$container_info" | head -c 12 echo -n " | " echo "$container_info" | cut -d'|' -f2- | tr '|' ' | ' done echo "\nSecurity Recommendations:" echo "========================"

Check for containers without AppArmor

no_apparmor=$(docker ps --format 'table {{.ID}}\t{{.Image}}' --filter "label=security.apparmor=unconfined") if [[ -n "$no_apparmor" ]]; then echo "⚠️ Containers running without AppArmor protection detected" fi

Check for privileged containers

privileged=$(docker ps --filter "status=running" --format 'table {{.ID}}\t{{.Image}}' | while read -r line; do container_id=$(echo "$line" | awk '{print $1}') if [[ "$container_id" != "CONTAINER" ]] && [[ -n "$container_id" ]]; then is_privileged=$(docker inspect "$container_id" --format '{{.HostConfig.Privileged}}') if [[ "$is_privileged" == "true" ]]; then echo "$line" fi fi done) if [[ -n "$privileged" ]]; then echo "🚨 Privileged containers detected - review if necessary:" echo "$privileged" fi echo "\n✅ Assessment completed at $(date)"

Make the assessment tool executable:

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

Verify your setup

Test that your container security implementation is working correctly.

# Check AppArmor is active and profiles are loaded
sudo aa-status | grep docker

Verify seccomp profiles are valid

docker run --rm --security-opt seccomp=~/seccomp-profiles/webapp-seccomp.json alpine:latest echo "Seccomp test passed"

Run security assessment

sudo /usr/local/bin/assess-container-security.sh

Check security monitoring service

sudo systemctl status container-security-monitor.service

View recent security logs

sudo tail -20 /var/log/container-security.log

You can now integrate these security profiles with your container orchestration platform. For Kubernetes environments, you would reference these profiles in Pod Security Standards or use admission controllers for policy enforcement.

Common issues

SymptomCauseFix
Container fails to start with AppArmor profileProfile too restrictive or missing required permissionsCheck /var/log/apparmor.log and add necessary permissions to profile
Application can't write files with custom profileMissing file path permissions in AppArmor profileAdd specific file paths with rw permissions to profile
Seccomp profile blocks legitimate system callsRequired system calls not in allowlistUse strace to identify needed syscalls and add to profile
No security violations in logsLogging not configured or rsyslog not restartedVerify rsyslog config and restart service
Monitoring script not capturing violationsLog paths don't match system configurationCheck /var/log/syslog and /var/log/audit/audit.log paths
Docker won't start with custom daemon.jsonJSON syntax error in configurationValidate JSON syntax with jq . /etc/docker/daemon.json

Next steps

Running this in production?

Want this handled for you? Setting up container security once is straightforward. Keeping profiles updated, monitoring violations, and responding to security incidents across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Need help?

Don't want to manage this yourself?

We handle infrastructure security hardening for businesses that depend on uptime. From initial setup to ongoing operations.