The Ultimate Guide to SELinux: From “Permission Denied” to Policy Master

The CyberSec Guru

The Ultimate Guide to SELinux

If you like this post, then please share it:

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Your contribution powers free tutorials, hands-on labs, and security resources that help thousands defend against digital threats.

Why your support matters:

  • Zero paywalls: Keep HTB walkthroughs, CVE analyses, and cybersecurity guides 100% free for learners worldwide
  • Community growth: Help maintain our free academy courses and newsletter

Perks for one-time supporters:
☕️ $5: Shoutout in Buy Me a Coffee
🛡️ $8: Fast-track Access to Live Webinars
💻 $10: Vote on future tutorial topics + exclusive AMA access

If opting for membership, you will be getting complete writeups much sooner compared to everyone else!

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

  • 100% creator-owned platform (no investors)
  • 95% of funds go directly to content (5% payment processing)
Buy Me a Coffee Button

Discover more from The CyberSec Guru

Subscribe to get the latest posts sent to your email!

Let’s be honest. If you’re a system administrator, a DevOps engineer, or just a Linux enthusiast, your first encounter with SELinux (Security-Enhanced Linux) was probably not a happy one. You were setting up a web server. Everything looked perfect. The files were in place, Apache was running, and curl localhost gave you… a 403 Forbidden. Or maybe you saw the dreaded “Permission Denied” in /var/log/httpd/error_log.

You checked file permissions. You ran chmod -R 755 /var/www/html. Nothing. In a fit of desperation, you ran chmod -R 777 /var/www/html. Still nothing.

Then, you found it. A post on a forum. The answer was one simple command: setenforce 0.

You ran it, and like magic, your website loaded. You sighed in relief, added SELINUX=disabled to /etc/selinux/config, rebooted, and never thought about it again.

We have all done this. But in doing so, we’ve thrown away the single most powerful and effective security tool built into the Linux kernel. We’ve unbolted the bank vault door because we couldn’t figure out the combination.

This is the guide I wish I had. This is the 10,000-word masterclass that will take you from “disabling it” to understanding it. We’re not just going to learn commands; we’re going to learn the philosophy. By the end of this guide, SELinux will no longer be a magical black box of “no.” It will be your most potent weapon for building secure, resilient, and bulletproof systems. You will be able to diagnose and fix any SELinux denial in minutes, and you’ll even be able to write your own custom security policies.

Strap in. It’s time to stop fearing SELinux and start mastering it.

The “Why?” – Unlearning Everything You Know About Linux Security

The biggest hurdle to learning SELinux is that it requires you to unlearn the security model you’ve used for decades. The model you know is called Discretionary Access Control (DAC).

The Old Way: Discretionary Access Control (DAC)

DAC is the security model of “ownership.” In the Linux world, this is your standard user, group, and other permissions.

  • You (the user) own a file, file.txt.
  • You decide who gets to access it. You run chmod 600 file.txt.
  • This means only the user (you) can read/write, and the group and other can do nothing.
  • The discretion is yours.

This model has a fundamental, catastrophic flaw: processes inherit the permissions of the user who runs them.

Let’s say you’re running Apache as the apache user. An attacker finds a vulnerability in a PHP script and gets a reverse shell. That attacker is now “running” as the apache user. What can they do? They can do anything the apache user can do.

They can read and write to all web directories (/var/www/html). They can run any program the apache user can (curl, wget, bash). If the apache user can read /etc/some_config.conf, the attacker can, too.

Now, what happens if the attacker finds a privilege escalation bug and becomes root?

It’s game over.

With DAC, the root user is God. Root can read any file, write to any file, kill any process, and load any kernel module. A single compromised process that achieves root privileges owns the entire system. This is the “hard outer shell, soft gooey center” model of security.

Discretionary Access Control
Discretionary Access Control

The New Way: Mandatory Access Control (MAC)

SELinux implements Mandatory Access Control (MAC). This is a completely different philosophy.

In a MAC world, access is not determined by user ownership. Access is determined by a central, system-wide policy written by the administrator (or, in most cases, by the OS vendor).

This policy dictates everything. It says:

  • “A process running as a web server is only allowed to read files labeled as ‘web content’.”
  • “A web server process is never allowed to write to /etc/passwd, even if it is running as root.”
  • “A web server process is only allowed to listen on ports 80 and 443.”
  • “A web server process is not allowed to run /bin/bash.”

With MAC, even if an attacker compromises your Apache process, they are trapped in a tiny sandbox.

  • They get a shell. They try to cat /etc/passwd. The kernel stops them. SELinux: Denied.
  • They try to run wget to download their rootkit. The kernel stops them. SELinux: Denied.
  • They try to write a file to /tmp (a common staging ground). The kernel stops them. SELinux: Denied.
  • They try to connect to their command-and-control server on port 6666. The kernel stops them. SELinux: Denied.

The attacker is “running as” the apache user, but the apache user itself is confined by SELinux policy. The compromised process is effectively neutered.

This is the most important concept to grasp: With SELinux, root is no longer God. A process running as root is still subject to the SELinux policy like everyone else. The policy is the true administrator of the machine.

Mandatory Access Control
Mandatory Access Control

The Core Components of SELinux

How does it actually work? It’s all about labels.

  1. Subjects vs. Objects:
    • A Subject is a process (e.g., the Apache httpd process).
    • An Object is the thing a subject wants to access (e.g., a file, a directory, a network port, a system socket).
  2. Labels (Contexts):
    • Every single Subject and every single Object on an SELinux system has a label. This label is called an SELinux Context.
    • A process’s label is called a domain.
    • A file’s label is called a type.
  3. The Policy:
    • The SELinux policy is just a massive database of rules that says which domains can access which types.
    • A rule might say: “Allow the domain httpd_t (the Apache process) to read files with the type httpd_sys_content_t (web content).”
  4. Type Enforcement (TE):
    • This is the name of the engine that enforces these rules. When a Subject (process) tries to access an Object (file), the Linux kernel’s hook for SELinux checks their labels.
    • It looks up the rule in the policy.
    • If a rule allow httpd_t httpd_sys_content_t:file { read }; exists, the access is granted.
    • If no “allow” rule exists, the access is implicitly denied, and an event is logged to /var/log/audit/audit.log.

That’s it. That is the entire theory of SELinux. Every “Permission Denied” error you’ve ever seen is just the kernel’s Type Enforcement engine saying, “I looked in the policy, and there is no rule that allows the domain of that process to access the type of that file.”

The Three Modes: Your System’s “Attitude”

SELinux can operate in one of three modes. You must know what they are and how to switch between them.

You can check the current mode at any time with the getenforce command.

$ getenforce
Enforcing

A more detailed view is provided by sestatus:

$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      33

1. Enforcing Mode

This is the default, the standard, and the mode your systems should always be in.

  • What it does: The SELinux policy is enforced.
  • Denials: Access is blocked, and the denial is logged to /var/log/audit/audit.log.
  • This is the mode that causes “Permission Denied” errors when things are misconfigured.

2. Permissive Mode

This is your #1 troubleshooting tool.

  • What it does: The SELinux policy is NOT enforced.
  • Denials: Access is ALLOWED, but the denial is still logged to /var/log/audit/audit.log as if it would have been blocked.
  • Why it’s so useful: If you’re having a problem, you can temporarily switch to Permissive mode. If the problem goes away, you know 100% it’s an SELinux issue. You can then check the audit.log to see what would have been denied, and you can fix the policy while the application is still working.

3. Disabled Mode

This is the mode you should never use.

  • What it does: The SELinux kernel module is not loaded. The system runs as if SELinux doesn’t exist (pure DAC).
  • Why it’s bad:
    1. You have zero MAC security.
    2. When the system is Disabled, files are created on disk without any SELinux context labels.
    3. If you later try to re-enable SELinux, your system will be unbootable. Why? Because critical files like /sbin/init will have no labels, and the SELinux policy will deny init from reading… well, anything.
    4. The only way to recover from Disabled is to boot into a rescue shell and run a full-system autorelabel, which can take hours. touch /.autorelabel and reboot.

How to Change Modes

Temporarily (Stays until next reboot):

This is perfect for troubleshooting.

# Switch to Permissive mode
$ setenforce 0

# Check the mode
$ getenforce
Permissive

# Switch back to Enforcing mode
$ setenforce 1

# Check the mode
$ getenforce
Enforcing

Note: 0 = Permissive, 1 = Enforcing. You can’t switch to Disabled without a reboot.

Permanently (Requires a reboot):

You do this by editing the main SELinux configuration file.

$ nano /etc/selinux/config

You will see this line:

# 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

You can change SELINUX=enforcing to SELINUX=permissive.

DO NOT EVER CHANGE THIS TO SELINUX=disabled. If you want to turn off SELinux, use permissive. It gives you the same “it doesn’t block anything” behavior but doesn’t destroy your file labels, making it safe to re-enable later.

After editing this file, you must reboot for the change to take effect.

The Heart of SELinux – Contexts and Labels

This is it. This is the 90% of SELinux that confuses people. If you understand this part, you’ve mastered the core concept.

As we said, every process (Subject) and file (Object) has a label, called a context.

An SELinux context is a string with four parts, separated by colons: user:role:type:level

  • User: An SELinux user (e.g., system_u, unconfined_u). This is not the same as your Linux user (root, john). It’s an identity within the policy.
  • Role: Used in Role-Based Access Control (RBAC). (e.g., system_r, object_r).
  • Level: Used in Multi-Level Security (MLS) and Multi-Category Security (MCS). (e.g., s0, or s0:c0,c1). This is what keeps containers (Docker, Podman) separate from each other.

For 99% of all system administration, you can COMPLETELY IGNORE the user, role, and level.

The only part that matters for day-to-day troubleshooting is the third part: the type.

This is the real label.

  • For a process (Subject), the type is its domain.
  • For a file (Object), the type is its type.

Let’s see it in action.

Viewing File Contexts with ls -Z

The standard ls -l command has a big brother: ls -Z. The -Z flag tells ls to show the SELinux context.

Let’s look at some files on a standard RHEL/CentOS system:

$ ls -Z /
system_u:object_r:root_t:s0       /
system_u:object_r:bin_t:s0        /bin
system_u:object_r:boot_t:s0       /boot
...
system_u:object_r:etc_t:s0        /etc
system_u:object_r:home_root_t:s0  /home
...
system_u:object_r:var_t:s0        /var

$ ls -Z /var/www/html/
system_u:object_r:httpd_sys_content_t:s0  index.html

$ ls -Z /var/log/
system_u:object_r:var_log_t:s0      audit
system_u:object_r:var_log_t:s0      messages
system_u:object_r:httpd_log_t:s0    httpd

$ ls -Z /etc/passwd
system_u:object_r:passwd_file_t:s0  /etc/passwd

Look at the third field (the type).

  • The Apache web root /var/www/html has the type httpd_sys_content_t. This is the “web content” label.
  • The Apache log directory /var/log/httpd has the type httpd_log_t. This is the “web server logs” label.
  • The system-wide log directory /var/log has the type var_log_t.
  • The /etc/passwd file has the type passwd_file_t.

Viewing Process Contexts with ps -Z

The ps command also has a -Z flag.

$ ps -eZ | grep httpd
system_u:system_r:httpd_t:s0      1234 ?        00:00:01 httpd
system_u:system_r:httpd_t:s0      1235 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0      1236 ?        00:00:00 httpd

$ ps -eZ | grep sshd
system_u:system_r:sshd_t:s0-s0:c0.c1023 1100 ?  00:00:00 sshd

$ ps -eZ | grep auditd
system_u:system_r:auditd_t:s0     800 ?         00:00:02 auditd

Look at the type (domain) for these processes:

  • The Apache processes are running in the httpd_t domain.
  • The SSH daemon is running in the sshd_t domain.
  • The audit daemon is running in the auditd_t domain.

Putting It All Together: The “Permission Denied” Moment

Now, let’s walk through the scenario.

  1. A request comes in to your Apache server.
  2. A Subject (a process in the httpd_t domain) needs to read an Object (the file /var/www/html/index.html).
  3. The kernel’s Type Enforcement (TE) engine wakes up.
  4. It checks the process label: httpd_t.
  5. It checks the file label: httpd_sys_content_t.
  6. It consults the policy: “Is there an allow rule for httpd_t to read files of type httpd_sys_content_t?”
  7. Answer: Yes. The policy has this rule: allow httpd_t httpd_sys_content_t:file { read getattr open };.
  8. The kernel grants access. The page is served.

Now, let’s see the “Permission Denied” moment.

  1. Your web app tries to write to its log file at /var/log/httpd/error_log.
  2. Subject: httpd_t process.
  3. Object: /var/log/httpd directory, which has the type httpd_log_t.
  4. Policy Check: “Is there an allow rule for httpd_t to write to files of type httpd_log_t?”
  5. Answer: Yes. The policy allows this. The log is written.

Now, the real “Permission Denied” moment.

  1. Your poorly coded web app tries to write a log file to /tmp/app.log.
  2. Subject: httpd_t process.
  3. Object: /tmp directory, which has the type tmp_t.
  4. Policy Check: “Is there an allow rule for httpd_t to write to files of type tmp_t?”
  5. Answer: No. The policy does not allow this. Why would a web server need to write to the general-purpose temp directory? This is suspicious behavior (often used by malware).
  6. The kernel DENIES the access.
  7. The kernel logs the denial to /var/log/audit/audit.log.
  8. Your application fails.

This is the key. Your chmod permissions were irrelevant. The apache user probably could write to /tmp (which is often world-writable). But the httpd_t process could not, because the policy forbade it.

The Daily Workflow – Managing SELinux with Booleans

So, what if your web application legitimately needs to do something the policy forbids? For example, what if your web app needs to connect to a remote database server on port 5432?

  1. Subject: httpd_t process.
  2. Object: A network socket.
  3. Action: connect.
  4. Policy Check: “Is there an allow rule for httpd_t to make outbound network connections?”
  5. Answer: No. By default, the policy is tight. A web server’s job is to serve content, not make random outbound connections. This is denied.

You have two choices:

  1. (Wrong) Write a custom policy module to allow this.
  2. (Right) Check if there’s an “off” switch for this rule.

This “off” switch is called an SELinux Boolean.

Booleans are on/off toggles for common policy rules. They are the first thing you should check when you have a denial.

How to List and Find Booleans

To list all booleans on your system (there will be hundreds):

$ getsebool -a

The output will look like this:

ftpd_anon_write --> off
ftpd_full_access --> off
ftpd_use_passive_mode --> off
httpd_anon_write --> off
httpd_builtin_scripting --> on
httpd_can_check_spam --> off
httpd_can_network_connect --> off
httpd_can_network_connect_db --> off
httpd_can_network_relay --> off
...and many more

This is a lot. Let’s filter it for just Apache (httpd):

$ getsebool -a | grep httpd

And there, we see our culprit: httpd_can_network_connect_db --> off

The name is perfectly descriptive. “Can the httpd process connect to a database?” And it’s set to off.

How to Change Booleans

We need to flip this switch to on.

To change it temporarily (does not survive reboot): This is great for testing if this is actually the fix.

$ setsebool httpd_can_network_connect_db on

(You can also use 1 for on and 0 for off)

Now, re-run your application. Does it connect to the database? Yes? Congratulations, you’ve found the problem.

To change it permanently (survives reboot): Once you’ve confirmed the fix, you add the -P (Permanent) flag.

$ setsebool -P httpd_can_network_connect_db on

This command will take a few seconds. It’s not just changing a variable; it’s recompiling the local policy on your disk. Once it’s done, this setting is permanent.

Your troubleshooting workflow for 50% of all SELinux problems should be:

  1. Get a denial.
  2. Run getsebool -a | grep service_name.
  3. Read the list of booleans and find one that sounds like the action being denied.
  4. Temporarily flip it with setsebool.
  5. Test. If it works, make it permanent with setsebool -P.

The #1 Job – Fixing “Permission Denied” Errors (The Audit Log)

What about the other 50% of problems? The problem is almost always a file context mismatch. This is where the troubleshooting workflow becomes a science.

The Scenario: You’re a developer. You’ve been working on new_website.tar.gz in your home directory, /home/dev/. You extract it. Then, as root, you mv /home/dev/new_website /var/www/html/.

You refresh your browser. 403 Forbidden.

You run setenforce 0. The site loads. You run setenforce 1. The site breaks.

It’s 100% an SELinux problem. But getsebool -a | grep httpd shows no obvious boolean. The problem isn’t what Apache is doing; it’s what it’s accessing.

Step 1: Check the Audit Log, The Easy Way (sealert)

Your first stop should always be the SELinux audit log. But /var/log/audit/audit.log is a raw, unreadable mess.

That’s why we have setroubleshoot-server. This package (which you should yum install or dnf install if it’s not present) provides a tool called sealert. It reads the raw audit log, aggregates all the denials, and gives you a human-readable report.

This is the command:

$ sealert -a /var/log/audit/audit.log

(Run this as root)

The output will be a gift from the heavens:

100% done
found 1 alerts in /var/log/audit/audit.log
--------------------------------------------------------------------------------

SELinux is preventing /usr/sbin/httpd from getattr access on the file /var/www/html/new_website/index.html.

***** Plugin restorecon (99.5 confidence) suggests   *************************

If you want to fix the label.
/var/www/html/new_website/index.html default label should be httpd_sys_content_t.
Then you can run restorecon.
Do
# restorecon -v /var/www/html/new_website/index.html

***** Plugin httpd_can_network_connect (0.5 confidence) suggests   *********
...

Look at that! It tells you exactly what happened and exactly how to fix it.

  • What: httpd was denied getattr (get attributes, a basic read) on your new index.html file.
  • Why (implied): The file has the wrong label.
  • Confidence: It’s 99.5% sure this is a labeling problem.
  • The Fix: It tells you the default label should be httpd_sys_content_t and gives you the exact command to run: restorecon -v /var/www/html/new_website/index.html.

This is why you don’t need to be afraid. The system tells you how to fix it.

Step 2: Understanding the Problem (Why sealert is right)

Why did this happen? When you moved the file (mv /home/dev/new_website /var/www/html/), the file retained its original SELinux context.

Let’s check with ls -Z:

$ ls -Z /home/dev/new_website/index.html
unconfined_u:object_r:user_home_t:s0 /home/dev/new_website/index.html

The file had the type user_home_t.

When you moved it:

$ ls -Z /var/www/html/new_website/index.html
unconfined_u:object_r:user_home_t:s0 /var/www/html/new_website/index.html

It still has the type user_home_t!

Now, let’s trace the denial:

  1. Subject: httpd_t process.
  2. Object: /var/www/html/new_website/index.html, which has type user_home_t.
  3. Policy Check: “Is there an allow rule for httpd_t to read files of type user_home_t?”
  4. Answer: NO! And thank goodness. If this rule existed, a compromised web server could read all user home directories.
  5. Result: Access denied.

The sealert tool correctly identified this and told us to fix the label.

(Side note: If you had copied the file (cp) instead of moving it (mv), the new file would have inherited the context of the destination directory /var/www/html, which is httpd_sys_content_t, and everything would have worked. mv preserves context, cp inherits context. This is a common trip-up.)

$ sealert -l 8d6f4a2b-3c5e-4d7a-9b1f-2e8c6a5d4b3c

SELinux is preventing /usr/sbin/httpd from open access on the file /var/www/html/index.html.

*****  Plugin restorecon (99.5 confidence) suggests   ************************

If you want to fix the label. 
/var/www/html/index.html default SELinux type should be httpd_sys_content_t.
Then you can run restorecon. The access attempt may have been stopped due to insufficient permissions to access a parent directory in which case try to change the following command accordingly.
Do
# restorecon -v /var/www/html/index.html

*****  Plugin catchall (1.41 confidence) suggests   **************************

If you believe that httpd should be allowed open access on the index.html file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'httpd' --raw | audit2allow -M my-httpd
# semodule -X 300 -i my-httpd.pp


Additional Information:
Source Context                system_u:system_r:httpd_t:s0
Target Context                unconfined_u:object_r:user_home_t:s0
Target Objects                /var/www/html/index.html [ file ]
Source                        httpd
Source Path                   /usr/sbin/httpd
Port                          <Unknown>
Host                          localhost.localdomain
Source RPM Packages           httpd-2.4.57-1.el9.x86_64
Target RPM Packages           
SELinux Policy RPM            selinux-policy-targeted-38.1.23-1.el9.noarch
Local Policy RPM              selinux-policy-targeted-38.1.23-1.el9.noarch
Selinux Enabled               True
Policy Type                   targeted
Enforcing Mode                Enforcing
Host Name                     localhost.localdomain
Platform                      Linux localhost.localdomain 5.14.0-362.el9.x86_64
                              #1 SMP PREEMPT_DYNAMIC Mon Nov 6 12:21:16 UTC 2023
                              x86_64 x86_64
Alert Count                   3
First Seen                    2025-10-26 14:32:18 IST
Last Seen                     2025-10-26 14:35:42 IST
Local ID                      8d6f4a2b-3c5e-4d7a-9b1f-2e8c6a5d4b3c

Raw Audit Messages
type=AVC msg=audit(1729935942.856:245): avc:  denied  { open } for  pid=2341 comm="httpd" path="/var/www/html/index.html" dev="dm-0" ino=67891234 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

Fixing Contexts: chcon vs. restorecon (The Golden Rule)

You have two commands to fix file contexts. One is a temporary, “quick fix” that you should avoid. The other is the “golden” command that fixes it permanently and correctly.

The Quick Fix (The “Wrong” Way): chcon

chcon is “change context.” It works just like chmod or chown.

# chcon -t httpd_sys_content_t /var/www/html/new_website/index.html

This will manually change the file’s type to httpd_sys_content_t. If you run ls -Z, it will look correct. Your website will start working.

So what’s the problem? This change is temporary. It’s just a manual override. If your system ever runs a system-wide autorelabel (which can happen during an OS update or if you run it manually), the system will look at the “master policy,” see that /var/www/html/new_website/index.html still has a manual override, and “fix” it back to what it thinks it should be. Or worse, if the “master policy” doesn’t even know about /var/www/html/new_website, it might revert it to a default.

chcon is the chmod 777 of SELinux. It’s a sign you don’t know the right way.

The Correct Fix (The Golden Rule): restorecon

restorecon is “restore context.” This is the command sealert told us to use.

# restorecon -vR /var/www/html/new_website
  • -v: Verbose (shows you what it’s changing).
  • -R: Recursive.

What is restorecon doing? It’s not just changing a label. It’s:

  1. Looking at the path /var/www/html/new_website.
  2. Consulting the system’s “master list” of file contexts (the “truth”).
  3. This master list has a rule that says "/var/www/html(/.*)?" (i.e., /var/www/html and everything under it) should have the type httpd_sys_content_t.
  4. restorecon sees your files have the type user_home_t.
  5. It “restores” them to the correct, master-policy-defined type.

This fix is permanent. It will survive relabels, reboots, and updates, because you haven’t just changed a label; you’ve aligned the file’s label with the system’s master policy.

Your troubleshooting workflow for 99% of all SELinux problems should be:

  1. See a denial.
  2. Run sealert -a /var/log/audit/audit.log.
  3. If it mentions restorecon, run the command it gives you.
  4. Problem solved.

What if I Have a New Directory? The semanage fcontext Workflow

This is the final piece of the “contexts” puzzle.

The Scenario: You don’t want to host your website in /var/www/html. You’re a modern admin. You want to host it from /srv/my_awesome_app.

You create the directory, put your files in it, and configure Apache. You get a 403 Forbidden. You run sealert. It says to run restorecon -vR /srv/my_awesome_app. You run it. And… nothing happens. restorecon runs, but changes no labels.

Why? Because restorecon looked in the “master list” for /srv/my_awesome_app, and… it found nothing. The master policy has no rules for this directory. So restorecon shrugged and did nothing. Your files still have their default type (probably var_t or whatever /srv is, which httpd_t can’t read).

You need to teach SELinux about your new directory. You need to add a rule to the master list.

The command for this is semanage (SELinux Manage).

Step 1: Tell SELinux about your new file context. We will add (-a) a file context (fcontext) rule. The type (-t) should be httpd_sys_content_t (web content), and the path is "/srv/my_awesome_app(/.*)?".

# semanage fcontext -a -t httpd_sys_content_t "/srv/my_awesome_app(/.*)?"
  • semanage fcontext: We are managing the file context “master list.”
  • -a: Add a new rule.
  • -t httpd_sys_content_t: The type we want to assign.
  • "/srv/my_awesome_app(/.*)?": The path. This is a regular expression that means “the directory /srv/my_awesome_app and (/) everything inside it (.*) whether the slash is there or not (?)”.

This command adds your new rule to the master list.

Step 2: Apply the new rule. Now that the master list is updated, now you can run restorecon.

# restorecon -vR /srv/my_awesome_app

This time, restorecon will:

  1. Look at /srv/my_awesome_app.
  2. Consult the master list.
  3. Find your shiny new rule!
  4. It will see the files are not httpd_sys_content_t and will “fix” them all.

Your site is now live. And the fix is 100% correct, permanent, and follows SELinux best practices.

The Last Resort – audit2why and audit2allow

What if sealert has no suggestion? What if there’s no boolean? What if the file contexts are correct, but you’re still getting a denial?

This means your process is trying to do something truly unexpected that the policy authors never anticipated. This is common with custom, third-party applications.

This is when you have to read the raw log and, potentially, write your own custom policy.

Step 1: audit2why (Read the denial)

audit2why is a tool that reads a raw audit log entry and translates it to English.

First, find the raw log entry.

# grep "denied" /var/log/audit/audit.log | tail -n 1

This will give you a huge, ugly line of text. Copy it.

Now, feed it to audit2why:

# grep "denied" /var/log/audit/audit.log | tail -n 1 | audit2why

The output will be something like:

type=AVC msg=audit(1666795864.453:123): avc:  denied  { read } for  pid=4567 comm="my_custom_app" path="/var/custom/data.bin" dev="dm-0" ino=12345 scontext=system_u:system_r:my_app_t:s0 tcontext=system_u:object_r:var_lib_t:s0 tclass=file permissive=0

audit2why will read this and tell you:

“The process my_custom_app (running in the my_app_t domain) was denied read access to the file /var/custom/data.bin (which has the type var_lib_t).”

Okay, so your my_app_t process needs to read var_lib_t files. This is a legitimate need for your app. The policy doesn’t allow it. You have to create a new allow rule.

Step 2: audit2allow (Create the new rule)

You could learn to write Type Enforcement policy files (.te), compile them with checkmodule, package them into a policy package (.pp), and load them with semodule.

Or, you can use audit2allow, which does all of that for you.

audit2allow reads the audit log, finds the denials, and auto-generates the exact policy module you need to allow them.

The Workflow:

1. Generate the Type Enforcement (.te) file: First, let’s just see what rule it would create.

# grep "my_app_t" /var/log/audit/audit.log | audit2allow -m myCustomAppPolicy
  • -m myCustomAppPolicy: Names our new module.

This will output the .te file to your screen:

module myCustomAppPolicy 1.0;

require {
	type my_app_t;
	type var_lib_t;
	class file { read getattr open };
}

#============= my_app_t ==============
allow my_app_t var_lib_t:file { read getattr open };

That’s it. It’s a new “allow” rule.

2. Create and Install the Module (The “Magic” Command): Now, let’s tell audit2allow to actually build and load this module. We use the -M flag.

# grep "my_app_t" /var/log/audit/audit.log | audit2allow -M myCustomAppPolicy

This command will:

  1. Create myCustomAppPolicy.te (the human-readable rule).
  2. Create myCustomAppPolicy.pp (the compiled policy package).
  3. Immediately load the package into the kernel.

It will print:

******************** IMPORTANT ***********************
To make this policy package active, execute:

semodule -i myCustomAppPolicy.pp

(Note: The -M flag often loads it, but if not, it gives you the command to run.)

Run the command it gives you:

# semodule -i myCustomAppPolicy.pp

You are done. You have just authored, compiled, and loaded a custom SELinux policy module. Your application will now work, and the fix is permanent and “SELinux-correct.”

A Word of Warning: audit2allow is a sledgehammer. It will allow whatever was denied. You must be sure that the denial you are allowing is safe and expected. If you just pipe your entire audit log into audit2allow, you’ll end up allowing malicious activity as well. Be specific. grep for the exact process (my_app_t) that you want to fix.

Advanced Concepts (Where to Go from Here)

You now have 99% of what you need for daily SELinux administration. The “master” workflow is:

  1. Is it Enforcing? (getenforce)
  2. What does sealert say?
  3. Is it a boolean? (getsebool, setsebool -P)
  4. Is it a context? (ls -Z, restorecon)
  5. Is it a new context? (semanage fcontext, restorecon)
  6. Is it a new policy? (audit2allow)

But there’s more to the user:role:type:level string.

Users and Roles (RBAC)

SELinux maps Linux users (like root) to SELinux users (like system_u or unconfined_u). In a default “targeted” policy, most users, including root, are mapped to unconfined_u. This means they run in the unconfined_t domain, which… is unconfined. This is the “get-out-of-jail-free” card that makes the system usable.

The processes that are started by systemd (like httpd_t, sshd_t) are not unconfined. They are “targeted.” This is why it’s called a targeted policy: system daemons are locked down, but user-run scripts are not.

You can run a much stricter policy (like mls) where every user, including root, is confined. This is for high-security environments and is beyond the scope of this guide.

Level (MLS/MCS) and Containers

This is the s0 or s0:c1,c2 part of the context. This is Multi-Category Security (MCS).

This is, simply, what keeps your Docker/Podman containers from killing each other.

When you start a container, it gets assigned a unique category, like c1,c2. When you start another container, it gets c3,c4.

The SELinux policy has a rule that says a process with category c1,c2 can only access files with category c1,c2.

This is why, even if an attacker breaks out of their container and onto the host, they are still trapped. They are a process with category c1,c2… and all the host’s files have a category of s0. The kernel denies them access to everything. This is true container isolation.

You Are Now an SELinux Administrator

SELinux is not a tool of “no.” It is a tool of explicit permission. It forces you, the administrator, to define exactly what your system is supposed to do, and it denies everything else.

You’ve learned that chmod 777 is a relic of the DAC world and that the real permissions are in the type context.

You’ve learned that the system wants to help you. sealert is your best friend, translating cryptic denials into copy-and-paste fixes.

You’ve learned the difference between a temporary fix (chcon) and the permanent, correct fix (restorecon).

You’ve learned how to teach SELinux new tricks, either with simple “on/off” switches (setsebool -P) or by adding new knowledge to its master list (semanage fcontext).

And finally, you’ve learned how to become a policy author yourself, using audit2allow to forge new rules when your applications need them.

The next time you see “Permission Denied,” don’t run setenforce 0.

Smile. Open a root shell. Run sealert -a /var/log/audit/audit.log. And in 30 seconds, you’ll have the fix.

You are no longer afraid of SELinux. You are in control.

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Your contribution powers free tutorials, hands-on labs, and security resources that help thousands defend against digital threats.

Why your support matters:

  • Zero paywalls: Keep HTB walkthroughs, CVE analyses, and cybersecurity guides 100% free for learners worldwide
  • Community growth: Help maintain our free academy courses and newsletter

Perks for one-time supporters:
☕️ $5: Shoutout in Buy Me a Coffee
🛡️ $8: Fast-track Access to Live Webinars
💻 $10: Vote on future tutorial topics + exclusive AMA access

If opting for membership, you will be getting complete writeups much sooner compared to everyone else!

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

  • 100% creator-owned platform (no investors)
  • 95% of funds go directly to content (5% payment processing)
Buy Me a Coffee Button

If you like this post, then please share it:

Tutorials

Discover more from The CyberSec Guru

Subscribe to get the latest posts sent to your email!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from The CyberSec Guru

Subscribe now to keep reading and get access to the full archive.

Continue reading