SELinux and AppArmor: Enforcement Models and Misconfigurations

SELinux and AppArmor: Enforcement Models and Misconfigurations - article cover image Linux Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

SELinux and AppArmor are Linux’s two mainstream Mandatory Access Control (MAC) systems — a layer enforced *in addition to* standard discretionary permissions (owner/group/mode bits) that the kernel checks on every relevant operation, regardless of what the file’s owner allows. Where traditional Unix permissions ask “is this user allowed to touch this file,” MAC asks “is this specific *process, in this specific security context*, allowed to do this specific thing to this specific resource” — and it applies even to root, which is what makes it valuable as a last line of defense against a compromised or malicious process.

The two systems take different philosophical approaches — SELinux labels every subject and object and enforces policy centrally by type, AppArmor attaches a profile per binary path and is comparatively easier to author — but both fail the same way in practice: a policy or profile that is too permissive, or a system left in a non-enforcing mode, provides zero protection while giving administrators false confidence that MAC is “on.” Auditors and attackers alike check enforcement status first, because a MAC system in permissive/complain mode is functionally identical to having none at all — except for the log noise.

Attack Prerequisites

Bypassing or nullifying SELinux/AppArmor protections typically requires one of:

  • Root or CAP_SYS_ADMIN/CAP_MAC_ADMIN, needed to flip enforcement mode (setenforce 0, aa-complain) or load new/broader policy modules.
  • A process already running unconfined — labeled unconfined_t under SELinux, or with no AppArmor profile loaded/attached — so that any code execution inside it inherits no MAC restriction at all.
  • An overly broad policy generated by audit2allow applied without review, effectively whitelisting whatever a service happened to do during a noisy test run.
  • A profile shipped in complain/permissive mode by a distribution package and never switched to enforce.

How It Works

SELinux enforces type enforcement: every process runs with a security *domain* (e.g. httpd_t) and every object — file, socket, device — carries a *type* (e.g. httpd_sys_content_t). The policy is a set of allow rules of the form “domain X may perform action Y on type Z,” and by default everything not explicitly allowed is denied — a default-deny, whitelist model. Labels are stored as extended attributes (security.selinux) and must be kept correct as files move or are created; a mislabeled file (wrong type) is either wrongly denied or, more dangerously, wrongly granted access it should not have.

AppArmor instead attaches a profile per executable path, listing the files, capabilities, and network operations that binary may use, and supports both enforce mode (violations blocked and logged) and complain mode (violations only logged, nothing blocked — commonly used during profile development and, mistakenly, left in production). Because AppArmor is path-based rather than label-based, it is generally easier to author and reason about, but it also means a process that starts under a different, unprofiled path (e.g. exec’d from an unexpected location, or via bash -c reinterpreting the binary) may run entirely unconfined even though a profile exists for the “normal” path.

Both systems share a critical property: they are *policy engines*, not automatic protections. A process the policy grants broad allow rules to — whether from a vendor’s overly generous default policy or from an administrator running audit2allow -a -M mymodule and loading whatever it produced without review — is just as exposed as if MAC were absent, because the kernel is faithfully enforcing exactly what it was told to.

Vulnerable Code / Configuration

The most common enabling misconfiguration is enforcement simply turned off, or a custom policy module generated by blindly running audit2allow against denial logs and loading the result without reading it:

$ getenforce
Permissive              # VULNERABLE: logs denials, blocks nothing

$ ausearch -m avc -ts recent | audit2allow -M httpd_broad
$ semodule -i httpd_broad.pp     # loaded without reviewing the rules
$ semodule -l | grep httpd_broad
httpd_broad
Bash
# Resulting module, if inspected, often contains sweeping grants like:
allow httpd_t unconfined_t:process { transition sigchld };
allow httpd_t admin_home_t:file { read write open getattr };
# The web server domain can now read/write files under user home
# directories it was never intended to touch.
TEXT

The AppArmor equivalent is a profile shipped or edited into complain mode and forgotten:

$ aa-status
apparmor module is loaded.
3 profiles are in enforce mode.
1 profiles are in complain mode.
   /usr/sbin/nginx            # VULNERABLE: logs only, does not block
Bash

Walkthrough / Exploitation

An attacker who already has a foothold in a confined service checks enforcement status first — it determines whether MAC is a real obstacle:

getenforce                 # SELinux: Enforcing / Permissive / Disabled
id -Z                      # current SELinux context
aa-status                  # AppArmor: per-binary enforce/complain state
Bash

If the target domain/profile is permissive or complain, or if the current process is unconfined_t, MAC is not a factor — proceed with normal post-exploitation. If enforcing but over-permissive (the audit2allow-generated module above), the exploit path is simply to use the access the sloppy module already granted:

# Web-shell context, httpd_t domain, with the broad module loaded above
cat /home/appadmin/.ssh/id_rsa   # allowed by admin_home_t grant
ls -la /root/                    # test other newly-granted paths
Bash

Where the attacker has root but SELinux/AppArmor is actively blocking a specific action, disabling enforcement (loudly, but effectively) removes the obstacle outright:

setenforce 0                     # SELinux -> permissive, root required
aa-complain /usr/sbin/nginx       # AppArmor -> complain for one profile
systemctl stop apparmor           # or disable the whole subsystem
Bash

Note: setenforce 0 only survives until reboot or the next setenforce 1; persistent disablement requires editing /etc/selinux/config (SELINUX=disabled) or /etc/selinux/config set to permissive, and for AppArmor, removing the profile from /etc/apparmor.d/ or disabling the apparmor service/kernel parameter — both are far more visible changes than a transient runtime flip.

Opsec: Every AVC denial (even a permissive-mode one that was not enforced) is logged to the audit subsystem with type=AVC, and setenforce/aa-complain calls generate their own audit records — a SIEM rule on type=MAC_STATUS or on enforcement-mode transitions catches this class of tampering even when the follow-on activity looks benign.

Detection and Defense

MAC systems are only protective when enforcing and correctly scoped:

  • Verify getenforce/aa-status return Enforcing across the fleet, not just installed-but-permissive; treat Permissive/Disabled as a finding, not a baseline state.
  • Never load audit2allow output without reading it — trim generated modules to the minimum rules needed and prefer targeted booleans (semanage boolean) over broad custom modules.
  • Alert on type=AVC denials and on type=MAC_STATUS/setenforce events via ausearch -m avc,mac_status, and on AppArmor’s apparmor="STATUS" kernel log lines.
  • Keep file labels correctrestorecon -R after out-of-band file operations, and audit for unconfined_t/unlabeled processes running network-facing services.
  • Pin custom policy/profile changes behind change control and review diffs the same way you would review a firewall rule change.

Real-World Impact

MAC bypass and misconfiguration is a routine finding in Linux hardening assessments and CIS Benchmark audits, and “disable SELinux” remains a widely (and dangerously) repeated troubleshooting step in forum answers and outdated internal runbooks, which is exactly how production systems end up permanently in SELINUX=disabled. Container platforms including OpenShift rely heavily on correctly enforced SELinux to contain container-to-host escapes, making silent permissive-mode drift a meaningful risk in those environments specifically.

Conclusion

SELinux and AppArmor add a default-deny, kernel-enforced layer that survives even a root compromise of discretionary permissions — but only when actually enforcing and only as strict as the policy loaded. Verify enforcement status as a first-class fleet metric, treat every audit2allow-generated module as a draft to be trimmed rather than shipped, and alert on enforcement-mode transitions so a setenforce 0 or aa-complain becomes a detected event rather than a silent hole.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL