SELinux is the most powerful access-control framework in mainstream Linux and the one most often disabled in production. The reason is almost never a real shortcoming of the framework - it is that the first time SELinux blocks an application the operator does not know which boolean to flip, which file context to relabel, or how to author a small policy module for the genuinely new behaviour. So setenforce 0 happens, then SELINUX=disabled in /etc/selinux/config, and a layer of defence that would have absorbed several years of CVEs is gone for the lifetime of the host. This guide is the workflow that keeps SELinux enforcing on every host you operate. It walks the four real tools - booleans, file contexts, audit2allow, and custom policy modules - with concrete examples for the applications you actually run, and a free PDF cheat sheet of the commands.
Table of Contents
- Why SELinux is worth keeping
- The four tools you actually need
- Reading an SELinux denial
- Booleans - the easy fix
- File contexts - the next-easy fix
- audit2allow and the .te file
- Authoring a custom policy module
- The production workflow
- Per-domain permissive (without disabling)
- Common pitfalls
- Audit checklist
- FAQ
Why SELinux is worth keeping
SELinux is mandatory access control - rules apply to every process regardless of UID. A web server compromised through a remote code execution vulnerability runs in the httpd_t domain; httpd_t cannot read /etc/shadow regardless of which user the daemon runs as, cannot connect to a database on a non-allowed port, cannot exec a shell that does not have the right transition. These constraints absorb whole classes of post-exploitation behaviour without any application code change. The CVE archive is full of bugs that did not become breaches because SELinux blocked the second step.
The cost is real - learning the workflow takes a few days. But the workflow is not large. Once you have done two or three SELinux-related tickets the right way, you will not reach for setenforce 0 again.
The four tools you actually need
Most SELinux problems can be fixed with one of four primitives, in this order of effort:
- A boolean - a built-in policy switch that toggles a category of behaviour on or off.
setsebool -P httpd_can_network_connect_db ontells the policy that web servers may now talk to databases. Always try this first. - A file context relabel - tell the system that
/srv/myapp/uploadsshould have thehttpd_sys_rw_content_ttype, then relabel the directory. Most "permission denied" errors that survive booleans are file-context errors. audit2allow- read the AVC denials in the audit log and generate the policy lines that would allow them. The output is a starting point, not a finished module.- A custom policy module - a small
.tefile built into a.ppmodule and loaded into the kernel. This is the right tool for genuinely new behaviour your application introduces.
Anything outside these four is rarely needed - rebuilding the base policy, writing reference policy from scratch, or running in permissive mode "for now". If you find yourself reaching for those, step back and revisit the four primitives first.
Reading an SELinux denial
Every SELinux block writes an AVC (Access Vector Cache) message to the audit log. The fastest way to see them in human-readable form:
ausearch -m avc -ts recent
sealert -a /var/log/audit/audit.log | less
journalctl _TRANSPORT=audit -p warning --since "1 hour ago"
A typical AVC line tells you the source domain (httpd_t), target context (var_t), object class (file), action (read), the inode and the binary - everything you need to choose the right fix. sealert is the operator-friendly wrapper that suggests the boolean or the relabel, often correctly. Trust the suggestion to identify the problem; do not blindly copy the suggested audit2allow command without reading it.
Booleans - the easy fix
SELinux ships hundreds of booleans, most named informatively. List them and grep for the area you care about:
getsebool -a | grep httpd
# httpd_can_network_connect -- off
# httpd_can_network_connect_db -- off
# httpd_can_sendmail -- off
# httpd_enable_homedirs -- off
# httpd_use_nfs -- off
# httpd_unified -- off
setsebool -P httpd_can_network_connect_db on # -P persists across reboot
semanage boolean -l | grep httpd_can_network_connect_db
# httpd_can_network_connect_db (on , on) Allow httpd to ...
The -P flag is critical - without it the change is in-memory only and reverts at reboot. The most common booleans in real life: httpd_can_network_connect, httpd_can_network_connect_db, httpd_unified (write to httpd_sys_rw_content_t), nis_enabled, ssh_chroot_rw_homedirs, container_manage_cgroup.
File contexts - the next-easy fix
If the daemon needs to read a path under a non-standard location, the fix is rarely "change the permissions" - it is "change the SELinux type label". Use semanage fcontext to add a permanent context rule, then restorecon to apply it:
# Tell the system that /srv/myapp/web is web content
semanage fcontext -a -t httpd_sys_content_t '/srv/myapp/web(/.*)?'
# And /srv/myapp/uploads is web-writable
semanage fcontext -a -t httpd_sys_rw_content_t '/srv/myapp/uploads(/.*)?'
# Apply to existing files
restorecon -Rv /srv/myapp
# Verify
ls -lZ /srv/myapp
The regex form (with (/.*)?) covers the directory and everything under it. The -a adds; later -d deletes. The fcontext rule survives a relabel of the entire filesystem (restorecon -R / or a fixfiles run); it is the canonical place to record "files under this path are this type".
audit2allow and the .te file
For genuinely novel application behaviour - your daemon needs to talk to a Unix socket that no built-in domain ever talks to, for instance - audit2allow reads the AVC denials and synthesises the allow rules that would have permitted them:
ausearch -m avc -ts today | audit2allow -m myapp_local
# Outputs a .te (type enforcement) snippet:
# module myapp_local 1.0;
# require { type httpd_t; type myapp_var_lib_t; class file { read open getattr }; }
# allow httpd_t myapp_var_lib_t:file { read open getattr };
The output is a draft. Read it carefully - audit2allow does not know whether the denial was a legitimate need or a sign of compromise. If the source domain is trying to read /etc/shadow the right answer is to investigate, not to add an allow rule. If the source domain is trying to read its own data directory under a custom type, the right answer is to add the rule. Use judgement.
Authoring a custom policy module
Once you have a clean .te file, build it into a loadable module:
# Save the .te file
vi myapp_local.te
# Build the .pp module
make -f /usr/share/selinux/devel/Makefile myapp_local.pp
# Load it
semodule -i myapp_local.pp
# List loaded modules
semodule -l | grep myapp
# Remove if it causes a problem
semodule -r myapp_local
The module is now loaded into the running kernel and will persist across reboot. Keep the .te source in your config-management repo with a clear commit message - "Allow myapp_t to read its own state directory; new feature in v3.2". A reader six months from now needs the why, not just the what.
The production workflow
The workflow that keeps SELinux on without breaking deployments:
- Stay in enforcing mode. Never
setenforce 0on a production host; never setSELINUX=disabled. - When something breaks, read the AVC.
ausearch -m avc -ts recentorsealert -a /var/log/audit/audit.log. The denial tells you exactly what was blocked. - Try a boolean first.
getsebool -a | grep <domain>. If a sensible boolean exists, that is the fix. - Try a file context next.
semanage fcontext -a -t <type> '<path>(/.*)?',restorecon -Rv <path>. - If the application has genuinely new behaviour, build a custom module.
audit2allowas starting point, edit, build, load, commit to source control. - If you need temporary permissive while debugging, scope it to one domain.
semanage permissive -a httpd_t- SELinux still enforces everything else, and the domain logs but does not block.
Per-domain permissive (without disabling)
The escape hatch most operators do not know about: SELinux supports putting a single domain into permissive mode while everything else stays enforcing.
semanage permissive -a httpd_t # httpd_t now logs but does not block
semanage permissive -l # list permissive domains
semanage permissive -d httpd_t # back to enforcing
This is the right tool when an application has just been deployed and is generating denials you have not yet had time to fix. Per-domain permissive lets you finish the rollout, collect the full set of AVCs, and write the right policy module - without giving the rest of the system the gift of a free pass.
Common pitfalls
setenforce 0as a "quick fix". The fix is now your shipping configuration. Resist.- Running
audit2allowblindly. The output may "fix" a denial that was a legitimate block of a security violation. Read the rule before applying it. - Forgetting
-Ponsetsebool. The change reverts at reboot and the on-call at 03:00 will be very confused. - Editing files in
/etc/selinux/<policy>by hand. Always go throughsemanage/semodule; the binary policy files are not user-editable. - Per-host SELinux divergence. If one host has a custom module another does not, troubleshooting becomes painful. Ship modules through configuration management.
Audit checklist
- Every host is in
enforcingmode (1 pt) - No production host has
SELINUX=disabledin/etc/selinux/config(1 pt) - Custom modules live in a config-management repo with rationale (1 pt)
- AVC denials forwarded to the SIEM and triaged weekly (1 pt)
- Per-domain permissive is the only tool used during incidents - never global (1 pt)
5/5 = PASS, 3-4 = WARN, <3 = FAIL.
FAQ
Does this cost performance?
SELinux adds <1% on most workloads, measurable but not material. The cost of not having it during a breach is several orders of magnitude higher.
What about containers?
The container-selinux policy package supports both Docker and Podman. Containers run in a confined domain (container_t) and the host policy applies on top. Keep both layers enforcing.
Does Ubuntu support this?
Ubuntu ships AppArmor by default; SELinux is available but not preinstalled. Most of this guide applies to RHEL, CentOS Stream, AlmaLinux, Rocky and Fedora.
How do I know if I need a custom module?
Run a denied behaviour and look at the AVC. If the only sensible answer to "should this be allowed" is yes, write a module. If the answer is no, fix the application.
Can I share modules across the fleet?
Yes, and you should. Build the .pp in CI from the .te source, ship via Ansible/Puppet, install with semodule -i on every host that needs it.