SELinux and AppArmor
Linux Security Fundamentals introduced Mandatory Access Control (MAC) as a policy layer that constrains a process regardless of what its DAC permissions allow. File Permission Security already showed the basic diagnostic commands and how to fix a mislabeled SELinux context or check an AppArmor profile's mode, in the context of tracking down a "permission denied despite correct file permissions" problem. This article goes further into each system on its own terms: how policy is actually structured, how to change enforcement modes safely, how to build a new AppArmor profile from scratch, and how to let SELinux itself suggest the fix for a denial instead of interpreting raw audit records by hand.
What You Will Learn
By the end of this article you will be able to:
- explain SELinux's enforcing/permissive/disabled modes and switch between them safely;
- read an SELinux context and toggle a boolean to adjust policy without editing the policy itself;
- use
audit2allowto turn a denial into a reviewable policy module; - build a new AppArmor profile for an application with
aa-genprof/aa-logprof; - decide, for a given denial, whether the correct fix is a context change, a boolean, or a real policy exception.
SELinux: Modes and the Boot-Time Switch
SELinux runs in one of three modes, checked with getenforce:
| Mode | Behavior |
|---|---|
Enforcing |
Denials are blocked and logged |
Permissive |
Denials are logged only — nothing is actually blocked |
Disabled |
SELinux is off entirely; no labels, no checks, no logging |
setenforce 0/setenforce 1 toggles between Enforcing and Permissive immediately, but the change doesn't survive a reboot — it's meant for temporary troubleshooting, not policy:
A persistent change goes in /etc/selinux/config:
SELINUXTYPE=targeted is the default policy on nearly every RHEL-family install: it confines a specific, curated list of network-facing daemons (httpd, sshd, named, and others) while leaving most other processes unconfined. The far less common mls type applies full multi-level security labeling to the entire system and is reserved for environments with a formal classification requirement.
Switching to or from Disabled requires a relabel
Changing SELINUX= to or from disabled in /etc/selinux/config does not take effect until reboot, and going from disabled back to enforcing requires every file on the system to be relabeled on that next boot — a process that can take a long time on a large filesystem and that the kernel triggers automatically once it detects the mismatch (touch /.autorelabel forces it manually). Never set SELINUX=disabled as a quick fix for a denial; use Permissive instead, which keeps labeling intact and only suspends enforcement.
Reading a Context and the DAC/MAC Distinction
An SELinux context has four parts — user, role, type, and sensitivity — though in day-to-day administration, the type is almost always the one that matters:
httpd_sys_content_t is the type; the policy defines which types the httpd_t process type (what Apache itself runs as) is permitted to read, write, or execute. This is why moving a file into a web root from a home directory can produce Permission denied even with correct rwx bits: the file inherits user_home_t from its source location, and no rule in the targeted policy grants httpd_t read access to user_home_t. File Permission Security walks through diagnosing and fixing exactly this case with restorecon and semanage fcontext — that fix applies here unchanged.
Booleans: Adjusting Policy Without Editing It
Many common policy trade-offs are exposed as booleans — named on/off switches the policy author already anticipated, rather than requiring a custom rule:
httpd_can_network_connect off is why a correctly labeled, correctly permissioned PHP application can still fail to reach a remote API or a database on another host — SELinux's targeted policy denies outbound network connections from httpd_t by default, on the reasoning that most web content is static and a compromised web app making arbitrary outbound connections is itself a signal worth blocking.
-P makes the change persistent across reboots; without it, setsebool only affects the running system, the same temporary-versus-permanent distinction as setenforce versus editing /etc/selinux/config. Toggling an existing boolean is always preferable to writing a custom policy exception for the same behavior — it's a change the policy maintainers already reviewed and scoped, rather than a new, unaudited rule.
audit2allow: Turning a Denial into a Reviewable Policy
When no existing boolean covers a legitimate need, and restorecon/semanage fcontext don't apply because the access itself — not just the label — is genuinely outside what the policy permits, audit2allow reads the recorded denial and generates the exact policy module that would permit it:
******************** IMPORTANT ***********************
To make this policy package active, execute:
semodule -i mycustompolicy.pp
module mycustompolicy 1.0;
require {
type httpd_t;
type var_lib_t;
class file { read open };
}
#============= httpd_t ==============
allow httpd_t var_lib_t:file { read open };
This generated rule should be reviewed before installing it, every time — audit2allow grants exactly the access that was denied, without judging whether that access should actually be permitted. A single AVC denial might reflect a legitimate, narrow requirement, or it might reflect an application trying to do something it has no real business doing; installing every audit2allow suggestion unread turns SELinux into a system that logs violations and then quietly authorizes them, which defeats its purpose.
AppArmor: Profile Modes and Building a New Profile
AppArmor, Ubuntu's default MAC system, takes a path-based approach instead of SELinux's label-based one: a profile lists the specific files, capabilities, and network access a named executable is allowed to use.
apparmor module is loaded.
50 profiles are loaded.
45 profiles are in enforce mode.
5 profiles are in complain mode.
- enforce — violations are blocked and logged, the AppArmor equivalent of SELinux's
Enforcing; - complain — violations are only logged, equivalent to
Permissive, and the standard mode while a new profile is still being developed.
Building a Profile from Scratch: aa-genprof
For an application with no existing profile, aa-genprof walks through generating one interactively, by observing what the application actually does:
Writing updated profile for /usr/local/bin/mydaemon.
Please start the application to be profiled in
another window and exercise its functionality now.
Once completed, select the "Scan" option below in order
to scan the system logs for AppArmor events.
[(S)can system log for AppArmor events] / (F)inish
With the profile now in place and in complain mode, the application is exercised through its normal, legitimate workflows — every route, every scheduled job, every startup and shutdown path — while aa-genprof watches the log for anything the profile doesn't yet cover, and interactively asks whether to allow it:
Profile: /usr/local/bin/mydaemon
Path: /etc/mydaemon/config.yaml
New Mode: r
[1 - #include <abstractions/base>]
2 - /etc/mydaemon/config.yaml r,
(A)llow / (D)eny / (I)gnore / (G)lob / (N)ew / (C)lean exit / (Q)uit
This is significantly more manual than SELinux's audit2allow, and it's a deliberate trade-off: it forces a human decision for each access pattern up front, at profile-authoring time, rather than automatically encoding whatever the application asked to do.
Refining an Existing Profile: aa-logprof
Once a first-draft profile exists, aa-logprof performs the same log-scan-and-ask cycle against ongoing activity — the tool to run periodically after a profile has been in complain mode for a while, to fold observed, legitimate behavior into the profile before switching it to enforce:
Switching a Profile's Mode
sudo aa-complain /usr/local/bin/mydaemon # observe only, log but don't block
sudo aa-enforce /usr/local/bin/mydaemon # block anything the profile doesn't allow
A profile should stay in complain mode until its behavior under real, representative use has stopped producing new log entries — switching to enforce too early blocks legitimate functionality nobody exercised yet during development.
Interview angle: SELinux versus AppArmor, in one sentence each
A concise, accurate way to state the difference if asked directly: SELinux labels objects with types and defines what each process type may do to each object type, giving very fine-grained, system-wide control at the cost of a steeper learning curve; AppArmor attaches a profile to a specific executable path and lists what that one program may access, which is easier to read and author but only as thorough as the profiles that have actually been written. Neither is "more secure" in the abstract — SELinux's targeted policy on RHEL and AppArmor's default Ubuntu profile set both leave plenty of unconfined territory by design, and the real security value comes from writing and maintaining profiles/policy for the applications that actually matter on a given server.
Common Mistakes
- Setting SELinux to
Disabledto make a denial go away. As covered above, this also disables all future logging of what would have been denied, and re-enabling it later triggers a full filesystem relabel.Permissiveis the correct temporary diagnostic state. - Installing every
audit2allowsuggestion without reading it. The generated policy grants exactly what was denied, with no judgment about whether it should be — reviewing the.tefile beforesemodule -iis not optional. - Switching a new AppArmor profile straight to
enforce. Without acomplain-mode observation period covering the application's real usage patterns,enforcemode blocks legitimate paths nobody exercised during profile development. - Treating a boolean and a custom policy module as interchangeable. A boolean is a pre-reviewed, scoped switch; a custom
audit2allow-generated module is a new, unaudited rule. Always check for an existing boolean first.
Exercises
- On a RHEL-family lab VM, switch to
Permissivemode, reproduce a denial by attempting an operation you know is blocked, then useausearchandaudit2allowto see what policy it would generate — without installing it. - Find a boolean relevant to a service you run (
getsebool -a | grep <service>) and explain, from its name alone, what trade-off it controls. - On an Ubuntu lab VM, run
aa-genprofagainst a small script of your own, exercise its normal behavior, and finish the profile. Check its resulting mode withaa-status. - Explain why a security team might prefer AppArmor's manual, one-decision-at-a-time profiling process over SELinux's
audit2allow, despite it being slower.
Summary
Both SELinux and AppArmor add a mandatory policy layer on top of ordinary file permissions, but they take different approaches: SELinux labels objects and processes with types and evaluates policy against those types, with getenforce/setenforce controlling temporary mode changes, booleans providing pre-reviewed policy toggles, and audit2allow generating (but never auto-approving) new exceptions; AppArmor attaches a profile directly to an executable's path, built incrementally with aa-genprof/aa-logprof while in complain mode before being switched to enforce. Next, Firewall and UFW moves the same default-deny philosophy from what a process can touch on disk to what can reach the host over the network at all.