SELinux (Security-Enhanced Linux) is a mandatory access control system built into the kernel of Red Hat Enterprise Linux and its rebuilds. Every process and file carries a security label, and the policy decides which labels may interact, so a compromised web server cannot read files outside what the policy grants it even when running as root. In this tutorial you will learn the SELinux modes on Rocky Linux 9 and, using Nginx as a practical example, fix the three most common causes of denials: file contexts, booleans and port labels.
Prerequisites
To follow this tutorial, you need:
- A server running Rocky Linux 9 (AlmaLinux 9 and RHEL 9 behave the same), such as a CubePath VPS.
- A non-root user with
sudoprivileges. - Basic familiarity with editing files and managing services with
systemctl.
SELinux is enabled in enforcing mode on a default Rocky Linux 9 installation. Ubuntu and Debian use AppArmor instead, so this guide does not apply to them.
Step 1 - Checking the SELinux status
Start by seeing how SELinux is running:
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
The important lines are Current mode, which is what applies right now, and Mode from config file, which is what applies after a reboot. Loaded policy name: targeted means the default policy: network-facing services such as Nginx, SSH or MariaDB run in confined domains, and everything else runs unconfined.
SELinux has three modes:
| Mode | Behaviour |
|---|---|
enforcing | The policy is applied: disallowed actions are blocked and logged. |
permissive | Nothing is blocked, but actions that would be denied are logged. Useful for diagnosing. |
disabled | SELinux is not loaded and files are not labelled. |
getenforce prints only the current mode, which is handy in scripts.
Step 2 - Switching between enforcing and permissive
Switch to permissive mode temporarily, for example to check whether SELinux is what breaks an application:
sudo setenforce 0
getenforce
Permissive
Go back to enforcing:
sudo setenforce 1
getenforce
Enforcing
setenforce does not survive a reboot. The persistent mode is set in /etc/selinux/config:
sudo nano /etc/selinux/config
SELINUX=enforcing
SELINUXTYPE=targeted
Keep SELINUX=enforcing on production servers. If an application does not work, the fix is almost always one of the adjustments in Steps 4 to 6, not turning SELinux off.
NoteOn Rocky Linux 9,
SELINUX=disabledin the config file no longer fully disables SELinux at boot; it only stops the policy from loading. If you truly need to disable it, the supported method is the kernel argumentsudo grubby --update-kernel ALL --args selinux=0. When you later re-enable SELinux, relabel the whole filesystem withsudo touch /.autorelabeland reboot, because files created while it was disabled have no labels.
Step 3 - Installing the management tools and reading contexts
The semanage command and the sealert analyser come in separate packages. Install them, along with Nginx for the examples:
sudo dnf install policycoreutils-python-utils setroubleshoot-server nginx
sudo systemctl enable --now nginx
Open HTTP in the firewall:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
Every file and process has a context of the form user:role:type:level. In the targeted policy, the type (ending in _t) is what matters. Display file contexts with ls -Z:
ls -Z /usr/share/nginx/html/index.html
system_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/html/index.html
And process contexts with ps -Z:
ps -eZ | grep nginx
system_u:system_r:httpd_t:s0 3120 ? 00:00:00 nginx
system_u:system_r:httpd_t:s0 3121 ? 00:00:00 nginx
Nginx runs in the httpd_t domain (the same one Apache uses), and the policy allows httpd_t to read files of type httpd_sys_content_t. That rule is what the next steps build on.
Step 4 - Fixing file contexts for a custom web root
A common case: you serve a site from /srv/www instead of /usr/share/nginx/html. Create the directory and a test page:
sudo mkdir -p /srv/www
echo "Hello from /srv/www" | sudo tee /srv/www/index.html
ls -Zd /srv/www
unconfined_u:object_r:var_t:s0 /srv/www
The files inherited the generic var_t type, which httpd_t is not allowed to read. Point Nginx at the new root:
sudo nano /etc/nginx/conf.d/srv-www.conf
server {
listen 80 default_server;
server_name _;
root /srv/www;
}
Test the configuration. If nginx -t reports a duplicate default server, remove default_server from the listen lines of the server block in /etc/nginx/nginx.conf. Then reload and request the page:
sudo nginx -t
sudo systemctl reload nginx
curl -i http://localhost/
HTTP/1.1 403 Forbidden
The file permissions are fine, but SELinux blocked the read. The denial is in the audit log:
sudo ausearch -m AVC -ts recent
type=AVC msg=audit(1790331600.123:412): avc: denied { read } for pid=3121 comm="nginx" name="index.html" dev="vda1" ino=393221 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:var_t:s0 tclass=file permissive=0
Read it as: the process with type httpd_t (scontext) was denied read on a file with type var_t (tcontext).
The correct fix is to add a permanent file context rule for the path and apply it. semanage fcontext stores the rule in the policy, and restorecon relabels existing files according to it:
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
sudo restorecon -Rv /srv/www
Relabeled /srv/www from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /srv/www/index.html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Test again:
curl http://localhost/
Hello from /srv/www
Because the rule is stored in the policy, new files created under /srv/www get the right type automatically, and a full relabel will not undo it. Avoid chcon for this: it changes the label only until the next relabel.
If the application must write somewhere, for example an upload directory, use the httpd_sys_rw_content_t type for that directory only. List your custom rules with sudo semanage fcontext -l -C.
Step 5 - Changing behaviour with booleans
Booleans are on/off switches built into the policy for optional behaviour. A typical example is using Nginx as a reverse proxy: by default httpd_t may not open outgoing network connections, so proxying to a backend on port 3000 returns 502 Bad Gateway, and /var/log/nginx/error.log shows (13: Permission denied) while connecting to upstream.
List the booleans related to the web server:
getsebool -a | grep httpd_can_network
httpd_can_network_connect --> off
httpd_can_network_connect_db --> off
httpd_can_network_memcache --> off
httpd_can_network_relay --> off
Enable the one you need. The -P flag makes the change persistent across reboots:
sudo setsebool -P httpd_can_network_connect on
getsebool httpd_can_network_connect
httpd_can_network_connect --> on
Enable only the booleans you actually need. httpd_can_network_connect_db, for example, is narrower and only allows connections to database ports. semanage boolean -l | grep httpd shows a one-line description of each.
Step 6 - Allowing a service on a non-standard port
SELinux also labels network ports, and a confined service may bind only to ports of its allowed types. If you configure Nginx to listen on port 8090, it will fail to start. Check which ports the web server may use:
sudo semanage port -l | grep -w http_port_t
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
Add port 8090 to that type:
sudo semanage port -a -t http_port_t -p tcp 8090
If the command reports that the port is already defined, it belongs to another type; modify it instead with sudo semanage port -m -t http_port_t -p tcp 8090. Confirm the change:
sudo semanage port -l | grep -w http_port_t
http_port_t tcp 8090, 80, 81, 443, 488, 8008, 8009, 8443, 9000
Nginx can now bind to port 8090. Remember to open it in firewalld as well (sudo firewall-cmd --permanent --add-port=8090/tcp && sudo firewall-cmd --reload); SELinux and the firewall are independent.
Step 7 - Diagnosing denials and creating a local policy module
When a denial does not match the cases above, let sealert analyse the audit log. It explains each denial and usually suggests the right semanage or setsebool command:
sudo sealert -a /var/log/audit/audit.log
SELinux is preventing /usr/sbin/nginx from read access on the file index.html.
***** Plugin catchall_labels (83.8 confidence) suggests *******************
If you want to allow nginx to have read access on the index.html file
Then you need to change the label on index.html
...
Always prefer the suggestion that fixes labels or booleans. Only when no label or boolean covers a legitimate need should you generate a small local policy module from the denials with audit2allow:
sudo ausearch -m AVC -c nginx --raw | audit2allow -M my-nginx
This writes my-nginx.te (human-readable rules) and my-nginx.pp (compiled module) in the current directory. Read the .te file before loading it: audit2allow allows whatever was denied, including things that should stay blocked.
cat my-nginx.te
sudo semodule -i my-nginx.pp
List and remove custom modules with sudo semodule -l | grep my- and sudo semodule -r my-nginx.
For a single troublesome service you can also make only its domain permissive while the rest of the system stays enforcing, which is far safer than setenforce 0:
sudo semanage permissive -a httpd_t
Denials for httpd_t are then logged but not enforced. Remove it with sudo semanage permissive -d httpd_t once you have the correct fix in place.
Troubleshooting
No AVC messages, but the service still fails. Some rules are marked dontaudit and are not logged. Temporarily enable their logging with sudo semodule -DB, reproduce the problem, check ausearch -m AVC -ts recent, and restore normal logging with sudo semodule -B.
Files moved with mv keep the wrong label. mv preserves the original context, while cp creates files with the context of the destination. After moving files into a web root, run sudo restorecon -Rv on the directory.
Service fails after re-enabling SELinux. The filesystem was not relabelled. Run sudo touch /.autorelabel and reboot; on large disks the relabel can take several minutes.
Conclusion
You now know how to check and change SELinux modes on Rocky Linux 9 and how to fix denials the right way: persistent file context rules with semanage fcontext and restorecon, booleans with setsebool -P, port labels with semanage port, and local modules only as a last resort. As next steps, review getsebool -a for the services you run, check ausearch -m AVC after each deployment, and read the *_selinux manual pages (install selinux-policy-doc, for example man httpd_selinux) for the labels and booleans of each service.
