--- title: "How to Configure SELinux Modes and Contexts on Rocky Linux 9" description: "Learn SELinux modes on Rocky Linux 9, then fix real Nginx denials with file contexts, booleans and port labels instead of disabling SELinux on your server." category: "Server Security" language: en source: https://cubepath.com/docs/server-security/selinux-basic-configuration-and-modes updated: 2026-09-25 --- # How to Configure SELinux Modes and Contexts on Rocky Linux 9 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: ```bash sestatus ``` ```output 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: ```bash sudo setenforce 0 getenforce ``` ```output Permissive ``` Go back to enforcing: ```bash sudo setenforce 1 getenforce ``` ```output Enforcing ``` `setenforce` does not survive a reboot. The persistent mode is set in `/etc/selinux/config`: ```bash sudo nano /etc/selinux/config ``` ```ini 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. > Note: On Rocky Linux 9, `SELINUX=disabled` in 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 argument `sudo grubby --update-kernel ALL --args selinux=0`. When you later re-enable SELinux, relabel the whole filesystem with `sudo touch /.autorelabel` and 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: ```bash sudo dnf install policycoreutils-python-utils setroubleshoot-server nginx sudo systemctl enable --now nginx ``` Open HTTP in the firewall: ```bash 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`: ```bash ls -Z /usr/share/nginx/html/index.html ``` ```output system_u:object_r:httpd_sys_content_t:s0 /usr/share/nginx/html/index.html ``` And process contexts with `ps -Z`: ```bash ps -eZ | grep nginx ``` ```output 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: ```bash sudo mkdir -p /srv/www echo "Hello from /srv/www" | sudo tee /srv/www/index.html ls -Zd /srv/www ``` ```output 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: ```bash sudo nano /etc/nginx/conf.d/srv-www.conf ``` ```nginx 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: ```bash sudo nginx -t sudo systemctl reload nginx curl -i http://localhost/ ``` ```output HTTP/1.1 403 Forbidden ``` The file permissions are fine, but SELinux blocked the read. The denial is in the audit log: ```bash sudo ausearch -m AVC -ts recent ``` ```output 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: ```bash sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?" sudo restorecon -Rv /srv/www ``` ```output 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: ```bash curl http://localhost/ ``` ```output 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: ```bash getsebool -a | grep httpd_can_network ``` ```output 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: ```bash sudo setsebool -P httpd_can_network_connect on getsebool httpd_can_network_connect ``` ```output 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: ```bash sudo semanage port -l | grep -w http_port_t ``` ```output http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000 ``` Add port 8090 to that type: ```bash 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: ```bash sudo semanage port -l | grep -w http_port_t ``` ```output 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: ```bash sudo sealert -a /var/log/audit/audit.log ``` ```output 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`: ```bash 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. ```bash 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`: ```bash 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.