SELinux & AppArmor - Street-Level Ops¶
Quick Diagnosis Commands¶
# ── SELinux Status ──
getenforce # One-word answer: Enforcing/Permissive/Disabled
sestatus # Full status including policy type
cat /etc/selinux/config # Permanent config
# ── SELinux File Contexts ──
ls -Z /path/to/file # Show file security context
ls -Zd /path/to/dir # Show directory context (not contents)
matchpathcon /var/www/html/index.html # What context SHOULD this path have?
# ── SELinux Process Contexts ──
ps -eZ | grep nginx # What type is a process running as?
id -Z # Your current SELinux identity
# ── SELinux Denials ──
ausearch -m AVC -ts recent # Recent denials from audit log
ausearch -m AVC -c httpd -ts today # Denials for httpd today
sealert -a /var/log/audit/audit.log # Human-readable analysis (RHEL/CentOS)
journalctl -t setroubleshoot --since "1 hour ago" # setroubleshoot entries
# ── SELinux Booleans ──
getsebool -a | grep httpd # All httpd-related booleans
semanage boolean -l | grep httpd # Booleans with descriptions
# ── SELinux Ports ──
semanage port -l | grep -w 8080 # What type owns port 8080?
# ── AppArmor Status ──
aa-status # All profiles, modes, and confined processes
aa-status --json 2>/dev/null | python3 -m json.tool # Machine-parseable
# ── AppArmor Logs ──
journalctl -k | grep apparmor # Kernel log AppArmor entries
dmesg | grep -i apparmor # Same from dmesg
grep "apparmor=\"DENIED\"" /var/log/syslog # Denied actions (Ubuntu)
Gotcha: File Moved Instead of Copied¶
You mv a file from /tmp to /var/www/html/ and Apache returns 403. SELinux is blocking because the file kept its tmp_t label.
# Diagnose
ls -Z /var/www/html/config.yml
# ... tmp_t ... ← wrong
# Fix
restorecon -v /var/www/html/config.yml
# or for a whole tree
restorecon -Rv /var/www/html/
Prevention: Use cp instead of mv when placing files into labeled directories. cp creates a new inode that inherits the parent directory's context.
Under the hood:
mvwithin the same filesystem is a rename operation — same inode, same extended attributes (including SELinux label).mvacross filesystems is copy-then-delete, which does inherit the target context. This inconsistency is whymvcauses more SELinux problems than any other command.cpalways creates a new inode.
Gotcha: Custom Web Root Outside /var/www¶
You point Nginx to /srv/webapp/ and get 403s even with correct DAC permissions.
# Check what SELinux expects
matchpathcon /srv/webapp/
# /srv/webapp system_u:object_r:var_t:s0 ← wrong type
# Define the correct context for your custom path
semanage fcontext -a -t httpd_sys_content_t "/srv/webapp(/.*)?"
restorecon -Rv /srv/webapp/
# If the app needs to WRITE (uploads, sessions):
semanage fcontext -a -t httpd_sys_rw_content_t "/srv/webapp/uploads(/.*)?"
restorecon -Rv /srv/webapp/uploads/
Gotcha: Service Can't Make Outbound Network Calls¶
Your app behind Apache needs to call a REST API or database, but connections time out or get refused. SELinux blocks httpd_t from initiating network connections by default.
# Check the boolean
getsebool httpd_can_network_connect
# httpd_can_network_connect --> off
# Enable it
setsebool -P httpd_can_network_connect on
# For database-only access (more restrictive):
setsebool -P httpd_can_network_connect_db on
Pattern: Systematic SELinux Denial Resolution¶
When something breaks and you suspect SELinux:
# Step 1: Confirm SELinux is the cause
setenforce 0
# Test again — if it works now, SELinux is the issue
setenforce 1 # PUT IT BACK
# Step 2: Find the denial
ausearch -m AVC -ts recent | head -50
# Step 3: Get human-readable advice (RHEL/CentOS)
sealert -a /var/log/audit/audit.log | head -80
# Step 4: Try the suggested fix (usually a boolean or fcontext)
# sealert often suggests the exact setsebool or semanage command
# Step 5: If no standard fix, generate a custom module
ausearch -m AVC -ts recent | audit2allow -M myapp_fix
cat myapp_fix.te # READ THIS FIRST
semodule -i myapp_fix.pp
# Step 6: Verify
ausearch -m AVC -ts recent # Should show no new denials
Pattern: Adding a Custom SELinux Port¶
Your app binds to port 9443 and SELinux blocks it:
# See what type currently owns 9443
semanage port -l | grep 9443
# (probably nothing)
# Add it to http_port_t
semanage port -a -t http_port_t -p tcp 9443
# If it's already assigned to another type and you need to modify:
semanage port -m -t http_port_t -p tcp 9443
# Verify
semanage port -l | grep 9443
Pattern: Container Volume Mount SELinux Fix¶
Podman/Docker container can't read a bind-mounted volume:
# The fix: add :Z (private relabel) or :z (shared relabel) to the mount
podman run -v /host/data:/container/data:Z myimage
# :Z → relabels for THIS container only (private)
# :z → relabels for shared access across containers
# Manual alternative (if you can't change the run command):
chcon -Rt svirt_sandbox_file_t /host/data/
# Better (persistent):
semanage fcontext -a -t svirt_sandbox_file_t "/host/data(/.*)?"
restorecon -Rv /host/data/
Gotcha: AppArmor Profile Blocks After Package Update¶
A package update changes a binary path or adds new file access. The existing AppArmor profile doesn't cover the new paths.
# Check for denials
grep "apparmor=\"DENIED\"" /var/log/syslog | tail -20
# Put the profile in complain mode temporarily
aa-complain /etc/apparmor.d/usr.sbin.myapp
# Exercise the application (do the thing that was failing)
# Update the profile from logged violations
aa-logprof
# Interactive: it shows each access and asks Allow/Deny/Glob
# Re-enforce
aa-enforce /etc/apparmor.d/usr.sbin.myapp
Pattern: Writing an AppArmor Profile from Scratch¶
# Step 1: Create skeleton
aa-genprof /usr/bin/myapp
# This puts the profile in complain mode and starts scanning
# Step 2: In another terminal, exercise the app fully
# Click every button, trigger every code path
# Step 3: Back in aa-genprof, press S to scan logs
# Answer each prompted access: (A)llow, (D)eny, (G)lob, etc.
# Step 4: Save and enforce
# aa-genprof handles this at the end
# Step 5: Test in enforce mode, iterate with aa-logprof
aa-logprof # picks up any new denials and offers to update the profile
Pattern: Emergency SELinux Recovery on Boot Failure¶
System fails to boot after enabling SELinux (common when switching from disabled):
# At GRUB menu, edit the kernel line and add:
enforcing=0
# This boots in permissive mode. Then:
# Fix the underlying issue (usually a relabel is needed)
touch /.autorelabel
reboot
# The system will relabel every file on next boot
# This takes time — 10 minutes to over an hour on large filesystems
Gotcha: NFS Mounts and SELinux¶
SELinux doesn't label NFS files the same way as local files. NFS mounts get nfs_t by default.
# Option 1: Use a boolean
setsebool -P httpd_use_nfs on
# Option 2: Mount with a specific context
mount -t nfs -o context="system_u:object_r:httpd_sys_content_t:s0" \
nfs-server:/share /var/www/html
Pattern: Auditing What a Profile Actually Allows¶
# SELinux: query the policy
sesearch --allow -s httpd_t | head -40 # What can httpd_t access?
sesearch --allow -t httpd_sys_content_t # What can access httpd_sys_content_t?
# AppArmor: read the profile directly
cat /etc/apparmor.d/usr.sbin.nginx
# AppArmor: parse enforcement data
aa-status --verbose
Pattern: Bulk Boolean Discovery¶
When troubleshooting a service, find all related booleans:
# By service name
semanage boolean -l | grep -i mysql
semanage boolean -l | grep -i httpd
semanage boolean -l | grep -i container
semanage boolean -l | grep -i nfs
# By description keyword
semanage boolean -l | grep -i "home dir"
semanage boolean -l | grep -i "network"
A boolean is almost always the right fix before reaching for audit2allow.
Remember: The SELinux troubleshooting order is: (1) check
restoreconfixes the label, (2) check if a boolean enables the access, (3) check if a port needssemanage port, (4) only then consideraudit2allowfor a custom module. Most production SELinux issues are solved at steps 1-3. Reaching foraudit2allowfirst is like reaching for a compiler when you need a config change.