Skip to content

Cheat sheet — AppArmor & SELinux

Companion to Module 04 — Exploit Mitigation & Allowlisting · CC BY 4.0 — print it, pin it, share it.

Last reviewed: 2026-07

AppArmor — what's confined?

aa-status                            # profiles loaded, enforce vs complain, confined processes
aa-status --json | jq .             # machine-readable — good for scripting an audit
aa-enabled                           # is AppArmor active in the kernel at all?
sudo aa-unconfined                   # network-facing procs running WITHOUT a profile — the gap
  • The finding that matters: your critical app listed as unconfined. "Installed" ≠ "confining."

Loading and reloading profiles

ls /etc/apparmor.d/                          # profiles live here (named after the binary path)
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.app   # -r = reload (replace) a profile
sudo apparmor_parser -R /etc/apparmor.d/usr.bin.app   # -R = remove/unload
sudo apparmor_parser -p /etc/apparmor.d/usr.bin.app   # -p = preprocess only (syntax-check, no load)
sudo systemctl reload apparmor               # reload all profiles from /etc/apparmor.d/

Complain vs enforce (the safe deployment path)

sudo aa-complain /etc/apparmor.d/usr.bin.app   # log violations, DON'T block — the tuning mode
sudo aa-enforce  /etc/apparmor.d/usr.bin.app   # block AND log — production
sudo aa-disable  /etc/apparmor.d/usr.bin.app   # unload + symlink into disable/ so it stays off
  • Pattern: complain → exercise the app → refine → enforce. Never enforce a fresh hand-written profile blind.

Authoring a profile from real behaviour

sudo aa-genprof /usr/bin/app     # start a profile; run the app in another shell, then (S)can logs
sudo aa-logprof                  # replay new denials into an existing profile, interactively
# in genprof/logprof: (A)llow, (D)eny, (I)nherit, (C)hild, then (S)ave
  • aa-logprof only sees what actually ran — exercise every code path or enforce breaks later.

Reading a denial

sudo dmesg | grep -i apparmor              # kernel denials
sudo journalctl -k | grep -i apparmor      # same via journald
# apparmor="DENIED" operation="open" profile="/usr/bin/app" name="/etc/shadow" requested_mask="r" denied_mask="r"

SELinux — mode and status

getenforce                    # Enforcing | Permissive | Disabled
sestatus                      # full status: current mode, policy, mode from config
sudo setenforce 0             # → Permissive (log only) — NOT persistent across reboot
sudo setenforce 1             # → Enforcing
sudoedit /etc/selinux/config  # SELINUX=enforcing|permissive — the persistent setting

SELinux — contexts and labels

ls -Z /var/www/html                        # -Z shows security context on files
ps -eZ | grep httpd                        # context a process runs in
id -Z                                       # your own context
sudo restorecon -Rv /var/www/html          # relabel to policy default — fixes mislabeled files
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"  # add a persistent label rule
sudo restorecon -Rv /srv/web               # then apply it
getsebool -a | grep httpd                  # list tunable booleans
sudo setsebool -P httpd_can_network_connect on   # -P = persist the boolean

SELinux — troubleshooting a denial

sudo ausearch -m avc -ts recent            # recent AVC (access vector cache) denials
sudo ausearch -m avc -ts recent | audit2allow -w   # -w = plain-English "why was this denied"
sudo ausearch -m avc -ts recent | audit2allow -M myapp   # generate a candidate policy module
# READ the .te it produced, THEN: sudo semodule -i myapp.pp

Gotchas worth remembering

  • Complain mode logs, it does not block. It's for authoring a profile safely, not for running one in production. A profile left in complain thinks it's protecting you and isn't — check aa-status for the mode, not just that the profile loaded.
  • aa-logprof/aa-genprof only capture accesses that actually happened. If you don't exercise every path (error handling, log rotation, that once-a-day cron job), enforce mode will deny a real access weeks later. Drive the app hard before you flip to enforce.
  • setenforce 0 is not persistent. It survives until reboot, then SELinux reads /etc/selinux/config. Edit the config file for a lasting change — and don't leave a host permissive "temporarily" and forget.
  • Never disable SELinux to "fix" a denial. A denial is usually SELinux working correctly. Understand why with audit2allow -w, then write a targeted policy module or set the right boolean — disabling it removes protection for the whole host to unblock one app.
  • restorecon fixes the common case. Most SELinux "it broke" tickets are a mislabeled file (moved with mv instead of cp, so it kept the source context). Check ls -Z and relabel before you reach for policy edits.
  • AppArmor is path-based, SELinux is label-based. Rename or bind-mount a file and an AppArmor rule may no longer match; SELinux follows the label, not the path. Know which one you're running before you reason about a bypass.

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).