SELinux vs AppArmor: Find the Best Linux Protection


SELinux vs AppArmor comparison


SELinux vs AppArmor comparison

Linux provides several security mechanisms to control what applications, services, and users are allowed to do. Two of the most widely used mandatory access control systems are SELinux and AppArmor.

Both can strengthen Linux security by restricting unauthorized access, but they use different approaches and are commonly associated with different Linux distributions. Understanding how SELinux and AppArmor work, where they differ, and when to use each one can help you choose the right security model for your server or application.

Metric Value Source Relevance
LSM hooks covered 217 (SELinux) vs 80 (AppArmor) apparmor.net, Linux v6.19 Shows mediation depth gap between the two frameworks
Default distro split RHEL/Rocky/Fedora = SELinux, Ubuntu/Debian/SUSE = AppArmor TechTarget Tells you which one you already have running today
Container isolation model SELinux supports MCS separation, AppArmor does not Red Hat blog Matters directly for multi-tenant Docker or Podman hosts
Rule model Label based vs path based FDC Servers Drives how you write, test, and debug policy

What This Guide Covers:

  • How SELinux labels and AppArmor paths actually differ under the hood
  • A side by side comparison you can use to justify a decision to a team lead
  • Setup steps across Ubuntu, Rocky/RHEL/AlmaLinux, Fedora, and Arch
  • Six real production failures and the exact fix for each
  • A validation script that tells you PASS or FAIL, not "looks fine"
  • A monitoring cadence you can hand to a junior sysadmin without babysitting it

Both frameworks do the same job on paper. They confine what a process can touch even after it has root, using Mandatory Access Control instead of the discretionary permissions you already know from chmod. Where they split is enforcement model, tooling maturity, and how forgiving they are when you get a rule wrong.

This guide treats both as production tools, not as an academic comparison. Expect real commands, real failure modes, and an honest answer on which one to run depending on what distro you're already standing on.

Use this table the way you'd use it in an architecture review. If you're inheriting a fleet that's already Ubuntu or already RHEL, most of this decision has already been made for you. This is mainly useful for greenfield builds or when you're deciding whether to fight the default.

Category SELinux AppArmor Winner Notes
Policy model Label based, type enforcement Path based profiles SELinux Labels survive file renames, paths do not
Learning curve Steep, real learning investment Readable profiles, easier to reason about AppArmor A junior engineer can read an AppArmor profile in minutes
Container isolation MCS separates containers by default No container to container separation SELinux Big deal for multi-tenant Kubernetes nodes
Debugging tooling audit2allow, sealert, setroubleshoot aa-genprof, aa-logprof Tie Both are workable once you know the workflow
Runtime overhead Slightly higher on label heavy workloads Lower, fewer LSM hook crossings AppArmor Difference is small on modern hardware, still measurable at scale

Tip:

Don't fight the distro default unless you have a specific reason. Running SELinux on Ubuntu or AppArmor on RHEL means you lose vendor tested policies and inherit a maintenance burden nobody on your team signed up for.

Environment and Prerequisites

# Environment / Distro Type
1 Ubuntu 24.04 / 26.04 LTS AppArmor default
2 Rocky Linux 9 / 10, RHEL 9 / 10, AlmaLinux 9 / 10 SELinux default
3 Fedora 44 SELinux default, enforcing out of the box
4 Arch Linux Neither installed by default, manual setup required
5 Root or sudo access Required for all steps below
Requirement Details Status
Kernel with LSM support Ships enabled on every mainstream distro for well over a decade REQUIRED
policycoreutils-python-utils / apparmor-utils Provides semanage, audit2allow, aa-genprof, aa-logprof REQUIRED
auditd Needed to capture AVC denials for review OPTIONAL
Staging environment Test enforcing mode before touching production REQUIRED

Warning:

Never flip a production node straight from disabled to enforcing without a staging test first. A single missing rule for a background cron job or a mail relay can silently break a workflow nobody checks until end of month.

If you're building this on a bare VPS rather than a managed platform, pair this with our server hardening checklist before you go further, and confirm sudo access is scoped correctly using our sudo configuration guide.

Architecture Overview

Both frameworks sit at the same layer of the kernel, they just answer a different question when a process asks for access.

MAC ENFORCEMENT PATH

  USER SPACE PROCESS
        |
        v
  +----------------------------+
  |   SYSTEM CALL (open/exec)  |
  +----------------------------+
        |
        v
  +----------------------------+
  |   LSM HOOK IN KERNEL       |
  +----------------------------+
        |
   -----+-----
   |         |
   v         v
 SELinux   AppArmor
 label:    path:
 checks    matches
 context   profile
   |         |
   v         v
 ALLOW / DENY -> logged to audit.log or dmesg

  key ports/paths referenced by policy:
  /etc/selinux/config   /etc/apparmor.d/
  semanage port -l      aa-status

Engineer note:

Both systems only mediate what the kernel already intercepts. If a process talks directly to hardware through a path neither framework watches, MAC will not save you. This is why MAC always sits alongside firewalling and patching, never in place of them.

Step-by-Step Setup

Step 1 - Check What You're Already Running

Don't assume. Confirm which framework is active before you change anything.

Ubuntu 24.04 / 26.04 LTS

bash
LinuxTeck.com
# Check AppArmor status and loaded profiles
sudo aa-status
OUTPUT
apparmor module is loaded.
34 profiles are loaded.
29 profiles are in enforce mode.
5 profiles are in complain mode.
12 processes have profiles defined.

Rocky Linux 9 / 10 - RHEL 9 / 10 - AlmaLinux 9 / 10

bash
LinuxTeck.com
# Check current SELinux mode and full status
getenforce
sestatus
OUTPUT
Enforcing
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
Current mode: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed

Step 2 - Install the Toolchain on Arch

Arch ships with neither framework by default, so this is the one distro where you're building from zero.

Arch Linux

bash
LinuxTeck.com
# AppArmor is the simpler path on Arch, install and enable it
sudo pacman -S apparmor
sudo systemctl enable --now apparmor
OUTPUT
Created symlink /etc/systemd/system/multi-user.target.wants/apparmor.service
Active: active (exited) since boot

Installing the package is not enough on Arch's default kernel. AppArmor has to be listed as an active LSM at boot, so add it to the kernel command line and rebuild GRUB before it will actually mediate anything.

Arch Linux

bash
LinuxTeck.com
# Add apparmor to the active LSM list, rebuild GRUB, then reboot to apply
sudo nano /etc/default/grub
# Append lsm=landlock,lockdown,yama,apparmor,bpf inside GRUB_CMDLINE_LINUX_DEFAULT
sudo grub-mkconfig -o /boot/grub/grub.cfg
sudo reboot
OUTPUT
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-linux
Found initrd image: /boot/initramfs-linux.img
done

Step 3 - Put SELinux in Permissive Before Enforcing

On Rocky, RHEL, and Fedora, permissive mode logs denials without blocking them, giving you a safe window to catch surprises.

Rocky Linux 9 / 10 - RHEL 9 / 10 - Fedora 44

bash
LinuxTeck.com
# Switch running mode immediately, then persist it across reboots
sudo setenforce 0
sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config
OUTPUT
Mode changed to permissive. Config file updated, will persist after reboot.

Warning:

If the box was previously running with SELINUX=disabled, moving to permissive or enforcing needs a full filesystem relabel first. Run sudo touch /.autorelabel and reboot before you touch anything else, or services can fail to start on the next boot. Also worth knowing: upstream is in the process of deprecating runtime SELinux disable through /etc/selinux/config entirely, so on newer kernels the supported way to fully disable it going forward is the selinux=0 GRUB kernel parameter, not the config file.

Step 4 - Build and Load a Real Profile

On Ubuntu, aa-genprof watches an application and builds a profile from its actual behavior instead of you writing rules blind.

Ubuntu 24.04 / 26.04 LTS

bash
LinuxTeck.com
# Generate a profile by watching the running binary
sudo aa-genprof /usr/sbin/nginx
OUTPUT
Writing updated profile for /usr/sbin/nginx.
Finished generating profile for /usr/sbin/nginx.

Step 5 - Set the SELinux Context on Custom Paths

Moving a web root outside the standard path is the number one cause of unexplained 403s on RHEL family systems.

Rocky Linux 9 / 10 - RHEL 9 / 10 - AlmaLinux 9 / 10

bash
LinuxTeck.com
# Label a custom app directory as web content and apply it
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/app(/.*)?"
sudo restorecon -Rv /srv/app
OUTPUT
restorecon reset /srv/app context unconfined_u:object_r:var_t:s0->unconfined_u:object_r:httpd_sys_content_t:s0
restorecon reset /srv/app/index.html context unconfined_u:object_r:var_t:s0->unconfined_u:object_r:httpd_sys_content_t:s0

Production Pitfalls and Fixes

Issue 01
Silent Permissive Mode on RHEL Family Servers

Environment: Rocky Linux 9, RHEL 9, AlmaLinux 9, discovered during a compliance audit.

Failure pattern: Someone runs setenforce 0 during a deploy to unblock a stuck task and never flips it back. It survives for months because getenforce is rarely checked outside of an audit.

bash
LinuxTeck.com
# Force back to enforcing and confirm
sudo setenforce 1
getenforce
OUTPUT
Enforcing
Issue 02
AppArmor Profile in Complain Mode Forever

Environment: Ubuntu 24.04 LTS, Docker host running a custom API service.

Failure pattern: A profile gets left in complain mode during initial testing and nobody promotes it to enforce, so the protection provides zero blocking, only logging that nobody reads.

bash
LinuxTeck.com
# Promote the profile from complain to enforce
sudo aa-enforce /etc/apparmor.d/usr.bin.myapi
OUTPUT
Setting /usr/bin/myapi to enforce mode.
Issue 03
Wrong SELinux Port Label Blocks a Custom Listener

Environment: RHEL 9, application team moved a web service from port 80 to 8088.

Failure pattern: The firewall opens fine, the service starts fine, but SELinux blocks the bind because port 8088 was never associated with http_port_t. The service crashes with a permission denied that looks like a code bug.

bash
LinuxTeck.com
# Register the new port under the correct type
sudo semanage port -a -t http_port_t -p tcp 8088
OUTPUT
Port 8088/tcp added under type http_port_t.
Issue 04
Container Escape Risk From a Missing AppArmor Profile

Environment: Ubuntu 24.04, Docker 26, multi-tenant CI runner.

Failure pattern: Docker's default AppArmor profile is broad by design and does not separate containers from each other the way SELinux MCS does. A compromised build job on one tenant's container had a much wider blast radius than the team expected.

bash
LinuxTeck.com
# Load or reload the strict profile first, docker won't do this for you
sudo apparmor_parser -r -W /etc/apparmor.d/docker-strict
# Run with a stricter custom profile instead of the loose default
docker run --security-opt apparmor=docker-strict -d myimage
OUTPUT
Profile /etc/apparmor.d/docker-strict loaded.
a3f9d21e8b4c... container started with profile docker-strict
Issue 05
Backup Script Silently Fails Under SELinux

Environment: AlmaLinux 9, nightly cron backup writing to a mounted NFS share.

Failure pattern: Cron exits with status 0 even though the copy fails partway through, because the AVC denial happens mid write and the script does not check for it. Nobody noticed until a restore test failed.

bash
LinuxTeck.com
# Persist the boolean that allows read/write access to NFS-exported shares
sudo setsebool -P nfs_export_all_rw 1
OUTPUT
Boolean nfs_export_all_rw set to on, persisted.
Issue 06
Overly Broad Custom Policy Defeats the Purpose

Environment: Any distro, junior engineer under deadline pressure.

Failure pattern: Instead of fixing the specific rule that's blocking an app, someone runs audit2allow -a -M mypol against a huge denial log and installs a policy that grants far more than the app needs.

bash
LinuxTeck.com
# Scope the policy to just this app's denials, not the whole log
grep myapp /var/log/audit/audit.log | audit2allow -M myapp-scoped
OUTPUT
Wrote myapp-scoped.pp and myapp-scoped.te, 3 rules, scoped to myapp_t only.

Post-Action Validation Script

Run this after any change to either framework. It checks status, mode, and recent denials in one pass instead of you jumping between five commands.

bash
LinuxTeck.com
#!/bin/bash
LSM_LIST=$(cat /sys/kernel/security/lsm 2>/dev/null)

if echo "$LSM_LIST" | grep -qw selinux; then
mode=$(getenforce)
if [ $mode = "Enforcing" ]; then
echo "PASS: SELinux is enforcing"
else
echo "FAIL: SELinux mode is $mode"
fi
elif echo "$LSM_LIST" | grep -qw apparmor; then
if sudo aa-status --enabled 2>/dev/null; then
ENFORCED=$(sudo aa-status --json 2>/dev/null | jq '[.profiles[] | select(. == "enforce")] | length')
echo "PASS: AppArmor active ($ENFORCED profiles enforced)"
else
echo "FAIL: AppArmor is not active"
fi
else
echo "FAIL: no MAC framework active in /sys/kernel/security/lsm"
fi

OUTPUT
PASS: SELinux is enforcing

Usage:

Save this as mac-check.sh, wire it into your post-deploy hook, and fail the pipeline on a FAIL result instead of catching it during a manual audit weeks later.

The AppArmor branch now checks actual enforced profile counts with aa-status --json, so a profile stuck in complain mode won't slip through as a false PASS. This requires jq to be installed on the host running the check.

Final Verdict and Best Practices

If you're already on RHEL, Rocky, AlmaLinux, or Fedora, stay on SELinux. The container isolation and audit depth are worth the steeper learning curve, especially once compliance is part of the conversation. If you're on Ubuntu, Debian, or SUSE, AppArmor is the right default and fighting it rarely pays off.

A short list of habits that separate teams that get burned from teams that don't: never leave a box in permissive or complain mode past a staging test, scope every custom policy to the exact denial instead of the whole log, and run the validation script from Section 07 as part of your normal deploy pipeline instead of as a one-off audit task.

If you'd rather not own this layer yourself, that's a legitimate call too. Managed platforms often ship a hardened MAC baseline out of the box, which is worth weighing against the setup time above. Our Cloudways review breaks down what comes pre-hardened on a managed stack versus what you'd still need to configure yourself on a bare VPS.

Security and Compliance

CIS Benchmark Aligned
PCI DSS Relevant
SOC 2 Control Support

Rocky Linux 9 / 10 - RHEL 9 / 10

bash
LinuxTeck.com
# Pull recent AVC denials for a compliance review
sudo ausearch -m avc -ts recent
OUTPUT
type=AVC msg=audit(1770000012.123:456): avc: denied { write } for pid=1842 comm="myapp" name="app.log" scontext=system_u:system_r:myapp_t tcontext=system_u:object_r:var_log_t

Ubuntu 24.04 / 26.04 LTS

bash
LinuxTeck.com
# Pull kernel log entries related to AppArmor denials
sudo journalctl -k | grep -i apparmor | grep -i denied
OUTPUT
audit: type=1400 audit(1770000210.500:88): apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/etc/shadow"

Compliance Auditing Note:

For CIS Benchmark or SOC 2 reporting, forward these AVC and AppArmor denial events straight to your centralized log collector or SIEM. A clean 30 day window with zero unexplained denials is typically the bar external auditors expect to see.

Whichever framework you're running, tie the denial log into whatever you already use for alerting. If you haven't picked a stack for that yet, our roundup of Linux monitoring tools and the broader Linux security tools list are a good starting point. It's also worth checking your policy against current attack patterns in our Linux security threats overview. For the official kernel side of this, the semanage man page covers every option this guide touched on.

Monitoring and Maintenance Checklist

Neither framework is set and forget. Denials pile up quietly, and policy drift is real once more than one person touches the box.

On Alert:

  • Check getenforce or aa-status the moment an app throws an unexplained permission error
  • Pull the last hour of AVC or AppArmor denials before assuming it's a code bug

Weekly:

  • Review /var/log/audit/audit.log or the AppArmor journal for new denial patterns
  • Confirm no profile was left in complain mode after a deploy

Monthly:

  • Diff current policy against the last known good baseline
  • Run the validation script from Section 07 across the whole fleet, not just one host

Quarterly:

  • Re-run the full audit checklist against your compliance framework
  • Revisit any custom policy written under deadline pressure and tighten scope where possible
  • Re-evaluate custom policy modules against major OS releases and distribution updates
  • Audit custom port bindings with semanage port -l and prune deprecated service entries

Questions I Get Asked About This All the Time

My app works fine until I put SELinux in enforcing mode. What's the fastest way to find out why?
Run sudo ausearch -m avc -ts recent right after the failure and look for the denied action. Nine times out of ten it's a context that never got labeled after a file move.
Can I run AppArmor on RHEL or SELinux on Ubuntu?
Technically yes for SELinux on Ubuntu, but you lose the vendor-tuned policies and inherit a lot of manual work. Installing AppArmor on RHEL is not recommended since the distro's tooling and repos are built around SELinux.
Is it safe to just disable SELinux entirely when something breaks?
It's safe in the sense that the error goes away, but it's not a fix. You're removing an entire layer of defense instead of correcting one label or boolean. Use permissive mode temporarily instead, and set it back to enforcing once you've found the rule.
Why does my app work after a fresh install but break after I move the data directory?
SELinux labels are tied to the path at the time the file was created or copied. Moving files with mv across filesystems, or into a directory with a different default context, leaves the wrong label behind. Run restorecon -Rv on the new path.
Does AppArmor protect Docker containers by default?
Docker applies a default AppArmor profile automatically on Ubuntu and Debian hosts, but that profile is intentionally loose to avoid breaking common images. For real isolation between tenants, write a scoped profile per image instead of relying on the default.
How do I know if a custom SELinux policy I installed is too permissive?
Check the generated .te file from audit2allow line by line. If it grants access to types the app has no business touching, like shadow_t or unrelated log directories, scope it down manually before loading it.
What is the actual performance cost of running either of these in production?
On modern hardware it's usually low single digit percent, and AppArmor tends to be a bit lighter because it evaluates fewer LSM hooks. The overhead is rarely the deciding factor. The security depth you need should drive the choice, not raw CPU cost.
I inherited a server and don't know if MAC is even configured. Where do I start?
Run the Step 0 commands from this guide first. Then run the validation script in Section 07 and treat any FAIL result as your first priority before you touch anything else on that box.

Conclusion

Neither framework is objectively better. SELinux gives you 217 LSM hooks of coverage and real container separation, which matters if you're running RHEL, Rocky, or Fedora and handling anything close to regulated data. AppArmor gives you a profile you can actually read in five minutes, which matters just as much when the person maintaining it isn't a security specialist.

The long-term trend is convergence, not divergence. AppArmor's maintainers have said publicly they're actively working to close the mediation coverage gap, and SELinux tooling keeps getting friendlier with each release. We wouldn't put a specific number on how fast that gap is closing, but the direction is clear enough that it shouldn't be the deciding factor in your choice today.

Where the tools are converging fastest is container tooling. Both frameworks now ship with better default Docker and Podman integration than they did even two years ago, and the gap in day to day usability for containerized workloads is smaller than the raw feature comparison suggests.

If you're standing up a new fleet, don't overthink it, stick with whatever your distro ships and get comfortable with its tooling before you consider switching. Pair this with our guide on RHEL vs Ubuntu Server if you're still choosing a base distro, our firewalld command reference to close the loop on network level hardening, and our broader Linux system administration guide if you're building this hardening work into a bigger playbook. Today's action item: run Step 0 from this guide on one production box and confirm you actually know what mode it's in.

LinuxTeck : A Practical Guide to Real Protection
This guide walked through how SELinux and AppArmor differ in architecture, setup, and real production failure modes across Ubuntu, Rocky Linux, RHEL, Fedora, and Arch.
LinuxTeck's Enterprise Linux category focuses on production-ready Linux skills including:
SELinux, AppArmor, Mandatory Access Control,
Linux hardening, container security, and compliance auditing.

About Aneeshya S

Aneeshya S is a Senior Linux Trainer and System Administrator with over 10 years of experience. She actively follows emerging technologies and industry trends. Outside the terminal, she enjoys music and travel.

View all posts by Aneeshya S →

Leave a Reply

Your email address will not be published.

L