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.
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 onVerify AppArmor status
Check that AppArmor is running and can enforce security policies.
sudo aa-status
sudo apparmor_statusThe 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 $USERsudo 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 $USERLog 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 SeccompThe 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/containersGenerate 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-webappCreate 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-databaseCreating custom seccomp profiles for containers
Create seccomp profile directory
Set up a directory structure for custom seccomp profiles.
mkdir -p ~/seccomp-profiles
cd ~/seccomp-profilesCreate 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
& stopRestart rsyslog to apply the configuration:
sudo systemctl restart rsyslogCreate 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
doneMake the script executable and create a systemd service:
sudo chmod +x /usr/local/bin/container-security-monitor.shCreate 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.targetEnable and start the monitoring service:
sudo systemctl daemon-reload
sudo systemctl enable container-security-monitor.service
sudo systemctl start container-security-monitor.serviceConfigure Docker daemon security defaults
Configure Docker to use your