The Error
Your Nginx error log shows something like this:
2024/01/15 10:23:41 [crit] 12345#0: *1 connect() to 127.0.0.1:8080 failed (13: Permission denied) while connecting to upstream, client: 203.0.113.1, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "example.com"
Nginx returns a 502 Bad Gateway to the browser. Restarting Nginx or the backend service does nothing. The backend is actually running โ curl localhost:8080 returns a response just fine. This is a classic SELinux block.
Root Cause
SELinux enforces mandatory access control on top of standard Linux permissions. On RHEL-based systems, it ships in Enforcing mode by default โ and that mode has opinions about what Nginx can do.
When Nginx acts as a reverse proxy, it needs explicit SELinux permission to make outbound network connections. Without that permission, the kernel returns EACCES (13: Permission denied), even if you're running as root and the file permissions look perfectly normal.
This typically bites you when you:
- Proxy to a Node.js, Python, Ruby, or Java app on a non-standard port
- Proxy to PHP-FPM via a Unix socket (
/run/php-fpm/www.sock) - Move an existing setup to a fresh RHEL/CentOS/Rocky server
- Switch a backend from port 80 or 443 (allowed by default) to something like 8080, 3000, or 5000
Confirm It's SELinux
Don't guess. Run these first:
# Check SELinux status
getenforce
# Should output: Enforcing
# Check the audit log for denials
sudo ausearch -m avc -ts recent | grep nginx
# Or scan the audit log directly
sudo grep 'nginx' /var/log/audit/audit.log | grep denied
A denial looks like this:
type=AVC msg=audit(...): avc: denied { name_connect } for pid=12345 comm="nginx" dest=8080 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:http_cache_port_t:s0 tclass=tcp_socket permissive=0
That denied { name_connect } is your smoking gun. SELinux is blocking Nginx from reaching the upstream port.
Fix 1: Enable httpd_can_network_connect (Recommended)
For most setups, this one boolean fixes everything. It lets the httpd_t domain โ which is what Nginx runs under โ make outbound TCP connections to any port:
# Enable permanently (survives reboots)
sudo setsebool -P httpd_can_network_connect 1
# Verify
getsebool httpd_can_network_connect
The -P flag is critical. Skip it and the setting reverts on the next reboot.
Then reload Nginx and test:
sudo nginx -t && sudo systemctl reload nginx
Fix 2: Allow a Specific Port via semanage
Rather than opening all outbound connections, you can label a single port so Nginx can reach it. This is tighter from a security standpoint.
# Install semanage if it's not available
sudo dnf install -y policycoreutils-python-utils
# Label your backend port (e.g., 3000) as http_port_t
sudo semanage port -a -t http_port_t -p tcp 3000
# Already labeled differently? Use -m to modify instead:
sudo semanage port -m -t http_port_t -p tcp 3000
# Confirm
sudo semanage port -l | grep http_port_t
Ports already labeled http_port_t out of the box: 80, 443, 8008, 8009, 8080, 8443. If your backend is on one of those and you're still hitting the error, skip to Fix 1 โ the issue is elsewhere.
Fix 3: Unix Sockets (PHP-FPM, Gunicorn, etc.)
Proxying to a Unix socket instead of a TCP port?
upstream backend {
server unix:/run/myapp/app.sock;
}
The socket file needs the right SELinux context. Check what it currently has:
ls -Z /run/myapp/app.sock
You want to see httpd_var_run_t or httpd_sock_t. If it shows var_run_t or something else, relabel it:
# One-time relabel
sudo chcon -t httpd_sock_t /run/myapp/app.sock
# Persistent โ set the context on the directory so it survives restarts
sudo semanage fcontext -a -t httpd_sock_t "/run/myapp(/.*)?"
sudo restorecon -Rv /run/myapp/
For PHP-FPM specifically, also set this boolean:
sudo setsebool -P httpd_can_network_connect 1
Verify the Fix
# Reload Nginx
sudo systemctl reload nginx
# Check for new AVC denials (should be empty)
sudo ausearch -m avc -ts recent | grep nginx
# Hit the endpoint
curl -I http://example.com/
# Watch the error log live
sudo tail -f /var/log/nginx/error.log
No 502s, no AVC denials in the audit log โ you're done.
What Not to Do
The most popular "fix" on Stack Overflow is this:
# DON'T do this in production
sudo setenforce 0
Yes, it works. That's exactly why people do it. But it disables SELinux enforcement across the entire system โ not just for Nginx. Every other service loses that protection too. Use the targeted fixes above instead.
Permissive mode is fine for confirming the diagnosis. Just re-enable enforcement afterward:
# Temporarily disable to confirm it's SELinux
sudo setenforce 0
curl -I http://example.com/ # Works now? It's definitely SELinux.
# Re-enable and apply the real fix
sudo setenforce 1
sudo setsebool -P httpd_can_network_connect 1
Tips
If you're wrestling with Unix socket directory permissions โ specifically what mode lets both Nginx and your app access the socket without opening it too wide โ the Unix Permissions Calculator on ToolCraft lets you work out the right chmod values visually. No octal math required.
For deeper SELinux debugging, audit2why and audit2allow (both in the policycoreutils-python-utils package) translate raw AVC denials into plain English. audit2allow can even generate a custom policy module for edge cases the standard booleans don't cover:
# Plain-English explanation of what was denied and why
sudo ausearch -m avc -ts recent | audit2why
# Build a custom policy module for unusual cases
sudo ausearch -m avc -ts recent | audit2allow -M mynginx
sudo semodule -i mynginx.pp

