Set up mandatory access control for Nginx, Apache, MySQL, and PostgreSQL using AppArmor security profiles. Learn to create custom policies, debug profile violations, and implement advanced enforcement for production web servers and databases.
Prerequisites
- Root or sudo access
- Basic understanding of Linux file permissions
- Web server or database already installed
- Command line experience
What this solves
AppArmor provides mandatory access control (MAC) that restricts programs to a limited set of resources, preventing security breaches even if applications are compromised. This tutorial shows you how to configure custom AppArmor profiles for web servers and databases, enforce strict security policies, and debug profile violations in production environments.
Step-by-step installation
Install and enable AppArmor
AppArmor is pre-installed on Ubuntu but requires additional utilities on other distributions. Install the complete AppArmor suite with profile management tools.
sudo apt update
sudo apt install -y apparmor-utils apparmor-profiles apparmor-profiles-extra
sudo systemctl enable apparmor
sudo systemctl start apparmor
Verify AppArmor status
Check that AppArmor is running and view the current profile status. This shows which profiles are loaded and their enforcement mode.
sudo apparmor_status
You should see output showing loaded profiles in enforce or complain mode. Enforce mode blocks violations while complain mode only logs them.
Install web servers and databases
Install the services we'll secure with AppArmor profiles. We'll use Nginx, Apache, MySQL, and PostgreSQL as examples.
sudo apt install -y nginx apache2 mysql-server postgresql postgresql-contrib
sudo systemctl enable nginx apache2 mysql postgresql
sudo systemctl start mysql postgresql
Configure Nginx AppArmor profile
Create custom Nginx profile
Create a restrictive AppArmor profile for Nginx that allows only necessary file access and network operations. This profile prevents Nginx from accessing sensitive system files.
#include
/usr/sbin/nginx {
#include
#include
#include
capability dac_override,
capability setuid,
capability setgid,
capability net_bind_service,
/usr/sbin/nginx mr,
/etc/nginx/** r,
/var/log/nginx/** w,
/var/lib/nginx/** rw,
/run/nginx.pid rw,
/var/cache/nginx/** rw,
# Web content directories
/var/www/** r,
/usr/share/nginx/** r,
# SSL certificates
/etc/ssl/certs/** r,
/etc/ssl/private/** r,
/etc/letsencrypt/** r,
# Network access
network inet stream,
network inet6 stream,
# Process management
/usr/sbin/nginx Px,
# Deny dangerous operations
audit deny /etc/passwd r,
audit deny /etc/shadow r,
audit deny /root/** rwx,
audit deny /home/** rwx,
}
Load and test Nginx profile
Load the profile in complain mode first to test for violations, then switch to enforce mode. Complain mode logs violations without blocking them.
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
sudo aa-complain /usr/sbin/nginx
sudo systemctl restart nginx
sudo systemctl status nginx
Monitor and adjust Nginx profile
Test Nginx functionality and check for AppArmor denials. Use logprof to automatically update the profile based on logged violations.
curl http://localhost
sudo dmesg | grep -i apparmor
sudo aa-logprof /usr/sbin/nginx
Review suggested changes carefully. Only allow access that Nginx actually needs for your specific configuration.
Enable Nginx profile enforcement
Once testing is complete, switch to enforce mode to actively block policy violations.
sudo aa-enforce /usr/sbin/nginx
sudo systemctl restart nginx
Configure Apache AppArmor profile
Create Apache profile
Apache requires different permissions than Nginx, especially for module loading and CGI execution. This profile accommodates Apache's modular architecture.
#include
/usr/sbin/apache2 {
#include
#include
#include
#include
capability dac_override,
capability setuid,
capability setgid,
capability net_bind_service,
capability sys_tty_config,
/usr/sbin/apache2 mr,
/etc/apache2/** r,
/var/log/apache2/** w,
/var/lib/apache2/** rw,
/run/apache2/** rw,
/var/cache/apache2/** rw,
# Web content
/var/www/** r,
/usr/share/apache2/** r,
# SSL certificates
/etc/ssl/certs/** r,
/etc/ssl/private/** r,
/etc/letsencrypt/** r,
# Module support
/usr/lib/apache2/modules/** mr,
/etc/mime.types r,
# PHP support (if needed)
/usr/bin/php* Px,
/etc/php/** r,
# Network access
network inet stream,
network inet6 stream,
network unix stream,
# Security denials
audit deny /etc/passwd r,
audit deny /etc/shadow r,
audit deny /root/** rwx,
}
Load Apache profile
Load the Apache profile and test in complain mode before enforcing.
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.apache2
sudo aa-complain /usr/sbin/apache2
sudo systemctl restart apache2
sudo systemctl status apache2
Configure MySQL AppArmor profile
Create MySQL security profile
MySQL requires access to data directories, socket files, and configuration. This profile restricts MySQL to only necessary system resources.
#include
/usr/sbin/mysqld {
#include
#include
#include
#include
capability dac_override,
capability setuid,
capability setgid,
capability sys_resource,
capability net_bind_service,
/usr/sbin/mysqld mr,
/etc/mysql/** r,
/var/lib/mysql/** rwk,
/var/log/mysql/** rw,
/run/mysqld/** rw,
/tmp/** rw,
# Configuration files
/etc/hosts.allow r,
/etc/hosts.deny r,
# SSL certificates for MySQL
/etc/ssl/certs/** r,
/etc/mysql/ssl/** r,
# Network access
network inet stream,
network inet6 stream,
network unix stream,
# Memory management
owner /proc/*/status r,
/sys/devices/system/node/*/meminfo r,
# Security restrictions
audit deny /etc/passwd r,
audit deny /etc/shadow r,
audit deny /root/** rwx,
audit deny /home/** rwx,
deny /bin/** x,
deny /usr/bin/** x,
}
Test MySQL profile
Load the MySQL profile and verify database functionality. Check for any access violations in the logs.
sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld
sudo aa-complain /usr/sbin/mysqld
sudo systemctl restart mysql
sudo mysql -u root -e "SELECT version();"
sudo journalctl -u mysql | tail -20
Configure PostgreSQL AppArmor profile
Create PostgreSQL profile
PostgreSQL has a more complex process model with background workers. This profile accounts for PostgreSQL's architecture while maintaining security.
#include
/usr/lib/postgresql/*/bin/postgres {
#include
#include
#include
#include
capability dac_override,
capability setuid,
capability setgid,
capability sys_resource,
capability ipc_lock,
/usr/lib/postgresql/*/bin/postgres mr,
/etc/postgresql/** r,
/var/lib/postgresql/** rwk,
/var/log/postgresql/** rw,
/run/postgresql/** rw,
/tmp/** rw,
# Shared memory and IPC
owner /dev/shm/PostgreSQL.* rw,
/proc/*/stat r,
/proc/*/statm r,
# SSL support
/etc/ssl/certs/** r,
/etc/ssl/private/** r,
# Network access
network inet stream,
network inet6 stream,
network unix stream,
# Process management
/usr/lib/postgresql/*/bin/postgres Px,
# Extensions and libraries
/usr/lib/postgresql/*/lib/** mr,
/usr/share/postgresql/** r,
# Security denials
audit deny /etc/passwd r,
audit deny /etc/shadow r,
audit deny /root/** rwx,
deny /bin/sh x,
deny /usr/bin/** x,
}
Load PostgreSQL profile
Apply the PostgreSQL profile and test database connectivity. PostgreSQL profiles may need adjustment based on your version and extensions.
sudo apparmor_parser -r /etc/apparmor.d/usr.lib.postgresql.*.bin.postgres
sudo aa-complain '/usr/lib/postgresql/*/bin/postgres'
sudo systemctl restart postgresql
sudo -u postgres psql -c "SELECT version();"
sudo journalctl -u postgresql | tail -20
Advanced policy enforcement and monitoring
Enable strict enforcement
Switch all profiles to enforce mode for production security. This blocks any access not explicitly allowed in the profiles.
sudo aa-enforce /usr/sbin/nginx
sudo aa-enforce /usr/sbin/apache2
sudo aa-enforce /usr/sbin/mysqld
sudo aa-enforce '/usr/lib/postgresql/*/bin/postgres'
Configure AppArmor notifications
Set up automatic monitoring for AppArmor violations. This script checks logs and sends alerts when profiles are violated.
#!/bin/bash
AppArmor monitoring script
LOGFILE="/var/log/apparmor-violations.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
Check for new AppArmor denials
DENIALS=$(dmesg | grep -i "apparmor.*denied" | tail -10)
if [ ! -z "$DENIALS" ]; then
echo "[$TIMESTAMP] AppArmor violations detected:" >> $LOGFILE
echo "$DENIALS" >> $LOGFILE
# Optional: Send email alert
# echo "$DENIALS" | mail -s "AppArmor Violation Alert" admin@example.com
fi
Clear old dmesg entries
sudo dmesg -c > /dev/null
sudo chmod +x /usr/local/bin/apparmor-monitor
Create monitoring systemd timer
Set up automated monitoring that runs every 5 minutes to catch violations quickly.
[Unit]
Description=AppArmor Violation Monitor
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/apparmor-monitor
User=root
[Unit]
Description=Run AppArmor Monitor every 5 minutes
Requires=apparmor-monitor.service
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now apparmor-monitor.timer
Create profile backup script
Backup your custom profiles to prevent loss during system updates. This ensures your security policies persist.
#!/bin/bash
Backup custom AppArmor profiles
BACKUP_DIR="/etc/apparmor.d/backups"
DATE=$(date +%Y%m%d-%H%M%S)
sudo mkdir -p $BACKUP_DIR
Backup custom profiles
sudo tar -czf $BACKUP_DIR/apparmor-profiles-$DATE.tar.gz /etc/apparmor.d/usr.*
echo "AppArmor profiles backed up to $BACKUP_DIR/apparmor-profiles-$DATE.tar.gz"
Keep only last 10 backups
sudo find $BACKUP_DIR -name "apparmor-profiles-*.tar.gz" -type f | sort -r | tail -n +11 | xargs -r sudo rm
sudo chmod +x /usr/local/bin/backup-apparmor-profiles
sudo /usr/local/bin/backup-apparmor-profiles
Profile debugging and troubleshooting
Debug profile violations
When services fail after enabling AppArmor, use these debugging techniques to identify missing permissions.
# Check recent AppArmor denials
sudo dmesg | grep -i "apparmor.*denied" | tail -20
Monitor real-time violations
sudo tail -f /var/log/syslog | grep -i apparmor
Use aa-decode to understand denial messages
sudo dmesg | grep -i "apparmor.*denied" | aa-decode
Generate profiles automatically
Use aa-genprof to create initial profiles by monitoring application behavior. This creates a baseline that you can then restrict further.
# Generate profile for a new application
sudo aa-genprof /path/to/application
Follow the prompts and test your application
Choose 'S' to save the profile when done
Test profile changes safely
Always test profile changes in complain mode before enforcing. This prevents service outages from overly restrictive policies.
# Switch to complain mode for testing
sudo aa-complain /usr/sbin/nginx
Test your application thoroughly
Check logs for violations
If everything works, enable enforcement
sudo aa-enforce /usr/sbin/nginx
Verify your setup
# Check AppArmor status
sudo apparmor_status
Verify all profiles are in enforce mode
sudo aa-status | grep enforce
Test web servers
curl -I http://localhost
curl -I http://localhost:8080
Test databases
sudo mysql -u root -e "SELECT 1;"
sudo -u postgres psql -c "SELECT version();"
Check for recent violations
sudo dmesg | grep -i "apparmor.*denied" | tail -5
Verify monitoring is active
sudo systemctl status apparmor-monitor.timer
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Service won't start after profile | Missing required permissions | Switch to complain mode, test, use aa-logprof to add permissions |
| Web server 403 errors | Profile blocking web content access | Add /var/www/** r or your document root to profile |
| Database connection failures | Socket file access denied | Add /run/mysqld/ rw or /run/postgresql/ rw to profile |
| SSL certificate errors | Certificate directory not accessible | Add /etc/ssl/ r and /etc/letsencrypt/ r to profile |
| Profile syntax errors | Invalid AppArmor syntax | Use sudo apparmor_parser -Q /etc/apparmor.d/profile to check syntax |
| Profiles not loading on boot | AppArmor service not enabled | Run sudo systemctl enable apparmor and reboot |
Next steps
- Install and configure NGINX with HTTP/3 and modern security headers
- Configure Linux system firewall with nftables and security hardening
- Configure SELinux policies for web applications and databases
- Implement container security with AppArmor and seccomp profiles
- Configure Linux audit logging for security compliance and monitoring
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Global variables
SCRIPT_NAME=$(basename "$0")
LOG_FILE="/var/log/apparmor-setup.log"
# Usage message
usage() {
echo "Usage: $SCRIPT_NAME [--web-root PATH] [--skip-services SERVICE1,SERVICE2]"
echo " --web-root PATH : Custom web root directory (default: auto-detect)"
echo " --skip-services : Skip specific services (nginx,apache,mysql,postgresql)"
echo "Example: $SCRIPT_NAME --web-root /opt/www --skip-services mysql"
exit 1
}
# Logging function
log() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
exit 1
}
warn() {
echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE"
}
# Cleanup on error
cleanup() {
warn "Installation failed. Rolling back changes..."
if systemctl is-active --quiet apparmor 2>/dev/null; then
aa-complain /usr/sbin/nginx 2>/dev/null || true
aa-complain /usr/sbin/apache2 2>/dev/null || true
aa-complain /usr/bin/mysqld 2>/dev/null || true
aa-complain /usr/bin/postgres 2>/dev/null || true
fi
}
trap cleanup ERR
# Parse arguments
WEB_ROOT=""
SKIP_SERVICES=""
while [[ $# -gt 0 ]]; do
case $1 in
--web-root)
WEB_ROOT="$2"
shift 2
;;
--skip-services)
SKIP_SERVICES="$2"
shift 2
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
;;
esac
done
# Check prerequisites
echo "[1/8] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root or with sudo"
fi
# Auto-detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
APACHE_SERVICE="apache2"
APACHE_CONF_DIR="/etc/apache2"
MYSQL_SERVICE="mysql"
WEB_ROOT=${WEB_ROOT:-"/var/www"}
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
APACHE_SERVICE="httpd"
APACHE_CONF_DIR="/etc/httpd"
MYSQL_SERVICE="mysqld"
WEB_ROOT=${WEB_ROOT:-"/var/www"}
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
APACHE_SERVICE="httpd"
APACHE_CONF_DIR="/etc/httpd"
MYSQL_SERVICE="mysqld"
WEB_ROOT=${WEB_ROOT:-"/var/www"}
;;
*)
error "Unsupported distribution: $ID"
;;
esac
else
error "Cannot detect distribution. /etc/os-release not found."
fi
log "Detected distribution: $ID"
log "Package manager: $PKG_MGR"
# Install AppArmor
echo "[2/8] Installing AppArmor..."
$PKG_UPDATE
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
$PKG_INSTALL apparmor-utils apparmor-profiles apparmor-profiles-extra
else
$PKG_INSTALL apparmor-utils apparmor-profiles apparmor-parser
fi
systemctl enable apparmor
systemctl start apparmor
log "AppArmor installed and started"
# Verify AppArmor status
echo "[3/8] Verifying AppArmor status..."
if ! apparmor_status >/dev/null 2>&1; then
error "AppArmor is not running properly"
fi
log "AppArmor is active and running"
# Install services (skip if requested)
echo "[4/8] Installing web servers and databases..."
SERVICES_TO_INSTALL=""
if [[ "$SKIP_SERVICES" != *"nginx"* ]]; then
SERVICES_TO_INSTALL="$SERVICES_TO_INSTALL nginx"
fi
if [[ "$SKIP_SERVICES" != *"apache"* ]]; then
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
SERVICES_TO_INSTALL="$SERVICES_TO_INSTALL apache2"
else
SERVICES_TO_INSTALL="$SERVICES_TO_INSTALL httpd"
fi
fi
if [[ "$SKIP_SERVICES" != *"mysql"* ]]; then
SERVICES_TO_INSTALL="$SERVICES_TO_INSTALL mysql-server"
fi
if [[ "$SKIP_SERVICES" != *"postgresql"* ]]; then
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
SERVICES_TO_INSTALL="$SERVICES_TO_INSTALL postgresql postgresql-contrib"
else
SERVICES_TO_INSTALL="$SERVICES_TO_INSTALL postgresql postgresql-server"
fi
fi
if [[ -n "$SERVICES_TO_INSTALL" ]]; then
$PKG_INSTALL $SERVICES_TO_INSTALL
log "Services installed: $SERVICES_TO_INSTALL"
fi
# Initialize PostgreSQL on RHEL-based systems
if [[ "$SKIP_SERVICES" != *"postgresql"* && ("$ID" == "almalinux" || "$ID" == "rocky" || "$ID" == "centos" || "$ID" == "rhel" || "$ID" == "fedora") ]]; then
if [ ! -d "/var/lib/pgsql/data" ]; then
postgresql-setup --initdb
fi
fi
# Create Nginx AppArmor profile
echo "[5/8] Creating Nginx AppArmor profile..."
if [[ "$SKIP_SERVICES" != *"nginx"* ]]; then
cat > /etc/apparmor.d/usr.sbin.nginx << 'EOF'
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/openssl>
capability dac_override,
capability setuid,
capability setgid,
capability net_bind_service,
/usr/sbin/nginx mr,
/etc/nginx/** r,
/var/log/nginx/** w,
/var/lib/nginx/** rw,
/run/nginx.pid rw,
/var/cache/nginx/** rw,
# Web content directories
/var/www/** r,
/usr/share/nginx/** r,
# SSL certificates
/etc/ssl/certs/** r,
/etc/ssl/private/** r,
/etc/letsencrypt/** r,
# Network access
network inet stream,
network inet6 stream,
# Process management
/usr/sbin/nginx Px,
# Deny dangerous operations
audit deny /etc/passwd r,
audit deny /etc/shadow r,
audit deny /root/** rwx,
audit deny /home/** rwx,
}
EOF
# Load profile in complain mode
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
aa-complain /usr/sbin/nginx
log "Nginx AppArmor profile created and loaded in complain mode"
fi
# Create Apache AppArmor profile
echo "[6/8] Creating Apache AppArmor profile..."
if [[ "$SKIP_SERVICES" != *"apache"* ]]; then
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
APACHE_BINARY="/usr/sbin/apache2"
else
APACHE_BINARY="/usr/sbin/httpd"
fi
cat > /etc/apparmor.d/usr.sbin.${APACHE_SERVICE} << EOF
#include <tunables/global>
${APACHE_BINARY} {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/openssl>
#include <abstractions/apache2-common>
capability dac_override,
capability setuid,
capability setgid,
capability net_bind_service,
${APACHE_BINARY} mr,
${APACHE_CONF_DIR}/** r,
/var/log/${APACHE_SERVICE}/** w,
/var/lib/${APACHE_SERVICE}/** rw,
/run/${APACHE_SERVICE}/** rw,
# Web content
${WEB_ROOT}/** r,
# SSL
/etc/ssl/certs/** r,
/etc/ssl/private/** r,
network inet stream,
network inet6 stream,
audit deny /etc/passwd r,
audit deny /etc/shadow r,
audit deny /root/** rwx,
}
EOF
# Load profile in complain mode
apparmor_parser -r /etc/apparmor.d/usr.sbin.${APACHE_SERVICE}
aa-complain ${APACHE_BINARY}
log "Apache AppArmor profile created and loaded in complain mode"
fi
# Start and enable services
echo "[7/8] Starting and enabling services..."
SERVICES_TO_START=""
[[ "$SKIP_SERVICES" != *"nginx"* ]] && SERVICES_TO_START="$SERVICES_TO_START nginx"
[[ "$SKIP_SERVICES" != *"apache"* ]] && SERVICES_TO_START="$SERVICES_TO_START $APACHE_SERVICE"
[[ "$SKIP_SERVICES" != *"mysql"* ]] && SERVICES_TO_START="$SERVICES_TO_START $MYSQL_SERVICE"
[[ "$SKIP_SERVICES" != *"postgresql"* ]] && SERVICES_TO_START="$SERVICES_TO_START postgresql"
for service in $SERVICES_TO_START; do
if systemctl list-unit-files "$service.service" >/dev/null 2>&1; then
systemctl enable "$service"
systemctl start "$service"
log "Service $service enabled and started"
fi
done
# Final verification
echo "[8/8] Performing final verification..."
apparmor_status | head -20
log "AppArmor profiles loaded successfully"
# Display next steps
cat << EOF
${GREEN}Installation completed successfully!${NC}
Next steps:
1. Test your web services and monitor AppArmor logs:
${YELLOW}sudo dmesg | grep -i apparmor${NC}
2. Use aa-logprof to adjust profiles based on violations:
${YELLOW}sudo aa-logprof${NC}
3. Switch profiles to enforce mode when ready:
${YELLOW}sudo aa-enforce /usr/sbin/nginx${NC}
${YELLOW}sudo aa-enforce ${APACHE_BINARY}${NC}
4. Check profile status:
${YELLOW}sudo apparmor_status${NC}
Configuration log saved to: $LOG_FILE
EOF
Review the script before running. Execute with: bash install.sh