Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

SELinux: understand, troubleshoot and disable

By updated

Improve this page

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, permissive and disabled modes safely
  • Read a security context and relabel a file durably
  • Manage port types, booleans and contexts with semanage
  • Diagnose a denial (AVC) with ausearch, audit2why and sealert

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-utils package, which provides semanage, audit2allow and audit2why. It is not part of the Core group of the minimal installation (checked with dnf group info core on AlmaLinux 10.2): check with rpm -q policycoreutils-python-utils and install it if needed.
  • For diagnosis, setroubleshoot-server provides the sealert command, but it is not installed on a minimal AlmaLinux or RHEL, any more than sesearch (package setools-console). Knowing how to read a raw AVC with ausearch, audit2why and matchpathcon remains 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:

Terminal window
getenforce

This command shows the current mode:

Enforcing

To switch to enforcing mode temporarily (until the next reboot):

Terminal window
sudo setenforce 1

Permissive (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:

Terminal window
sudo setenforce 0
getenforce
Permissive

Disabled

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):

Terminal window
SELINUX=enforcing
SELINUXTYPE=targeted

RHEL 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):

Terminal window
sudo touch /.autorelabel && sudo reboot

Modes summary

The table sums up what each mode does with a violation: block it, log it, or ignore it.

ModeAction
EnforcingApplies and enforces the policies
PermissiveDoes not enforce but logs violations
DisabledIgnores 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:level

Example 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_r for 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):

Terminal window
ls -Z /var/www/html
unconfined_u:object_r:httpd_sys_content_t:s0 index.html

To see the context of a process:

Terminal window
ps -eZ | grep httpd

Change labels

To change a type temporarily with chcon:

Terminal window
chcon -t httpd_sys_content_t my_file

Pitfall 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):

Terminal window
restorecon -v /var/www/html/my_file

A 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:

Terminal window
matchpathcon /opt/my_site
/opt/my_site system_u:object_r:usr_t:s0

In every case, the fix is the same:

Terminal window
semanage fcontext -a -t httpd_sys_content_t "/opt/my_site(/.*)?"
restorecon -R /opt/my_site

Managing 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:

Terminal window
cat /etc/selinux/config

You will see:

SELINUXTYPE=targeted

Essential 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:

Terminal window
semanage port -l
semanage fcontext -l
semanage boolean -l

Change file labels

To define (persistently) the expected type on a path:

Terminal window
semanage fcontext -a -t httpd_sys_content_t "/opt/my_site(/.*)?"
restorecon -R /opt/my_site

Change 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:

Terminal window
semanage port -a -t http_port_t -p tcp 9090

A 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):

Terminal window
semanage port -m -t http_port_t -p tcp 8080

Enable or disable options through SELinux booleans

Booleans let you adjust some rules on the fly. Example: allow Apache to open outgoing network connections:

Terminal window
setsebool -P httpd_can_network_connect on

You can list all the available booleans:

Terminal window
getsebool -a

Practical examples

Two gestures come up often with booleans: enable one persistently with -P, then check its state.

  • Allow Samba sharing:
Terminal window
setsebool -P samba_export_all_ro on
  • Check whether a specific rule is active:
Terminal window
getsebool samba_export_all_ro

Troubleshooting 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.log

To extract recent events:

Terminal window
ausearch -m avc --success no

dmesg | 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:

Terminal window
sudo grep 'avc: denied' /var/log/audit/audit.log | tail

ausearch -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:

Terminal window
sudo ausearch -m avc --input /var/log/audit/audit.log

Understand 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=0

It 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:

Terminal window
cat /var/log/audit/audit.log | audit2why

It 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:

Terminal window
cat /var/log/audit/audit.log | audit2allow -M mymodule
semodule -i mymodule.pp

audit2allow 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 permissive on the domain concerned to test without disabling SELinux globally:
Terminal window
semanage permissive -a httpd_t
  • Document your changes so they stay easy to maintain.
  • Restore the original contexts before thinking about creating exceptions:
Terminal window
restorecon -Rv /affected/path

Hands-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:

Terminal window
dnf install -y nginx
systemctl enable nginx --now

Check that the service works:

Terminal window
curl http://localhost

By default, Nginx serves files from:

/usr/share/nginx/html

Step 2: Create the new directory

To move the document root, start by creating the directory:

Terminal window
mkdir -p /data/html

Create a test page in it:

Terminal window
echo "<h1>Test Nginx SELinux</h1>" > /data/html/index.html

Step 3: Change the Nginx configuration

Edit the file:

/etc/nginx/nginx.conf

In the server section, change:

root /usr/share/nginx/html;

to:

root /data/html;

Restart Nginx:

Terminal window
systemctl restart nginx

Step 4: Test (without SELinux)

If SELinux is in permissive mode, you should see your page with this command:

Terminal window
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:

Terminal window
ls -Zd /usr/share/nginx/html /data/html
system_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/html
unconfined_u:object_r:default_t:s0 /data/html

You 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:

Terminal window
semanage fcontext -a -t httpd_sys_content_t "/data/html(/.*)?"

Apply the labels:

Terminal window
restorecon -Rv /data/html

Step 7: Allow SELinux access

Check that the httpd_can_network_connect boolean is on if your site makes outgoing connections:

Terminal window
setsebool -P httpd_can_network_connect on

Step 8: Restart and test

Restart Nginx:

Terminal window
systemctl restart nginx

Test again:

Terminal window
curl http://localhost
<h1>Test Nginx SELinux</h1>

It should work!

Check the logs in case of trouble

If you still run into errors:

Terminal window
ausearch -m avc --success no | audit2why

This 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=0 on the kernel command line, and re-enabling requires a relabel
  • chcon is temporary (lost at relabel); semanage fcontext + restorecon makes the change durable
  • Ports: semanage port -a for a new type, -m if 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
  • audit2allow only 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

6 questions
6 min
70% to pass

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

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.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn