SELinux (Security-Enhanced Linux) adds mandatory access control (MAC) on top of classic permissions: every process and every file carries a label, and the policy decides what a process may touch, even when it runs as root. A compromised service thus stays confined to its perimeter. This guide is for the RHEL, AlmaLinux or Rocky administrator who wants to diagnose a denial rather than turn SELinux off: modes, contexts, ports, booleans, reading an AVC denial, and a full workshop on a relocated Nginx document root.
What you will learn
- Tell DAC access control apart from SELinux MAC permissions
- Switch between the
enforcing,permissiveanddisabledmodes safely - Read a security context and relabel a file durably
- Manage port types, booleans and contexts with
semanage - Diagnose a denial (AVC) with
ausearch,audit2whyandsealert
Prerequisites
- A distribution where SELinux is native: RHEL, AlmaLinux, Rocky or Fedora. On Debian/Ubuntu, it is rather AppArmor: to decide, see the comparison SELinux or AppArmor (in French).
- root access and the
policycoreutils-python-utilspackage, which providessemanage,audit2allowandaudit2why. It is not part of theCoregroup of the minimal installation (checked withdnf group info coreon AlmaLinux 10.2): check withrpm -q policycoreutils-python-utilsand install it if needed. - For diagnosis,
setroubleshoot-serverprovides thesealertcommand, but it is not installed on a minimal AlmaLinux or RHEL, any more thansesearch(packagesetools-console). Knowing how to read a raw AVC withausearch,audit2whyandmatchpathconremains the core skill, and it is the one exams expect.
Understanding SELinux: the basics
SELinux (Security-Enhanced Linux) is a security extension built into the Linux kernel, initially developed by the NSA. Its main goal? To add an extra security layer on top of the traditional Unix access rights, based on DAC (Discretionary Access Control) permissions.
With SELinux, you move to a stricter model: MAC (Mandatory Access Control). But what does that mean in practice?
DAC vs. MAC
In the DAC model, a file belongs to a user and a group, and its rights are defined by the classic rw-r--r-- permissions. Running processes inherit the rights of the user who starts them. Result: if a service (for example Apache) is compromised, the attacker can exploit everything the associated user can touch.
With MAC, it is very different: even if the user has rights, SELinux policies can block actions based on precise contexts. Every process and every file is labelled with a security context. These labels are then matched against strict rules defined in the policies.
Why was SELinux created?
The idea behind SELinux is simple: administrators need to finely control what a process can or cannot do, even if it runs under a privileged user. This drastically limits the impact of a compromise.
A few application examples:
- A web server can read its HTML files, but it cannot access system files, even if the Unix permissions seem to allow it.
- A database server can write to its directories, but never execute a system binary.
Key takeaways
- SELinux is enabled at kernel level: it acts like a bodyguard that intercepts every sensitive action.
- It is designed to be modular: rules can adapt to the specific needs of services and applications.
- Complexity is the other side of the coin: badly configured, SELinux can block legitimate services, hence the importance of understanding how it works before enabling it fully.
The SELinux operating modes
SELinux can run in three distinct modes, each offering a different level of control and strictness. Understanding these modes is essential to start well and avoid unexpected blocks.
Enforcing (strict mode)
In enforcing mode, SELinux applies all the rules defined by its policies. Every action performed by a process is checked: if it violates a rule, it is blocked and the incident is recorded in the logs.
Useful commands:
getenforceThis command shows the current mode:
EnforcingTo switch to enforcing mode temporarily (until the next reboot):
sudo setenforce 1Permissive (observation mode)
Permissive mode is an observation mode. SELinux keeps checking the rules and recording violations, but blocks nothing. It is ideal to test a configuration, identify potential problems and adjust policies without risking interrupting services.
To switch temporarily to permissive mode, then check the result:
sudo setenforce 0getenforcePermissiveDisabled
Finally, in disabled mode, SELinux is completely turned off: no checks and no logging. This mode is strongly discouraged in production.
To change the mode permanently, edit /etc/selinux/config (no spaces
around the =, otherwise the line is ignored):
SELINUX=enforcingSELINUXTYPE=targetedRHEL 9 and 10: no longer disable SELinux through this file
Since RHEL 9 and derivatives, SELINUX=disabled in /etc/selinux/config
no longer really disables SELinux: according to the Red Hat documentation, the
system then boots with SELinux enabled but with no policy loaded. To
really disable it, go through the kernel command line:
sudo grubby --update-kernel ALL --args selinux=0. Above all, going back
from disabled to enforcing requires a full relabel (files created
without SELinux carry no label):
sudo touch /.autorelabel && sudo rebootModes summary
The table sums up what each mode does with a violation: block it, log it, or ignore it.
| Mode | Action |
|---|---|
| Enforcing | Applies and enforces the policies |
| Permissive | Does not enforce but logs violations |
| Disabled | Ignores SELinux entirely |
Best practices
To start using SELinux without risk:
- Enable it in permissive mode to observe the behaviour.
- Analyse the logs and solve the problems you find.
- Switch to enforcing once everything is ready.
SELinux contexts: understanding security labels
In SELinux, every file, directory, process and network port is associated with a security context. These contexts let the system know who can do what, beyond classic Unix permissions.
What is an SELinux context?
A context is a label structured as follows:
user:role:type:levelExample for a file:
unconfined_u:object_r:httpd_sys_content_t:s0- unconfined_u: the SELinux user (not necessarily tied to the Unix user).
- object_r: the role, often
object_rfor files. - httpd_sys_content_t: the type, the most important part, which defines the access rules.
- s0: the level (used by MLS policies, less common).
Why do these labels matter?
SELinux decides whether to allow an action based on types. For example, a
process with the httpd_t type (Apache) can access files labelled
httpd_sys_content_t, but not a file labelled user_home_t.
Even if the Unix permissions (rwx) are correct, a wrong label will block
access.
Display contexts
To see the contexts attached to files, ls -Z adds the security
context in front of each name (with -l, it sits between the group and
the size):
ls -Z /var/www/htmlunconfined_u:object_r:httpd_sys_content_t:s0 index.htmlTo see the context of a process:
ps -eZ | grep httpdChange labels
To change a type temporarily with chcon:
chcon -t httpd_sys_content_t my_filePitfall number 1: chcon is not persistent
chcon writes the label directly onto the file, not into the context
database (file_contexts.local). At the next restorecon or system
relabel, your chcon is lost and the file goes back to its default
context. For a durable change, declare it with semanage fcontext (see
below), then apply it with restorecon. Keep chcon for quick tests.
To restore the official labels (according to the policies):
restorecon -v /var/www/html/my_fileA concrete example
Imagine you put a web page in /opt/my_site. By default, this directory gets
an incompatible type and Apache cannot access it, even with the right
Unix permissions.
Be careful not to guess which one: under /opt, it is not default_t.
The policy declares /opt as usr_t, so a directory created there is
born usr_t. The default_t type only concerns paths that no rule
covers. Rather than guessing, ask the policy what it expects for a given
path:
matchpathcon /opt/my_site/opt/my_site system_u:object_r:usr_t:s0In every case, the fix is the same:
semanage fcontext -a -t httpd_sys_content_t "/opt/my_site(/.*)?"restorecon -R /opt/my_siteManaging SELinux policies
For SELinux to work, it relies on a structured set of policies. These policies define the precise rules that each process type can apply to each type of file, port or resource. Understanding how they are managed is essential to adapt SELinux to your needs.
Policy types
There are two main policy types:
- targeted: only some critical applications (such as Apache, PostgreSQL, SSH) are confined.
- MLS (Multi-Level Security): used in very high security environments, it introduces more complex levels and categories.
By default, on most systems (Fedora, RHEL, Rocky Linux), the targeted policy is active:
cat /etc/selinux/configYou will see:
SELINUXTYPE=targetedEssential tools
These commands live in packages separate from the SELinux kernel part, often missing from a minimal installation.
Check the policies in place
To list the ports, files and settings known to SELinux:
semanage port -lsemanage fcontext -lsemanage boolean -lChange file labels
To define (persistently) the expected type on a path:
semanage fcontext -a -t httpd_sys_content_t "/opt/my_site(/.*)?"restorecon -R /opt/my_siteChange the associated ports
By default, the http_port_t type covers TCP ports 80, 81, 443, 488, 8008, 8009, 8443, 9000 (read with semanage port -l | grep http_port_t on
AlmaLinux 10.2). To allow Apache or nginx on a non-standard port
such as 9090, add it to the type:
semanage port -a -t http_port_t -p tcp 9090A frequent pitfall: the port may already be defined under another type.
That is the case of 8080, which actually belongs to
http_cache_port_t. The correct form is then -m (modify) instead of
-a (add):
semanage port -m -t http_port_t -p tcp 8080Enable or disable options through SELinux booleans
Booleans let you adjust some rules on the fly. Example: allow Apache to open outgoing network connections:
setsebool -P httpd_can_network_connect onYou can list all the available booleans:
getsebool -aPractical examples
Two gestures come up often with booleans: enable one persistently
with -P, then check its state.
- Allow Samba sharing:
setsebool -P samba_export_all_ro on- Check whether a specific rule is active:
getsebool samba_export_all_roTroubleshooting common problems
Even with a good configuration, SELinux can surprise you with unexpected blocks. Fortunately, it provides powerful tools to diagnose and solve these problems. Here is how to use them effectively, always starting from the audit log.
Identify the errors
SELinux violations are recorded in the logs, mainly:
/var/log/audit/audit.logTo extract recent events:
ausearch -m avc --success nodmesg | grep -i denied returns nothing when auditd is running
This shortcut is often suggested. It produces a complete false negative
on a production machine. Measured on AlmaLinux 10: dmesg held 268 lines
and zero matches on denied, while /var/log/audit/audit.log did contain
the denials.
The explanation is simple: as soon as auditd is active, it consumes the
audit messages, which therefore do not stay in the kernel buffer. And
auditd runs by default on RHEL and derivatives.
The reader who follows this shortcut concludes "no SELinux denial" and looks
for the failure elsewhere. Stick to ausearch, or read the audit
file directly:
sudo grep 'avc: denied' /var/log/audit/audit.log | tailausearch -ts recent can wrongly answer <no matches>
Another false negative met several times:
ausearch -m avc -ts recent answers <no matches> while the denials are
indeed in audit.log. The pseudo-terminal changes nothing, contrary to
what you sometimes read: the behaviour is identical with or without
ssh -tt.
What works every time is to name the source explicitly:
sudo ausearch -m avc --input /var/log/audit/audit.logUnderstand the messages
A typical SELinux message looks like:
type=AVC msg=audit(1628765432.123:456): avc: denied { read } for pid=1234 comm="httpd" name="index.html" dev="sda1" ino=5678 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0It shows:
- the process (
httpd_t) that attempted the action, - the targeted file (
user_home_t), - the forbidden operation (
read).
Use audit2why
This tool translates SELinux logs into understandable explanations:
cat /var/log/audit/audit.log | audit2whyIt tells you why an action was blocked and often suggests a solution, such as enabling a specific boolean.
Generate rules with audit2allow
If you need to create a custom exception:
cat /var/log/audit/audit.log | audit2allow -M mymodulesemodule -i mymodule.ppaudit2allow is the third option, never the first
Generating a module with audit2allow must be a last resort, after
checking the two healthier solutions: fix the label (restorecon /
semanage fcontext) and enable a boolean. A module created blindly can
allow a confined domain to write into system types (etc_t, usr_t) - a
gaping hole - and it often masks a simple mislabelling that a
restorecon would have fixed. Always read the generated rules before
installing the module: they are in mymodule.te, next to the .pp, or are
displayed by audit2allow without -M. The audit2allow -w option does not
show these rules: it is the equivalent of audit2why, which explains the
denial.
Troubleshooting best practices
- Check the logs first, before changing anything.
- Use
permissiveon the domain concerned to test without disabling SELinux globally:
semanage permissive -a httpd_t- Document your changes so they stay easy to maintain.
- Restore the original contexts before thinking about creating exceptions:
restorecon -Rv /affected/pathHands-on workshop: move the Nginx document root to /data/html with SELinux
In this workshop, we will see how to move the document root of Nginx to a custom directory, for example /data/html, while staying compatible with SELinux restrictions.
Step 1: Prepare the environment
Install Nginx:
dnf install -y nginxsystemctl enable nginx --nowCheck that the service works:
curl http://localhostBy default, Nginx serves files from:
/usr/share/nginx/htmlStep 2: Create the new directory
To move the document root, start by creating the directory:
mkdir -p /data/htmlCreate a test page in it:
echo "<h1>Test Nginx SELinux</h1>" > /data/html/index.htmlStep 3: Change the Nginx configuration
Edit the file:
/etc/nginx/nginx.confIn the server section, change:
root /usr/share/nginx/html;to:
root /data/html;Restart Nginx:
systemctl restart nginxStep 4: Test (without SELinux)
If SELinux is in permissive mode, you should see your page with this command:
curl http://localhost<h1>Test Nginx SELinux</h1>But in enforcing mode, you will probably get a 403 error. Why?
Step 5: Check the SELinux contexts
Display the labels:
ls -Zd /usr/share/nginx/html /data/htmlsystem_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/htmlunconfined_u:object_r:default_t:s0 /data/htmlYou can see that:
/usr/share/nginx/html→httpd_sys_content_t/data/html→default_t
Yet Nginx can only read files labelled httpd_sys_content_t.
Step 6: Fix the labels
Declare the new path:
semanage fcontext -a -t httpd_sys_content_t "/data/html(/.*)?"Apply the labels:
restorecon -Rv /data/htmlStep 7: Allow SELinux access
Check that the httpd_can_network_connect boolean is on if your site makes
outgoing connections:
setsebool -P httpd_can_network_connect onStep 8: Restart and test
Restart Nginx:
systemctl restart nginxTest again:
curl http://localhost<h1>Test Nginx SELinux</h1>It should work!
Check the logs in case of trouble
If you still run into errors:
ausearch -m avc --success no | audit2whyThis will tell you whether another adjustment is needed.
Put it into practice
The theory about contexts, booleans and AVC denials only holds if you test it on a machine in enforcing mode. These three labs replay the everyday gestures: allow a service cleanly, diagnose a denial with the audit tools, then repair a file labelling durably.
The three labs above work on one mechanism at a time, boolean, AVC, then context. The drill mixes them under exam conditions: you are no longer told which one is at fault, and setenforce 0 does not count as a fix.
Key points
- SELinux applies MAC through labels (
user:role:type:level); the type is the core of daily work - Three modes: enforcing, permissive, disabled; since RHEL 9, you disable it with
selinux=0on the kernel command line, and re-enabling requires a relabel chconis temporary (lost at relabel);semanage fcontext+restoreconmakes the change durable- Ports:
semanage port -afor a new type,-mif the port is already defined (http_port_t≠http_cache_port_t) - Diagnosing a denial:
ausearch -m AVC→audit2why/sealert→ fix the label or a boolean audit2allowonly as a last resort; references ANSSI R46 (enable SELinux targeted) and R48 (SELinux variables)
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Understand and use AppArmor
A new full quiz with random questions
Work again only on the questions you missed
Conclusion
SELinux can look intimidating at first: its contexts, its policies, its specific commands and its mysterious blocks give the impression of a complex system reserved for experts. But do not get discouraged.
With regular practice, tests on a lab environment, and a little patience, you will see the mechanisms become familiar. Little by little, you will know where to look in the logs, how to adjust contexts, enable a boolean, or write a custom module.
Admittedly, the beginnings are sometimes frustrating, but after a while, you get by very well. And above all: you realise the huge added value SELinux brings in terms of security.
There is also an interesting alternative to know: AppArmor, used mainly on Ubuntu and SUSE. AppArmor is often simpler to configure, because it relies on profiles attached to executables rather than on file labels. Although it is less granular than SELinux, it can suit lighter deployments or environments where simplicity comes first.
So do not hesitate to experiment, to read the documentation, to follow practical tutorials, and above all to stay curious: SELinux (or AppArmor) will quickly become a precious ally in protecting your Linux systems.
FAQ: frequently asked questions
The questions below cover the basics of SELinux: its modes, contexts and booleans, with the command that goes with each gesture.
- Enforcing: strictly applies the security policies, blocking unauthorised access.
- Permissive: does not apply the policies but records violations for analysis.
- Disabled: turns SELinux off completely.
sestatus command to display the current state of SELinux, or getenforce to know the active mode (Enforcing, Permissive or Disabled).setenforce 0 (Permissive) or setenforce 1 (Enforcing). For a permanent change, edit the SELINUX= line of the /etc/selinux/config file. Since RHEL 9, fully disabling SELinux goes through the selinux=0 kernel parameter (grubby --update-kernel ALL --args selinux=0).ls -Z command to display the SELinux contexts of files and directories.chcon command to change a file's context temporarily. For a permanent change, use semanage fcontext followed by restorecon.getsebool -a command to display the list of all SELinux booleans and their current state.setsebool command to change the state of a boolean. For example, setsebool httpd_enable_homedirs on enables access to home directories for the HTTP server.