Skip to content

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: mv within the same filesystem is a rename operation — same inode, same extended attributes (including SELinux label). mv across filesystems is copy-then-delete, which does inherit the target context. This inconsistency is why mv causes more SELinux problems than any other command. cp always 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 restorecon fixes the label, (2) check if a boolean enables the access, (3) check if a port needs semanage port, (4) only then consider audit2allow for a custom module. Most production SELinux issues are solved at steps 1-3. Reaching for audit2allow first is like reaching for a compiler when you need a config change.