SELinux basics: what it blocks and how to work with it, not around it
A practical guide to SELinux on AlmaLinux - the three modes, how to diagnose a denial properly, and why the fix is almost never "turn it off".
22 October 2024 6 min read
At some point, something on your AlmaLinux server will fail for no obvious reason – a web server that can’t write to a directory it clearly has file permissions for, or a service that won’t bind to a port it should be allowed to use. Nine times out of ten, the cause is SELinux. The instinctive fix people reach for is to disable it entirely. That’s almost always the wrong move, and it’s usually just as quick to fix the actual problem.
What SELinux actually does
Standard Linux file permissions (owner, group, read/write/execute) are what’s called discretionary access control – if a process is running as a user that owns a file, it can do whatever it likes with it. SELinux adds a second, independent layer on top called mandatory access control. Every process and every file gets a security label, and the kernel enforces a policy that says exactly which labelled processes are allowed to interact with which labelled files, sockets, and other resources – regardless of standard Unix permissions.
In practice, this means that even if a process is running as root, SELinux can still stop it from touching something it shouldn’t. It’s the reason a compromised web server process typically can’t be used to read your SSH keys or rewrite system binaries, even though the standard file permissions might technically allow it.
The three modes
- Enforcing – SELinux policy is actively applied. Anything that violates the policy is blocked and logged. This is the default on AlmaLinux and what you should be running in production.
- Permissive – policy violations are logged but not blocked. Useful for diagnosing what a policy would block without actually breaking anything, but it gives you no real protection.
- Disabled – SELinux is switched off completely. No labelling, no enforcement, no logging.
Check your current mode with:
getenforce
For a more detailed view, including the configured mode, the loaded policy, and whether any booleans are set:
sestatus
Why disabling it is a bad default
Disabling SELinux removes an entire layer of defence in exchange for saving a few minutes of troubleshooting. It’s a particularly bad trade on a hosting server, where a single compromised application (a vulnerable WordPress plugin, for example) is exactly the scenario SELinux is designed to contain – by stopping the compromised process from reaching files and services outside what it strictly needs. Most of the “it broke, so I turned it off” denials people hit are one or two commands away from being fixed properly, which is the whole point of this post.
If you genuinely need to rule SELinux out while debugging, switch to permissive mode temporarily rather than disabling it outright – that way you still get a log of what it would have blocked:
sudo setenforce 0
This change doesn’t survive a reboot. To change the mode permanently, edit SELINUX= in /etc/selinux/config – but set it back to enforcing once you’re done testing.
Diagnosing a denial
Every SELinux denial gets logged. On modern AlmaLinux you can search recent denials with ausearch (part of the audit package):
sudo ausearch -m avc -ts recent
You can also find them in the systemd journal, since audit events get logged there too:
sudo journalctl -t setroubleshoot --since "10 min ago"
Once you’ve found the relevant denial, audit2allow can turn it into a human-readable explanation, or even generate a policy module that would allow it – install it first if it isn’t already present:
sudo dnf install -y policycoreutils-python-utils
sudo ausearch -m avc -ts recent | audit2allow
Treat audit2allow‘s suggested policy as a last resort, not the default answer. It’s built to generate a working rule, not a well-considered one, and it’s easy to end up granting far more access than the situation actually needs. Look at what it’s proposing before you apply it, and prefer the targeted fixes below where they apply.
A common scenario: web server can’t write to a custom directory
Say you’ve set up Nginx or Apache to serve uploads from a custom path like /data/uploads instead of the default /var/www. Standard file permissions look fine – the web server’s user owns the directory and can write to it – but uploads still fail, and the logs show a permission denied error. This is almost always SELinux, because /data was never labelled as something the web server is allowed to write to.
You can confirm the label with:
ls -Zd /data/uploads
Anything outside the standard web directories typically comes up labelled default_t, which the web server process isn’t permitted to write to. The correct fix isn’t to disable SELinux – it’s to tell SELinux that this directory should be treated like a web content directory. Use semanage fcontext to set a persistent rule, then restorecon to apply it:
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/uploads(/.*)?"
sudo restorecon -Rv /data/uploads
httpd_sys_rw_content_t is the label for web content the server is allowed to both read and write, as opposed to the more common httpd_sys_content_t, which is read-only. Setting the rule with semanage fcontext makes it stick across relabels and reboots, unlike a one-off chcon call, which gets wiped out the next time the filesystem is relabelled.
When the fix is a boolean instead
Some SELinux restrictions aren’t about file labels at all – they’re controlled by booleans, which are simple on/off switches for entire categories of behaviour. A common example is a web application that needs to make outbound network connections, such as to a remote database or API, which is blocked by default. List the booleans relevant to a service with:
getsebool -a | grep httpd
And enable the one you need, with -P to make it persist across reboots:
sudo setsebool -P httpd_can_network_connect on
The general approach
When something gets blocked: check ausearch or the journal to see what was actually denied, work out whether it’s a file labelling issue or a boolean, and apply the narrowest fix that solves it. It usually takes about the same amount of time as disabling SELinux – and at the end of it, your server still has the protection SELinux is there to provide.