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 sudo privileges.
  • 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:

ModeBehaviour
enforcingThe policy is applied: disallowed actions are blocked and logged.
permissiveNothing is blocked, but actions that would be denied are logged. Useful for diagnosing.
disabledSELinux 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.

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.