SELinux doesn’t just label files; it orchestrates a complex dance of permissions that can, and often does, prevent even root from doing what you think it should.

Let’s watch SELinux in action. Imagine you have a web server, Apache, running on a system with SELinux enabled. By default, Apache is confined to a specific security context, usually httpd_t. It can only read and write files labeled with contexts it’s allowed to interact with, like httpd_sys_content_t for web content.

# Simulate serving a file that Apache shouldn't touch
sudo setenforce 0 # Temporarily disable SELinux to copy a file
sudo cp /etc/shadow /var/www/html/shadow.txt
sudo restorecon -Rv /var/www/html/shadow.txt # Apply default httpd content context
sudo setenforce 1 # Re-enable SELinux

# Now, try to access it via the web server
# curl http://localhost/shadow.txt
# You'll likely get a 403 Forbidden, even though the file is world-readable

Why the 403? SELinux is enforcing its policy. Apache, running as httpd_t, is denied permission to read a file labeled shadow_t (the default context for /etc/shadow). This is the core of SELinux: it’s not just about Unix permissions (owner, group, other), but about the type of the process and the type of the resource it’s trying to access.

The problem SELinux solves is fine-grained access control beyond traditional Unix permissions. It prevents a compromised web server from reading sensitive system files, or a mail server from writing to arbitrary locations. It’s a mandatory access control (MAC) system, meaning the system enforces the policy, not just the user.

Internally, SELinux works by assigning security contexts (labels) to every process and every object (files, sockets, devices). These contexts are composed of a user, role, and type. The most critical part for policy writing is the type. When a process tries to access an object, SELinux checks the policy rules to see if the process’s type is allowed to perform the requested action on the object’s type.

The levers you control are primarily:

  • File Labeling: Assigning the correct security context to files and directories.
  • Policy Booleans: Toggling specific SELinux behaviors on or off without recompiling policies.
  • Custom Policies: Writing new rules to allow specific, otherwise forbidden, interactions.

Let’s say you want to run a custom application that needs to bind to a privileged port (e.g., 8080, which is not the default httpd port) and serve content from a non-standard directory.

First, you’d identify the current SELinux context of your application’s executable and its data directory.

# Assuming your app is at /opt/myapp/myapp
ls -Z /opt/myapp/myapp
# Example output: unconfined_u:object_r:user_home_t:s0 /opt/myapp/myapp

# And its data directory at /opt/myapp/data
ls -Zd /opt/myapp/data
# Example output: unconfined_u:object_r:user_home_t:s0 /opt/myapp/data

The user_home_t context is too restrictive. We need to assign it a type that SELinux understands as something an application can serve from, or a custom type. For simplicity, let’s use a predefined type like httpd_sys_content_t if it’s serving web content, or create a new type. We’ll also need to allow the process to bind to a non-standard port.

If your application were to be managed by systemd, you’d typically run it in its own domain. For this example, let’s assume we need to allow httpd_t to access /opt/myapp/data.

# Allow httpd_t to read and write to /opt/myapp/data
sudo semanage fcontext -a -t httpd_sys_content_t "/opt/myapp/data(/.*)?"
sudo restorecon -Rv /opt/myapp/data

This command adds a rule (-a) that associates the httpd_sys_content_t type with files and directories under /opt/myapp/data. restorecon then applies this rule, relabeling the files.

Now, if your application needs to bind to port 8080, and httpd_t is not allowed by default:

# Check if port 8080 is allowed for httpd_t
sudo semanage port -l | grep http
# You'll likely see 80, 443, etc., but not 8080 for httpd_t

# Allow httpd_t to bind to port 8080
sudo semanage port -a -t http_port_t -p tcp 8080

This adds a rule allowing the http_port_t type (which httpd_t is associated with) to bind to TCP port 8080.

If these steps don’t cover your specific need, you’ll write custom policies. The audit2allow tool is invaluable here. Run your application, trigger the denial, and then examine the audit logs:

sudo ausearch -m AVC,USER_AVC -ts recent
# This will show SELinux denials. Pipe this output to audit2allow.

# Example:
sudo ausearch -m AVC,USER_AVC -ts recent | sudo audit2allow -M myapp
# This creates two files: myapp.te (the policy source) and myapp.pp (the compiled policy module).
# You then install it:
sudo semodule -i myapp.pp

The myapp.te file will contain rules like allow httpd_t self:tcp_socket { name_connect }; or allow httpd_t myapp_data_t:dir { search read write add_name remove_name create };. You can edit this .te file to refine the permissions before compiling.

The most surprising truth is that SELinux policies are not just about granting permissions, but about denying everything else by default. The entire system is built around a whitelist of allowed actions. When you write a policy, you’re explicitly carving out exceptions to a very strict, system-wide default "deny all" stance. This means even if a file has rwxr-xr-x permissions, SELinux can still block access if the type context doesn’t match an allowed interaction.

The next hurdle is understanding how to manage SELinux contexts across different distributions or when dealing with complex network configurations.

Want structured learning?

Take the full Cdk course →