SELinux in enforcing, calmly
Read denials, write targeted modules, never disable.
Regular Linux file permissions work like handing out keys to your house. You own a file, so you decide who gets a key: read for the group, write for you, nothing for anyone else. This is DAC (Discretionary Access Control), because access is left to the discretion of the owner. It has one weakness. If a program running as you gets tricked into doing something bad, it already holds every key you hold. A hacked web server, running as the web-server user, can read whatever that user can read.
MAC (Mandatory Access Control) adds a second lock the owner cannot open. It works like a security guard posted inside the building who follows one fixed rulebook. It does not matter that you own the office, or that you are the CEO. If the rulebook says the mailroom clerk does not enter the server room, the clerk does not enter, and no key changes that. SELinux (Security-Enhanced Linux) is that guard on RHEL (Red Hat Enterprise Linux) and its relatives (Rocky, AlmaLinux, Fedora, CentOS Stream). The kernel (the core of the operating system that talks directly to the hardware) checks every access against the rulebook, and even root cannot argue it into a yes.
So a web server that gets fully taken over can still only do what the web server's rulebook allows: read its own content and answer requests. It cannot read /etc/shadow, wander into people's home directories, or open a reverse shell (a hijacked command line the attacker drives from across the internet), because nothing in the policy permits its type to do those things. That confinement holds even against code the attacker completely controls. Your job as the operator is to keep it on, read what it blocks, and grant back exactly the one thing a service legitimately needs, no more.
Check what mode you are in
Before touching anything, find out whether the guard is on duty. There are three modes. Enforcing means the rulebook is applied and violations are blocked. Permissive means violations are logged but allowed through, which is a debugging setting, not a safe place to live. Disabled means SELinux is off and labels are not even maintained. getenforce gives you the one-word answer.
sestatus gives the whole picture: which policy is loaded, the mode right now, and the mode the config file will boot into next time. Watch those last two. Someone can flip a box to permissive at runtime to unblock a problem and forget to note it, so 'Current mode' and 'Mode from config file' can disagree. Catching that gap is a real finding in a hardening review.
'Loaded policy name: targeted' is the one to understand. On the targeted policy, most of the system, including your login shell and admin tools, runs unconfined and is barely touched by SELinux. The confinement is aimed at the processes that actually get attacked: the network-facing daemons (long-running background programs) like the web server, the SSH login service, and the DNS (Domain Name System) server. That is a deliberate trade. SELinux is not trying to referee everything on the box; it is boxing in the services that face the internet.
The boot-time state lives in one plain file. If a runbook ever tells you to set SELINUX=disabled here, that is the line that removes the guard on the next reboot.
# This file controls the state of SELinux on the system.# SELINUX= can take one of these three values:# enforcing - SELinux security policy is enforced.# permissive - SELinux prints warnings instead of enforcing.# disabled - No SELinux policy is loaded.SELINUX=enforcing# SELINUXTYPE= can take one of these values:# targeted - Targeted processes are protected,# minimum - Modification of targeted policy. Only selected processes are protected.# mls - Multi Level Security protection.SELINUXTYPE=targeted
Labels are the whole game
Every process and every file wears a wristband, the kind you get at a festival that says which areas you are allowed into. SELinux calls that wristband a context, and it has four fields separated by colons: user, role, type, and level. Ignore three of them for now. The type is what the rulebook is written about. This is called type enforcement: the policy is a long list of rules shaped like 'a process of type A may perform action B on an object of type C', and anything not on that list is denied. Look at a web page's label with ls -Z.
The type here is httpd_sys_content_t, the label for content the web server is meant to serve. Now look at the running web server itself. Processes wear wristbands too, and you read them with ps -eZ.
The process runs as httpd_t. One rule in the policy says httpd_t may read httpd_sys_content_t, which is why the page loads. There is no rule letting httpd_t read shadow_t (the type on /etc/shadow) or ssh_home_t (the type on people's SSH keys). So if the web server is compromised and reaches for those files, the kernel says no and writes down exactly what it stopped. The attacker owns the process, but not the wristband, and the wristband is what the door checks.
When it breaks, read the denial
The guard keeps a logbook, and every time it turns someone away it records the who, the what, and the where. SELinux does the same. Every block becomes an AVC (Access Vector Cache) denial in the audit log. The name is internal plumbing; treat it as the guard's logbook entry. When something 'mysteriously' fails on an enforcing box, reading the last few entries is the entire diagnosis. Here is a denial you will meet in real life. You copied your site into place from a tarball (a .tar archive), or moved it with mv out of /root, so the files kept their old label. httpd_t is not allowed to read that old type, and the browser gets a 403 (the web server's Forbidden error). Pull the recent denials with ausearch.
Read the AVC line like a sentence. 'denied { read }' is the action that was blocked. comm="httpd" is the program. scontext ends in httpd_t, the source, which is the process asking. tcontext ends in admin_home_t, the target, which is the file's label. tclass=file says the target is a file. permissive=0 means enforcing actually blocked it (a 1 there would mean permissive mode let it through but still logged it). The tell is right there: the file is labeled admin_home_t, the type on things out of /root, not httpd_sys_content_t. The web server is behaving perfectly. The file is mislabeled. If you would rather not parse that by eye, pipe it to audit2why for the plain-English reason.
Three fixes, in order of preference
That last line is a trap for the impatient. audit2allow will happily write a rule to permit anything, including the exact access SELinux just blocked to protect you. Before you generate any policy, ask which of three fixes actually fits, and try them in this order.
The first question is always 'is this a labeling mistake?', because most denials are. In our case the file needs its correct label back. restorecon reads the system's default labeling rules and stamps the right type onto the file it finds in that location.
-R recurses into the directory, -v prints what changed. Reload the page and the 403 is gone, and you did not weaken the policy by an inch. If your content lives somewhere non-standard, say /srv/sites, teach the system that location's permanent label instead of fixing it by hand every deploy. semanage fcontext writes the rule; restorecon then applies it.
The second fix is for behavior that is legitimate but off by default. SELinux ships hundreds of on/off switches called booleans that toggle whole chunks of policy without any custom code. The classic case: your app server needs to reach a database on another host. By default httpd_t may not open outbound network connections, a sensible default that stops a hacked web app from scanning your network or phoning home. First list the network booleans and their current state.
Then turn on exactly the one you need. The -P flag makes it persistent so it survives a reboot. Skip -P and the switch reverts on the next boot, and your service breaks at the worst possible moment with no obvious cause.
Booleans are the safest fix because the distribution's engineers wrote and tested them. You are choosing from a supported menu, not writing new policy. Reach for a custom module only when the access is real, nothing is mislabeled, and no boolean covers it. audit2allow reads a denial and writes the allow rule that would have permitted it, then -M packages it under a name.
That command writes two files: my-app-fix.te, the human-readable rule, and my-app-fix.pp, the compiled package that gets loaded. Always read the .te first. This is the step people skip, and it is the one that matters.
module my-app-fix 1.0;require {type httpd_t;type admin_home_t;class file read;}#============= httpd_t ==============allow httpd_t admin_home_t:file read;
Read that as an operator. It says 'let the web server read files labeled admin_home_t', which is files out of /root. That is almost certainly wrong. The file should have been relabeled, not the policy widened to let an internet-facing daemon read root's home directory. Loading this module would take the exact hole SELinux caught and make it permanent and official. Seeing the rule in plain text is what stops you from rubber-stamping it. When the rule genuinely is what you want, load it with semodule and confirm it registered.
Verify, then move on
After any change, prove it worked instead of hoping. Reproduce the action that failed and check that the log stays quiet.
No new denials plus the feature actually working is your green light. Two things fool people here. First, some noisy denials are hidden on purpose by dontaudit rules that ship with the policy, so a program can misbehave while the log looks empty. If you suspect that, reveal the hidden ones with 'semodule -DB', reproduce the problem, then restore them with 'semodule -B'. Second, confirm you are still enforcing and did not quietly land in permissive, where everything works and nothing is protected.
setsebool httpd_can_network_connect_db on to let the web server reach a database, but you leave off the -P flag. What actually happens?sestatus shows Current mode: permissive and Mode from config file: enforcing. What does this tell you?One habit catches these before your users do. restorecon takes a -n flag for a dry run: it reports what is mislabeled without changing a thing. Point it at your service directories after every deploy and you find the next 403 while it is still cheap to fix.
Try this
Work through “Verify, then move on” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: a denial is the system working, not a switch to flip off. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.