nginx 502: connect() failed (13: Permission denied) while connecting to upstream
SELinux is blocking nginx from opening a network connection to your backend, so the proxy_pass fails with permission denied and nginx returns 502.
What you see
2026/06/15 14:22:01 [crit] 1182#1182: *5 connect() failed (13: Permission denied) while connecting to upstream, client: 10.0.0.4, upstream: "http://127.0.0.1:3000/", host: "app.example.com"
What’s actually happening
nginx serves a 502 Bad Gateway and the error log shows [crit] connect() failed (13: Permission denied) while connecting to upstream. The backend (Node, gunicorn, puma, a second app) is up and you can curl it directly from the same box, which rules out the app. The giveaway is errno 13 / EACCES specifically — a connection refused would be errno 111. It shows up right after standing up a fresh proxy on a RHEL, CentOS, Rocky, AlmaLinux, or Fedora host.
Common causes
- SELinux is enforcing and the httpd_can_network_connect boolean is off, so the httpd_t domain (which nginx runs under) is denied outbound TCP — by far the most common cause on RHEL-family systems.
- nginx is proxying to a Unix socket whose file or parent directory has an SELinux context nginx can't access.
- The backend listens on a non-standard port that isn't in SELinux's http_port_t label set, so even with general network access nginx can't reach it.
- A restrictive file ACL or ownership on a Unix socket (plain POSIX permissions, not SELinux) blocks the nginx worker user.
- Rarely, a custom/strict AppArmor profile on Debian/Ubuntu doing the equivalent confinement.
How to fix it
- Confirm it's SELinux with the audit logRun `getenforce` (expect Enforcing) and check the denial: `ausearch -m avc -ts recent` or `grep nginx /var/log/audit/audit.log`. You'll see an AVC denial like `denied { name_connect }` for `comm="nginx"`. That confirms the policy is the blocker, not nginx config — so don't touch nginx.conf.
- Flip the httpd_can_network_connect boolean`sudo setsebool -P httpd_can_network_connect 1`. The `-P` makes it persist across reboots; it takes effect immediately with no nginx reload. Re-test the request — this resolves the standard 127.0.0.1:port proxy_pass case outright. Verify with `getsebool httpd_can_network_connect`.
- Fix a non-standard backend portIf the backend is on an odd port and you'd rather scope it tightly than open all outbound, label that port instead: `sudo semanage port -a -t http_port_t -p tcp 3000`. Now nginx (httpd_t) is allowed to connect to 3000 specifically. Use `semanage port -l | grep http_port_t` to see what's already allowed.
- Repair Unix-socket context (socket upstreams)For `proxy_pass http://unix:/run/app/app.sock`, set the SELinux context on the socket and its directory: `sudo semanage fcontext -a -t httpd_var_run_t "/run/app(/.*)?"` then `sudo restorecon -Rv /run/app`. Also confirm POSIX perms let nginx's worker user read/write the socket.
- Use audit2allow only if booleans don't cover itIf a boolean doesn't exist for your exact case, generate a targeted policy module from the denial: `ausearch -m avc -ts recent | audit2allow -M nginx_upstream` then `semodule -i nginx_upstream.pp`. Review the generated .te first — don't blindly install rules wider than you need. Disabling SELinux entirely is a last resort, not a fix.
Stop it recurring
On RHEL-family hosts, set httpd_can_network_connect (or label the backend port) as part of provisioning any reverse-proxy box, so the proxy works the first time instead of 502-ing.