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 apparmorVerify 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 $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
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-webappCreate 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-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 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 dockerCreate 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.shVerify 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.logYou 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
| Symptom | Cause | Fix |
|---|---|---|
| Container fails to start with AppArmor profile | Profile too restrictive or missing required permissions | Check /var/log/apparmor.log and add necessary permissions to profile |
| Application can't write files with custom profile | Missing file path permissions in AppArmor profile | Add specific file paths with rw permissions to profile |
| Seccomp profile blocks legitimate system calls | Required system calls not in allowlist | Use strace to identify needed syscalls and add to profile |
| No security violations in logs | Logging not configured or rsyslog not restarted | Verify rsyslog config and restart service |
| Monitoring script not capturing violations | Log paths don't match system configuration | Check /var/log/syslog and /var/log/audit/audit.log paths |
| Docker won't start with custom daemon.json | JSON syntax error in configuration | Validate JSON syntax with jq . /etc/docker/daemon.json |
Next steps
- Implement container security monitoring with Falco runtime detection for comprehensive threat detection
- Configure Linux cgroups v2 for container resource management to complement security controls
- Setup Kubernetes Pod Security Standards for cluster-wide policy enforcement
- Implement container image scanning with Trivy for vulnerability management