Fix Nginx 'connect() failed (13: Permission denied)' While Connecting to Upstream (SELinux)

intermediateโšก Nginx2026-04-08| RHEL / CentOS / Rocky Linux / AlmaLinux with SELinux enforcing mode, Nginx as reverse proxy

Error Message

connect() to 127.0.0.1:8080 failed (13: Permission denied) while connecting to upstream
#nginx#selinux#permission-denied#upstream

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

Related Error Notes