Building oversight over confinement: reading and aggregating AppArmor denial logs, using aa-notify for periodic summaries, journald- and auditd-based alerting, and establishing a compliance baseline with per-service profile coverage documentation and periodic security reviews.

In episode 14 you realized AppArmor can be fragile — bugs in the kernel and parser are real threats. The question now: how do you know when confinement starts to break? Denials never read, profiles silently stopping to load, or an attack pattern trying to break into an application — all of that is only visible if there are eyes.
This episode builds those eyes. We'll read denial logs from various sources, aggregate them into comprehensible signals, set up alerting, and close with the compliance side: per-service profile coverage documentation and a security review baseline.
Confinement without monitoring is an alarm without sound. A working deny rule means some process tried to do something forbidden — and that attempt can be a sign of an ongoing exploit. If you don't read denials, you lose AppArmor's most valuable security signal.
Three levels of awareness to build:
AppArmor denials can appear in several places, depending on the system configuration:
journalctl -k shows AppArmor audit messages from the kernel; the fastest way on systems without auditd./var/log/syslog or /var/log/kern.log on systems using rsyslog./var/log/audit/audit.log if the audit daemon is active; the most complete format for compliance needs.All sources store the same events, just with different formats and locations. Start with journald — it's available on almost every modern system:
sudo journalctl -k --since "1 hour ago" --grep=apparmorNotice the event pattern: apparmor="DENIED" followed by the profile name, operation, path, and process name. This is the pattern we'll parse for aggregation.
aa-notify is a tool from apparmor-utils that presents denial summaries without reading raw logs. It reads the logs, counts denials per profile, and shows a list — perfect for periodic reports.
sudo aa-notify -s 86400The -s flag takes denials within a given second range. With -v you get more detail, and with -f you can point it at a specific log file if you want to process a non-default source. On systems with auditd, aa-notify automatically uses /var/log/audit/audit.log; on other systems it reads syslog or journald.
Tip
Schedule aa-notify as a daily job, not a tool only run during incidents. A consistent daily summary lets you recognize "normal" before you can detect "weird".
Raw logs contain thousands of repeating lines. The real value appears after aggregation: which profile gets denied most, which path is hunted most often, and whether a new profile suddenly shows up. A single shell line is enough to sketch the big picture:
sudo journalctl -k --since "24 hours ago" \
| grep 'apparmor="DENIED"' \
| sed -E 's/.*profile="([^"]+)".*/\1/' \
| sort | uniq -c | sort -rn | head -20Read the result with critical questions: why does this profile produce so many denials? Is there deny noise that should be handled with an explicit deny rule (episode 12)? Or is there a sensitive path being probed — a sign of attack? For deeper aggregation, build a pipeline that groups by profile, operation, and path combinations, then store the results as a metric.
A daily summary catches problems after they've happened. For issues needing fast reaction — for example a new profile suddenly blocked en masse, or denials appearing for sensitive paths — set up automated alerting. The simplest approach: a systemd unit that runs aa-notify daily, combined with a service to send the results:
# /etc/systemd/system/aa-deny-digest.service
[Unit]
Description=AppArmor daily denial digest
[Service]
Type=oneshot
ExecStart=/usr/bin/aa-notify -s 86400 -f /var/log/audit/audit.log# /etc/systemd/system/aa-deny-digest.timer
[Unit]
Description=Run AppArmor digest daily
[Timer]
OnCalendar=daily
[Install]
WantedBy=timers.targetFor real-time alerting, enable the audit daemon (auditd) and combine it with a tool that continuously reads audit.log — for example filtering AUDIT_APPARMOR_DENIED events with ausearch or forwarding them to your log pipeline and SIEM. A good alert threshold is usually a combination of two signals: a denial appearing on a sensitive path, or a sudden volume spike on one profile.
sudo ausearch -m 1400 -ts todayWarning
Don't alert on every denial line — you'll drown in notifications and stop paying attention. Alert on anomalies: denials on sensitive paths, a previously quiet profile suddenly busy, or spiking denial volume. Low, consistent denial volume is just part of a normal server's life.
The part often ignored until the audit arrives: documentation. Regulatory frameworks and internal policies almost always ask "which services are confined, with which profile, and in which mode?". Without records, the answer is a reconstruction job under pressure.
Build a simple coverage table updated on every change:
| Service | Profile | Mode | Status | Notes |
|---|---|---|---|---|
| nginx | usr.sbin.nginx | enforce | active | reloaded 2026-07, no regression |
| postgres | usr.lib.postgresql.*.bin.postgres | enforce | active | limited data read-write |
| sshd | usr.sbin.sshd | complain | review | awaiting full scenario tests |
| backup-agent | usr.sbin.backup-agent | enforce | active | data read access + write dir |
This documentation isn't just compliance — it's a map telling you where confinement is weak (complain mode), where testing isn't finished, and which services still have no profile at all.
A baseline is a snapshot of the system's state at a point in time, used as a comparison point in the future. For AppArmor, the most useful snapshot is the aa-status output — the list of profiles, modes, and confined processes:
sudo aa-status > /var/lib/apparmor/aa-status-baseline.txt
sudo aa-status > /tmp/aa-status-now.txt
diff /var/lib/apparmor/aa-status-baseline.txt /tmp/aa-status-now.txtMake this snapshot part of a periodic review. Changes worth questioning: a new profile? A profile changing mode from enforce to complain? A process that used to be confined now running unconfined? All three are markers that something changed — and change without a decision is the beginning of a security regression.
Tip
Store the baseline in version control together with your profiles, and set up a job that diffs aa-status every night against the baseline. An empty diff for weeks is proof of stable confinement — and a diff that suddenly appears is an invitation to investigate.
In this episode you've built full oversight over confinement: reading denial logs from journald, syslog, and auditd; aggregating them into signals with aa-notify and a one-line pipeline; setting up alerting with systemd timers and auditd; and establishing a compliance baseline with a per-service coverage table and regularly diffed aa-status snapshots.
The core takeaways:
aa-notify gives summaries; aggregation gives the big picture; alerting gives fast reaction.aa-status baseline detects regressions early.In episode 16 we zoom out from software to the kernel: where AppArmor sits in the Linux Security Module stack, how it interacts with Yama and Landlock, CONFIG_SECURITY_APPARMOR configuration, and the different layers filled by seccomp, capabilities, and LSMs. See you in episode 16!