Skip to content

Portal | Level: L2: Operations | Topics: SELinux & AppArmor, Linux Hardening, Linux Fundamentals | Domain: Security

SELinux & AppArmor - Primer

Why This Matters

Standard Linux permissions (DAC — Discretionary Access Control) answer one question: "Does this user/group have read/write/execute on this file?" That model breaks the moment a compromised process runs as root, because root bypasses DAC entirely. A hacked Apache running as root owns your entire filesystem.

Mandatory Access Control (MAC) adds a second, independent gate. Even if a process runs as root, the MAC policy can say "Apache only touches files labeled httpd_sys_content_t — everything else is denied." SELinux and AppArmor are the two MAC implementations that ship with mainstream distros. You will encounter one or the other on every production Linux box you touch.

If your instinct when something breaks is setenforce 0, this primer exists to give you a better instinct.

Name origin: SELinux stands for Security-Enhanced Linux. It was developed by the NSA (National Security Agency) and released as open source in 2000. The NSA contributed it to the Linux kernel in 2003 (kernel 2.6). AppArmor (originally "SubDomain") was created by Immunix Inc. and acquired by Novell in 2005 for SUSE Linux. The name is a portmanteau of "Application Armor."


DAC vs MAC — The Mental Model

 ┌──────────────────────────────────────────────┐
 │              Access Request                   │
 │  (process tries to read /var/www/index.html)  │
 └──────────────┬───────────────────────────────┘
        ┌───────▼────────┐
        │  DAC Check      │  ← traditional rwx / owner / group
        │  (permission    │
        │   bits, ACLs)   │
        └───────┬────────┘
                │ PASS
        ┌───────▼────────┐
        │  MAC Check      │  ← SELinux or AppArmor policy
        │  (label/profile │
        │   based)        │
        └───────┬────────┘
                │ PASS
        ┌───────▼────────┐
        │  Access Granted │
        └────────────────┘

Both gates must pass. MAC never loosens what DAC already denied.

Aspect DAC MAC
Who decides File owner System policy (admin)
Root bypasses? Yes No
Granularity User/group/other Process type + resource label
Default stance Allow unless denied Deny unless explicitly allowed
Compromise risk Root = game over Root process still confined

SELinux — Deep Dive

The Three Modes

Mode Enforcing? Logging? Use Case
Enforcing Yes Yes Production — the only real mode
Permissive No Yes Debugging — logs but doesn't block
Disabled No No Never in production

Check current mode:

getenforce
# or detailed:
sestatus

Switch temporarily (does not survive reboot):

setenforce 1   # Enforcing
setenforce 0   # Permissive

Permanent change lives in /etc/selinux/config:

SELINUX=enforcing
SELINUXTYPE=targeted

Changing from disabled to enforcing requires a full filesystem relabel and reboot. Plan for 10-60 minutes depending on disk size.

Security Contexts — The Four Fields

Every file, process, port, and user carries a security context:

user:role:type:level

Example:

ls -Z /var/www/html/index.html
# -rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/index.html

ps -eZ | grep httpd
# system_u:system_r:httpd_t:s0    1234 ?  00:00:01 httpd

Breakdown:

Field Example What It Does
User system_u SELinux user (not Linux user)
Role system_r Role-based access (RBAC layer)
Type httpd_t The one that matters 95% of the time
Level s0 MLS/MCS sensitivity (ignore unless MLS policy)

Type Enforcement is where almost all policy decisions happen. The rule looks like:

allow httpd_t httpd_sys_content_t:file { read getattr open };

Translation: "A process of type httpd_t may read/getattr/open files labeled httpd_sys_content_t."

Changing File Contexts

When you move files (not copy), they keep their old label. This is the #1 source of SELinux breakage.

# See current context
ls -Z /srv/myapp/

# Set context using semanage (persistent rule) + restorecon (apply)
semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
restorecon -Rv /srv/myapp/

# Quick temporary fix (does not survive restorecon/relabel)
chcon -t httpd_sys_content_t /srv/myapp/index.html

Always prefer semanage fcontext + restorecon over chcon. chcon is a bandaid.

Remember: Mnemonic: "chcon is a Ch-eat, semanage is the S-eal." chcon changes labels temporarily (lost on relabel), semanage writes to the policy store permanently. If you see chcon in a runbook, replace it with the two-step semanage fcontext + restorecon.

War story: The #1 SELinux-caused outage pattern: an admin moves a web root from /var/www/html/ to /srv/www/ using mv. The files keep their old default_t label. Apache gets AVC denials. The admin runs setenforce 0 "temporarily" and forgets. Months later, a security audit discovers the server has been running without MAC the entire time.

Booleans — Policy Toggle Switches

Booleans let you enable/disable specific policy behaviors without writing custom policy:

# List all booleans
getsebool -a

# Check specific boolean
getsebool httpd_can_network_connect
# httpd_can_network_connect --> off

# Enable (persistent across reboots with -P)
setsebool -P httpd_can_network_connect on

Common booleans you'll use:

httpd_can_network_connect       → Apache/Nginx can make outbound connections
httpd_can_network_connect_db    → Apache can talk to databases
httpd_enable_homedirs           → Apache can serve from ~/public_html
allow_ftpd_full_access          → FTP can read/write outside its default dirs
container_manage_cgroup         → Container runtimes can manage cgroups

Troubleshooting with audit2allow

When SELinux blocks something, it writes an AVC (Access Vector Cache) denial to the audit log:

# Find recent denials
ausearch -m AVC -ts recent

# Or use the audit log directly
grep "avc:  denied" /var/log/audit/audit.log

A denial looks like:

type=AVC msg=audit(1678234567.123:456): avc:  denied  { read } for
pid=1234 comm="httpd" name="config.yml" dev="sda1" ino=789012
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0

Generate a policy module to allow the denied action:

# From audit log
ausearch -m AVC -ts recent | audit2allow -M myfix
# Creates myfix.te (human-readable) and myfix.pp (compiled)

# Review the .te file FIRST
cat myfix.te

# Then install if it makes sense
semodule -i myfix.pp

Never blindly pipe audit2allow output into policy. Always read the .te file. It might grant far more than you intended.

Debug clue: When reading AVC denials, the key fields are scontext (source -- the process), tcontext (target -- the resource), and tclass (type of object -- file, socket, port). If scontext shows unconfined_t, the process was not confined at all, which usually means it was started outside systemd or from an interactive shell.

Port Contexts

SELinux labels network ports too:

# See what ports httpd_t can bind to
semanage port -l | grep http
# http_port_t    tcp    80, 81, 443, 488, 8008, 8009, 8443, 9000

# Add a custom port
semanage port -a -t http_port_t -p tcp 8090


AppArmor — The Path-Based Alternative

AppArmor confines processes based on file paths rather than labels. It ships as the default MAC on Ubuntu and SUSE.

Profile Modes

Mode Behavior
Enforce Blocks and logs violations
Complain Logs violations but does not block
Disabled Profile loaded but inactive (rare, use complain)
# See all profiles and their modes
aa-status
# or
apparmor_status

# Set a profile to complain mode
aa-complain /etc/apparmor.d/usr.sbin.nginx

# Set back to enforce
aa-enforce /etc/apparmor.d/usr.sbin.nginx

Profile Anatomy

Profiles live in /etc/apparmor.d/ and are named after the binary path with dots replacing slashes:

# /etc/apparmor.d/usr.sbin.nginx

#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # Binary itself
  /usr/sbin/nginx mr,

  # Config
  /etc/nginx/** r,

  # Web root
  /var/www/** r,

  # Logs
  /var/log/nginx/** rw,

  # PID file
  /run/nginx.pid rw,

  # Temp files
  /var/lib/nginx/tmp/** rw,

  # Network
  network inet stream,
  network inet6 stream,

  # Deny everything else (implicit)
}

Permission flags:

r  — read
w  — write
a  — append
k  — lock
l  — link
m  — mmap PROT_EXEC
x  — execute (ix=inherit, px=profile, ux=unconfined, cx=child)

Generating Profiles

# Generate a profile skeleton
aa-genprof /usr/sbin/myapp
# Starts myapp, scans logs, asks you about each access — interactive

# Update profile from logs (after running in complain mode)
aa-logprof

Abstractions — Reusable Rule Sets

AppArmor ships with abstractions in /etc/apparmor.d/abstractions/:

base          — basic system access every process needs
nameservice   — DNS, NSS, LDAP lookups
authentication — PAM, shadow, login
apache2-common — standard Apache paths
mysql         — MySQL client access

Include them with:

#include <abstractions/base>


SELinux vs AppArmor — When You Get to Choose

Criteria SELinux AppArmor
Shipped with RHEL, CentOS, Fedora, Rocky Ubuntu, SUSE, Debian
Approach Label-based (survives mv/rename) Path-based (simpler to reason)
Learning curve Steep Moderate
Multi-level security Yes (MLS) No
Default policy Comprehensive (targeted) Per-application profiles
Container support Excellent (built-in labeling) Good (Docker/LXC integration)

In practice, you use whatever your distro ships. RHEL shops run SELinux. Ubuntu shops run AppArmor. Fighting the default is rarely worth it.


Container Interactions

Both MAC systems interact with containers:

SELinux + Containers:

# Containers auto-labeled with container_t / svirt_lxc_net_t
# Volume mounts need :Z (private) or :z (shared) suffix
podman run -v /data:/data:Z myimage

# Without :Z, the container can't read the volume — SELinux blocks it

AppArmor + Containers:

# Docker applies default docker-default profile
# Override with:
docker run --security-opt apparmor=my-custom-profile myimage

# Disable (not recommended):
docker run --security-opt apparmor=unconfined myimage


Key Takeaways

  1. MAC is your second firewall inside the OS. DAC alone is not enough.
  2. SELinux type enforcement handles 95% of real-world policy decisions.
  3. semanage fcontext + restorecon is the correct way to fix file labels.
  4. Booleans are your first stop before writing custom policy.
  5. audit2allow is a diagnosis tool, not a fix-it button — read before you apply.
  6. AppArmor is path-based and easier to learn, but less granular.
  7. Never disable MAC in production. Use permissive/complain mode to debug, then fix and re-enforce.

Wiki Navigation

Prerequisites