sfw/fix
(13: Permission denied) high

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

  1. 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.
  2. 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`.
  3. 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.
  4. 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.
  5. 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.

Related errors