How To Harden a Linux Server with AppArmor on Ubuntu 22.04
Tricknowtech Team 13 min read
Goal
By the end, the reader can confirm AppArmor is active, distinguish enforce from complain mode, inspect the profiles Ubuntu ships, safely test a profile against live traffic in complain mode, read AppArmor's denial log entries, refine a profile with aa-logprof, and switch a validated profile into enforce mode with aa-enforce.
Prerequisites
An Ubuntu 22.04 LTS server, accessible over SSH as a non-root user with sudo privileges.
Basic comfort with the command line and reading structured log output.
Familiarity with systemd service management (systemctl status, systemctl reload) is helpful but not required.
Optional: a running service with a shipped AppArmor profile to practice against (this tutorial uses MariaDB as an example).
Let an AI agent do this for you
Copy a ready-made prompt for an AI coding assistant with terminal access to your server (Claude Code, Cursor, or similar) — it can carry out the steps below for you. Review what it plans to run before it executes anything.
Most Linux hardening advice focuses on the network edge: firewalls, SSH configuration, fail2ban. AppArmor works at a different layer entirely. It's a Linux Security Module (LSM) that confines individual programs to the specific files, network operations, and capabilities they actually need, regardless of what user account they're running as. A compromised web server process confined by AppArmor can't read your SSH keys or write to arbitrary system directories, even if the attacker gets code execution inside it and even if that process is technically running as root.
The good news for Ubuntu users is that you're not starting from zero. AppArmor ships enabled by default on every stock Ubuntu install, and several common daemons already come with confinement profiles out of the box. Most administrators never interact with it because it's silent when everything is working normally. This tutorial walks through the parts you do need to know: confirming AppArmor is actually protecting your system, understanding the difference between its enforce and complain modes, inspecting the profiles already on disk, safely testing a profile against real traffic before you tighten it, reading AppArmor's denial logs, and finally switching a validated profile into full enforcement. None of this requires writing a profile from scratch — you're learning to operate the tooling Ubuntu already gives you.
Prerequisites
An Ubuntu 22.04 LTS server, accessible over SSH as a non-root user with sudo privileges.
Basic comfort with the command line and reading structured log output.
Familiarity with systemd service management (`systemctl status`, `systemctl reload`) is helpful but not required.
Optional: a service that ships its own AppArmor profile, to practice the complain-mode workflow against something real. This tutorial uses MariaDB as a running example — install it with `sudo apt install mariadb-server` if you want to follow along exactly — but you can substitute any confined service already present on your system.
Step 1 — Verify AppArmor Is Active
Before touching any profile, confirm the AppArmor kernel module itself is loaded and enabled. This is a separate question from whether any individual program is confined — it's just checking that the security framework is switched on at all.
Tricknowtech VPS Hosting
Dedicated KVM resources and full root access — deployed in under 60 seconds, no ticket required.
A response of `Y` confirms the module is active. Next, check the systemd unit that loads profiles at boot:
bash
sudo systemctl status apparmor
Seeing `active (exited)` is normal and expected — `apparmor.service` is a one-shot unit that parses and loads every profile in `/etc/apparmor.d/` during boot, then exits; it doesn't need to keep running as a daemon afterward. Finally, get the full picture with the status tool itself:
bash
sudo aa-status
`aa-status` (a symlink to `apparmor_status`) is the single most useful AppArmor command — it prints how many profiles are loaded, which ones are in enforce mode, which are in complain mode, and which running processes are currently confined or unconfined.
“AppArmor being "active" does not mean every process on the system is confined. Only binaries that ship a matching profile are restricted — everything else runs completely unconfined, exactly as it would if AppArmor weren't installed at all.”
Step 2 — Understand Enforce Mode vs. Complain Mode
Every loaded AppArmor profile is in one of two modes. Enforce mode is the real thing: the kernel actively blocks any file access, network operation, or capability request that the profile doesn't explicitly permit, and logs the denial. Complain mode is a dry run: the kernel logs exactly what it would have blocked, but lets the action through anyway, so the confined program keeps working normally while you observe what its profile does and doesn't cover.
This is the mechanism that makes AppArmor safe to iterate on in production. Instead of guessing whether a profile matches your actual workload and risking an outage if it doesn't, you flip it to complain mode, let real traffic hit it for a while, review what got flagged, and only switch to enforce once you're confident nothing legitimate will be blocked. `aa-status` output makes the current mode of every profile explicit, grouped into sections like this:
text
apparmor module is loaded.
50 profiles are loaded.
30 profiles are in enforce mode.
/usr/sbin/mariadbd
/usr/sbin/cupsd
...
20 profiles are in complain mode.
/usr/bin/tcpdump
0 processes have profiles defined.
0 processes are in enforce mode.
0 processes are in complain mode.
0 processes are unconfined but have a profile defined.
The distinction to keep in mind for the rest of this tutorial: only enforce mode actually protects anything. Complain mode is purely for testing and observation.
Step 3 — List Available and Loaded Profiles
Profile source files live in `/etc/apparmor.d/`. Each file is named after the absolute path of the binary it confines, with slashes replaced by dots — a profile confining `/usr/sbin/mariadbd` is stored as `/etc/apparmor.d/usr.sbin.mariadbd`.
bash
ls /etc/apparmor.d/
That directory shows what's available on disk. To see what the kernel has actually loaded right now, along with each one's current mode, query the securityfs interface directly:
bash
sudo cat /sys/kernel/security/apparmor/profiles
A profile can also be fully disabled rather than just set to complain mode — disabled profiles are represented as symlinks in a separate directory:
bash
ls /etc/apparmor.d/disable/
Anything symlinked there is skipped entirely at load time (no confinement, no logging). You won't need this directory for the complain-to-enforce workflow in this tutorial, but it's useful to know it exists if you ever need to fully turn off a specific profile with `aa-disable`.
Step 4 — Install the AppArmor Utilities and Extra Profiles
The kernel-level AppArmor support is built into Ubuntu already, but the command-line tools for managing profiles — `aa-complain`, `aa-enforce`, `aa-disable`, `aa-logprof`, `aa-genprof`, `aa-notify` — live in a separate package, along with an optional bundle of extra ready-made profiles for common utilities that don't ship one by default.
`apparmor-utils` is what you'll use for the rest of this tutorial. `apparmor-profiles` is optional — install it if you want broader out-of-the-box coverage for common command-line tools.
“Installing new profile files doesn't load them into the kernel by itself. After adding or editing anything in /etc/apparmor.d/, reload the profile set with `sudo systemctl reload apparmor` so the changes actually take effect.”
Step 5 — Put a Profile Into Complain Mode
Pick a profile backing a service you actually run — check the enforce-mode list from `aa-status` and copy the exact path it prints. This example uses MariaDB's profile; substitute your own service's path throughout.
bash
sudo aa-complain /usr/sbin/mariadbd
`aa-complain` accepts either the full path to the confined binary or the path to its profile file under `/etc/apparmor.d/` — both resolve to the same profile. Confirm the switch took effect:
bash
sudo aa-status
The profile should now appear under the "profiles are in complain mode" section instead of the enforce section. The service itself keeps running exactly as before — nothing is blocked in this mode, so there's no risk of an outage from making this change.
“Complain mode is a testing tool, not a hardening setting. A profile left in complain mode indefinitely provides visibility but zero actual protection — anything the profile would otherwise block is allowed through and merely logged.”
Step 6 — Generate Activity and Review Denials in the Logs
With the profile in complain mode, exercise the service the way it's normally used in production — real client connections, normal configuration reloads, scheduled jobs, backups, whatever touches it day to day. Anything the current profile doesn't already permit gets written to the kernel audit log instead of being silently blocked.
On systems still running rsyslog alongside journald, the same messages also land in the traditional syslog file:
bash
sudo grep apparmor /var/log/syslog
A denial-style entry looks roughly like this — the important fields are `operation` (what the process tried to do), `name` (the exact file or resource involved), and `requested_mask`/`denied_mask` (what kind of access was attempted):
In complain mode you'll see `apparmor="ALLOWED"` even on lines that represent a would-be violation — the action was let through, but flagged because it fell outside the profile's rules. Under enforce mode the same violation instead shows `apparmor="DENIED"` and the operation is actually blocked. Each `name=` value is your evidence for deciding whether to fix the service's configuration (it's reaching somewhere it shouldn't) or extend the profile (the access is legitimate and just isn't covered yet).
Step 7 — Refine the Profile with aa-logprof
Rather than hand-editing the profile file for every log line you found, let AppArmor's own tooling walk you through it. `aa-logprof` scans the audit log for events tied to profiles currently in complain mode and presents each new access interactively.
bash
sudo aa-logprof
For each flagged access it shows you the operation and path, and offers choices like Allow, Deny, or a glob pattern covering a whole directory, along with the scope (just this exact path, or a wildcard). Accepting an option writes the corresponding rule into the profile file under `/etc/apparmor.d/` and reloads it automatically. If your test run produced no denials at all, that's a good sign too — it means the existing profile already matches real usage and you can skip straight to enforcing it.
Step 8 — Get Denial Summaries with aa-notify
For a quick recap instead of scrolling through raw log output, `aa-notify` can summarize recent AppArmor activity on demand.
bash
sudo aa-notify -s 1 -v
This prints a verbose summary of every AppArmor message logged in the last day, grouped by profile. `aa-notify` also has a polling mode (`-p`) that pushes desktop notifications as denials happen, which is more useful on a workstation with a notification daemon than on a headless server — for server use, the `journalctl`/`syslog` review from Step 6 remains the primary workflow, with `aa-notify -s` as a convenient recap.
Step 9 — Switch the Tested Profile to Enforce Mode
Once the profile has run in complain mode against real traffic for a reasonable period — long enough to cover your normal operational cycle, including things like log rotation or nightly backups — and any legitimate accesses have been folded in via `aa-logprof`, it's time to actually enforce it.
bash
sudo aa-enforce /usr/sbin/mariadbd
Confirm the switch the same way as before:
bash
sudo aa-status
The profile should now be listed under the enforce section. This mode change persists across reboots on its own — `aa-complain` and `aa-enforce` track state as symlinks under `/etc/apparmor.d/force-complain/`, which `apparmor.service` reads on every boot, so you don't need to do anything extra to make it stick. Watch the logs for a short window immediately after enforcing, since that's when any access you missed during testing will surface as a real, blocked `apparmor="DENIED"` event instead of just a logged warning.
“Never flip a profile straight from its default state to enforce mode in production without going through complain mode first. An overly strict enforced profile fails silently from the application's point of view — file opens and syscalls just get refused — and the resulting error often looks nothing like "AppArmor blocked this," which makes it painful to diagnose without already knowing to check `aa-status` and the audit log.”
Conclusion
AppArmor is already running on your Ubuntu server, protecting whatever it ships profiles for, whether or not you've ever looked at it. In this tutorial you confirmed it was active, learned to tell enforce mode apart from complain mode, inspected the profile files on disk versus what the kernel had actually loaded, and installed the userspace tooling needed to manage them. You then walked a real profile through the safe testing loop that AppArmor is built around: drop it into complain mode, generate genuine traffic against it, read the resulting denials out of the kernel log, fold in any legitimate accesses with `aa-logprof`, and only then commit to enforce mode with `aa-enforce`. Apply that same loop to any other confined service on your system, and revisit it whenever you change how that service is configured or where it stores its data — a profile that was correct yesterday can start generating denials the moment you point a service at a new file path.