AppArmor and SELinux Denials: Linux MAC Audit Logs
SELinux AVC and AppArmor DENIED records on Linux: where they are logged, how to read them, and how they expose web shells, exploits and disabled enforcement.
- Location
- /var/log/audit/audit.log (with auditd); otherwise kern.log, messages or the journal
- Proves
- Which confined process tried to open, write or execute what and was blocked (or would have been in permissive/complain mode), and when enforcement was switched off
- Timestamps
- msg=audit(epoch.msec:serial) in UTC; kernel log copies carry syslog or journal time
- Access
- root (audit.log mode 0600; kernel log readable by adm or root depending on distro)
- Retention
- Follows auditd rotation (upstream 8 MiB x 5) or syslog/journal retention
- Collection
- UAC, Velociraptor, cp -a, ausearch --raw
Tools
Compare all tools- ausearchCLI · built into Linux
- aureportCLI · built into Linux
- audit2whyCLI · built into Linux
- sealertCLI · built into Linux
- sestatusCLI · built into Linux
- semoduleCLI · built into Linux
- aa-statusCLI · built into Linux
- journalctlCLI · built into Linux
- grep / zgrepCLI · built into Linux
What it is
SELinux and AppArmor are the two mandatory access control (MAC) systems shipped by mainstream distributions. Both confine services to a policy and both report violations through the kernel audit subsystem. For an investigator, the denial records are a free intrusion detection log: a web server process trying to execute a file from its upload directory, a database writing to /tmp, or cupsd reading /etc/shadow are all recorded even when nobody configured auditd rules.
Which one you will find depends on the family: SELinux is enabled and enforcing by default on RHEL, Rocky, Alma and Fedora. AppArmor is enabled by default on Ubuntu, Debian (since 10) and SUSE releases up to SLES 15 and older openSUSE installs; newer openSUSE Tumbleweed installs and SLES 16 default to SELinux. Arch enables neither by default.
Where it lives
| Item | Path | Notes |
|---|---|---|
| Denials with auditd | /var/log/audit/audit.log | type=AVC for both SELinux and AppArmor, type=USER_AVC from user-space managers such as D-Bus or systemd |
| Denials without auditd | /var/log/kern.log or /var/log/syslog (Debian/Ubuntu), /var/log/messages (RHEL, SUSE), journal (_TRANSPORT=audit or kernel) | Shown as audit: type=1400 audit(...) |
| SELinux mode | /etc/selinux/config (`SELINUX=enforcing | permissive |
| SELinux custom modules | /var/lib/selinux/<policy>/active/modules/<priority>/ (priority 400 for semodule -i) | Locally loaded policy |
| setroubleshoot | journal entries "SELinux is preventing ..."; database under /var/lib/setroubleshoot/ | RHEL/Fedora desktops and some servers |
| AppArmor profiles | /etc/apparmor.d/, disabled profiles linked in /etc/apparmor.d/disable/, cache in /var/cache/apparmor/ | Profile flags complain or enforce |
What it proves
- A specific process (
comm,exe,pid) attempted a specific operation on a specific object and was denied, or allowed only because the domain or profile was permissive/complain. - The security context or profile involved, which tells you which service was compromised:
httpd_t,postgresql_t, profile/usr/sbin/nginx. - Enforcement changes:
setenforce 0(MAC_STATUS), policy loads (MAC_POLICY_LOAD), boolean changes (MAC_CONFIG_CHANGE), AppArmor profile replacement or removal (apparmor="STATUS"). - It does not show allowed actions in enforcing mode, and
dontauditrules suppress many expected denials.
Key fields
SELinux AVC from a web shell trying to run a dropped binary:
type=AVC msg=audit(1789874047.512:8812): avc: denied { execute } for pid=4302 comm="sh" name="x" dev="dm-0" ino=393219 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_sys_rw_content_t:s0 tclass=file permissive=0
AppArmor denial for the same kind of event:
type=AVC msg=audit(1789874047.512:231): apparmor="DENIED" operation="exec" class="file" profile="/usr/sbin/nginx" name="/var/www/html/uploads/x" pid=4302 comm="sh" requested_mask="x" denied_mask="x" fsuid=33 ouid=33
| Field | Meaning |
|---|---|
denied { ... } / apparmor="DENIED" | Operation was refused; apparmor="ALLOWED" means complain mode logged but allowed it |
permissive=1 | SELinux domain or system was permissive: the action succeeded |
scontext / profile | Confined subject: the compromised service |
tcontext, tclass / name, class | Target object and its type |
comm, exe, pid | Process name, executable, PID |
name, ino, dev | File name (last component for SELinux) and inode; resolve the full path with the inode on the image |
requested_mask, denied_mask | AppArmor permissions (r, w, x, m, c, d) |
Timestamps
The msg=audit(sec.msec:serial) stamp is Unix epoch UTC, as for all audit records. When records reach the kernel log instead, the same stamp is embedded in the message and the syslog line adds its own local time; prefer the embedded epoch.
Retention
In audit.log, denials share the space with every other audit event and roll over with auditd's num_logs and max_log_file. In the kernel log or journal they follow syslog rotation or journal size limits. Noisy denial loops (a misconfigured service retrying every second) can push older evidence out quickly; the kernel rate-limits audit messages with printk and audit backlog limits, so check for "audit: rate limit exceeded" or "callbacks suppressed" lines.
Collection
# Dead box
tar -C /mnt/evidence -cpf /cases/2026-017/mac.tar var/log/audit var/log/kern.log* var/log/syslog* \
var/log/messages* etc/selinux etc/apparmor.d var/lib/selinux var/lib/setroubleshoot 2>/dev/null
# Live
sestatus; getenforce; semodule -lfull | awk '$1!=100' > custom_modules.txt # SELinux: non-default priorities
aa-status > aa_status.txt # AppArmor: profiles and modes
UAC collects /etc and /var/log, which covers the configuration and the logs. Velociraptor can read /var/log/audit/audit.log with its audit parsing artifacts or grep the kernel log.
Parsing
A=/cases/2026-017/var/log/audit
ausearch -if "$A" -m AVC,USER_AVC -i # all denials, interpreted
ausearch -if "$A" -m AVC -c httpd -i # only those from comm=httpd
ausearch -if "$A" -m MAC_STATUS,MAC_POLICY_LOAD,MAC_CONFIG_CHANGE -i # enforcement tampering
aureport -if "$A" --avc --summary
ausearch -if "$A" -m AVC --raw | audit2why # why SELinux denied it
# AppArmor without auditd (Debian/Ubuntu)
zgrep -h 'apparmor="DENIED"\|apparmor="STATUS"' /cases/2026-017/var/log/kern.log* | sort
journalctl -D /cases/2026-017/var/log/journal -g 'apparmor=|avc:' -o short-iso-precise # all boots
sealert -a audit.log from setroubleshoot produces human-readable explanations but needs the policy of the original host.
Investigator tips
- Denials from
httpd_t,nginx,php-fpm,postgresql_tormysqld_tagainstexecute,execute_no_trans,name_connector writes to/tmpare classic post-exploitation signals. Pivot to the matching time in web server logs and the web root. permissive=1on a production RHEL host, or a profile moved to complain mode, means the control that would have stopped the attacker was off. Find out when: look forMAC_STATUSrecords, edits to/etc/selinux/config, or new links in/etc/apparmor.d/disable/.- A custom SELinux module at priority 400 that nobody on the admin team recognizes can grant an attacker's process rights silently. Extract it with
semodule -E <name>on a live host or read the CIL from/var/lib/selinux/. semodule -DBdisablesdontauditrules and reveals hidden denials during live monitoring; remember to re-enable withsemodule -B.- Absence of any AVC on a host where SELinux is supposedly enforcing is suspicious in itself: check that auditd and the kernel log were running.