If you've moved from cPanel shared hosting to a bare AlmaLinux or Rocky Linux VPS and your website suddenly throws 403 Forbidden or 502 Bad Gateway errors — even though the file permissions look perfectly fine, ownership is correct, and Nginx or Apache starts without complaint — there's a good chance the culprit isn't your web server config at all. It's SELinux quietly blocking the request before it ever reaches your app.

This one trips up a lot of people moving off shared hosting for the first time, because cPanel servers usually have SELinux tuned or disabled by the hosting provider. A fresh AlmaLinux/Rocky VPS ships with SELinux in Enforcing mode by default, and it enforces rules most tutorials never mention.

Symptom

Common signs you're dealing with SELinux, not a real permissions or config bug:

  • Nginx returns 403 Forbidden on static files even though chmod 644 and correct ownership are set.
  • PHP-FPM or an upstream app works fine when you curl it directly on the VPS, but Nginx returns 502 Bad Gateway when proxying to it.
  • A reverse proxy to a custom port (e.g. Node.js on 3000, Gunicorn on 8000) fails to connect, but curl localhost:3000 from the shell works.
  • Apache can't write to a directory you just chown'd to apache:apache.
  • Everything works right after you (or a previous admin) ran setenforce 0, and breaks again after a reboot re-applies Enforcing mode.

Cause

SELinux (Security-Enhanced Linux) enforces a second, independent permission model on top of standard Unix file permissions. Even if a file is world-readable and owned by the right user, SELinux checks whether the process's security context is allowed to touch a resource with that context type. Two contexts matter most for hosting:

  • File contexts — every file/directory has a label like httpd_sys_content_t. Web servers can only read files labeled for them.
  • Booleans — on/off switches like httpd_can_network_connect, which controls whether Nginx/Apache/PHP-FPM are allowed to open outbound network connections at all (needed for reverse proxying, remote MySQL, SMTP from PHP, etc).

The classic trap: you upload files somewhere non-standard (say, /data/websites/app instead of /var/www), or move a proxy target to a non-default port, and the underlying files/sockets simply don't carry the label SELinux expects — regardless of Unix permissions.

Fix

1. Confirm SELinux is actually the problem

Check the current mode:

getenforce

If it returns Enforcing, check the audit log for denials matching your timestamp:

sudo grep "denied" /var/log/audit/audit.log | tail -30

For a friendlier, plain-English explanation of a specific denial, install and run setroubleshoot:

sudo dnf install setroubleshoot-server -y
sudo sealert -a /var/log/audit/audit.log

It will print not just what was blocked, but the exact command to fix it — which is the fastest way to confirm SELinux is really the cause before you change anything.

2. Fix file context issues with restorecon / semanage

If your site lives in the default /var/www/html, restoring the default context usually fixes it instantly:

sudo restorecon -Rv /var/www/html

If your content lives in a non-standard path (very common on VPS setups), tell SELinux to treat that path like standard web content permanently:

sudo semanage fcontext -a -t httpd_sys_content_t "/data/websites/app(/.*)?"
sudo restorecon -Rv /data/websites/app

For directories the app needs to write to (uploads, cache, logs), use the writable content type instead:

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/data/websites/app/uploads(/.*)?"
sudo restorecon -Rv /data/websites/app/uploads

Note: semanage isn't installed by default on a minimal AlmaLinux/Rocky image. If the command isn't found:

sudo dnf install policycoreutils-python-utils -y

3. Fix network/proxy denials with booleans

If Nginx or Apache is proxying to PHP-FPM over TCP, a remote database, an SMTP relay, or any backend app on a custom port, enable the relevant boolean:

# Allow the web server to make outbound network connections (reverse proxy, remote DB, etc.)
sudo setsebool -P httpd_can_network_connect 1

# Allow connecting specifically to remote MySQL/MariaDB
sudo setsebool -P httpd_can_network_connect_db 1

# Allow sendmail/PHP mail() to relay through an external SMTP server
sudo setsebool -P httpd_can_sendmail 1

Always use -P — it makes the change persistent across reboots. Without it, the boolean resets the next time the server restarts, and you'll be back here in a month wondering why it broke again.

4. Proxying to a non-standard port

SELinux also restricts which ports a proxied service is allowed to bind to under the http_port_t type. If your app runs on, say, port 8000 and Nginx can't reach it as a reverse proxy target:

sudo semanage port -a -t http_port_t -p tcp 8000

If the port's already assigned to a different SELinux port type, use -m (modify) instead of -a (add).

Quick reference table

SymptomLikely SELinux fix
403 on static files despite correct chmodrestorecon -Rv or semanage fcontext on the content directory
502 proxying to PHP-FPM/app over TCPsetsebool -P httpd_can_network_connect 1
Can't connect to remote MySQL/MariaDBsetsebool -P httpd_can_network_connect_db 1
PHP mail() / SMTP relay fails silentlysetsebool -P httpd_can_sendmail 1
Proxy target on a custom port unreachablesemanage port -a -t http_port_t -p tcp <port>
App can't write to uploads/cache/logs dirLabel the directory httpd_sys_rw_content_t

Prevention

A few habits will save you from repeating this every time you deploy something new:

  • Never leave SELinux permanently disabled. setenforce 0 is fine for a 30-second test to confirm SELinux is the cause, but running Permissive or Disabled long-term on a public-facing VPS removes a real security layer — especially relevant if the box handles customer data or payment pages.
  • Set contexts as part of your deploy process, not as an afterthought. If you deploy with a script or CI/CD pipeline, add the restorecon/semanage fcontext calls right after files are copied into place.
  • Check sealert before Googling the error message. It usually names the exact boolean or context you need, which is faster than guessing from a generic 403/502 error.
  • Document any custom fcontext/port rules in your server's runbook. They don't show up in a normal diff of your Nginx/Apache config, so the next person (including future you) has no way to know they exist.

If you'd rather skip this layer entirely, SkyServer's managed cPanel VPS plans come with SELinux policies already tuned for standard hosting paths — but if you're running an unmanaged AlmaLinux/Rocky VPS and deploying outside the usual /var/www layout, this is a permission model you'll want to understand rather than switch off.

Frequently Asked Questions

Is it safe to just disable SELinux instead of fixing contexts?

You can, but it's not recommended on anything internet-facing. SELinux is a second line of defense — if an attacker does get code execution through a vulnerable plugin or script, SELinux can be the thing that stops them from reading sensitive system password files or pivoting to other sites on the same VPS. Fixing the context takes a few minutes and keeps that protection intact.

Why did this work fine yesterday and break today with no config changes?

The most common cause is a reboot or SELinux policy update re-applying Enforcing mode after someone ran setenforce 0 temporarily and forgot it doesn't persist. Check getenforce right after any server restart if a "no changes but it broke" issue shows up.

Does cPanel handle SELinux automatically?

On a stock cPanel/WHM install, SELinux is typically set to Permissive or Disabled because cPanel's own file structure and CloudLinux's LVE isolation provide overlapping protection, and cPanel doesn't ship pre-built policies for every possible layout. On a plain AlmaLinux/Rocky VPS without cPanel, SELinux defaults to full Enforcing mode, which is why this issue shows up mostly on unmanaged VPS setups.

How do I find which boolean controls a specific denial?

Run sealert -a /var/log/audit/audit.log after reproducing the error — it prints the exact setsebool or semanage command needed. You can also search booleans by keyword with getsebool -a | grep httpd to see every httpd-related boolean and its current state.

Will these changes survive a server reboot?

Only if you use the flags shown above. setsebool without -P and manual chcon changes are temporary and reset on reboot or the next restorecon. Always use setsebool -P for booleans and semanage fcontext (followed by restorecon) for file labels — both are persistent.