How to Fix Nginx 'upstream failed (13: Permission denied)' for PHP-FPM Unix Socket
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: Nginx's worker process user (
www-data,nginx, etc.) does not have read/write permission on/var/run/php-fpm.sock, so every PHP request returns a hard 502 Bad Gateway. - How to fix it: Align
listen.owner,listen.group, andlisten.modein your PHP-FPM pool config (www.conf) with the user Nginx runs as, then reload both services. - Shortcut: Use our Client-Side Sandbox above to paste your
www.confand Nginx server block — it auto-generates the corrected diff without sending your config to any external server.
The Incident (What Does the Error Mean?)
Your Nginx error log is printing:
2024/01/15 03:42:17 [crit] 18345#18345: *1 connect() to unix:/var/run/php-fpm.sock failed (13: Permission denied)
while connecting to upstream, client: 203.0.113.45, server: example.com,
request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php-fpm.sock:",
host: "example.com"
Errno 13 is EACCES — the kernel-level access denied error. This is not a misconfigured fastcgi_pass path. The socket file exists and PHP-FPM is running. The Nginx worker process simply does not have filesystem permission to open the socket descriptor. Every single PHP request on this host is returning 502 Bad Gateway until this is resolved. There is no graceful degradation — the site is fully down for dynamic content.
The Attack Vector / Blast Radius
This is a security misconfiguration with a dual blast radius: operational outage and a latent privilege escalation vector if fixed incorrectly.
The outage vector: PHP-FPM creates the socket file with ownership and mode dictated by listen.owner, listen.group, and listen.mode in the pool config. If those values don't match the Nginx worker user, the kernel rejects the connect() syscall. Common triggers:
- Installing PHP-FPM from a distro package that defaults
listen.owner = apacheon a system running Nginx aswww-data. - A config management tool (Ansible, Chef) that overwrites
www.confand resets pool ownership to defaults. - A Docker base image where the PHP-FPM and Nginx containers share a socket via a volume but have different UID mappings.
The dangerous wrong fix — world-readable sockets (chmod 777): The instinctive "just make it work" fix is setting listen.mode = 0777. This means any process on the system — including a compromised web application, a rogue cron job, or a container escape — can connect to the PHP-FPM socket and send arbitrary FastCGI requests. An attacker with local code execution can:
- Craft a raw FastCGI packet to the socket.
- Set
SCRIPT_FILENAMEto any.phpfile on disk, including system paths ifopen_basediris not enforced. - Execute arbitrary PHP, effectively achieving Remote Code Execution with the PHP-FPM process user's privileges.
Never use listen.mode = 0777 or 0666 in production. The correct fix uses group-based access control with mode 0660.
How to Fix It (The Solution)
Step 1: Diagnose the current state
# Find what user Nginx workers run as
ps aux | grep nginx | grep worker
# Find what user PHP-FPM pool workers run as
ps aux | grep php-fpm | grep pool
# Inspect the socket file permissions
ls -la /var/run/php-fpm.sock
# Example bad output:
# srw-rw---- 1 php-fpm php-fpm 0 Jan 15 03:40 /var/run/php-fpm.sock
# Nginx worker is 'www-data' — it is NOT in the 'php-fpm' group. Denied.
Basic Fix — Align Socket Ownership in PHP-FPM Pool Config
Edit /etc/php/8.x/fpm/pool.d/www.conf (path varies by distro/version):
; PHP-FPM Pool: www.conf
[www]
user = php-fpm
group = php-fpm
- listen = /var/run/php-fpm.sock
- listen.owner = php-fpm
- listen.group = php-fpm
- listen.mode = 0660
+ listen = /var/run/php-fpm.sock
+ listen.owner = www-data
+ listen.group = www-data
+ listen.mode = 0660
Then reload PHP-FPM (do NOT restart — reload is zero-downtime for existing workers):
systemctl reload php8.x-fpm
# Verify the socket now has correct ownership
ls -la /var/run/php-fpm.sock
# Expected: srw-rw---- 1 www-data www-data 0 Jan 15 03:55 /var/run/php-fpm.sock
Enterprise Best Practice — Group-Based ACL (Least Privilege)
Instead of changing socket ownership to the Nginx user directly, use a shared group. This is the correct pattern for multi-tenant servers or when multiple services need FPM access.
; /etc/php/8.x/fpm/pool.d/www.conf
[www]
- listen.owner = php-fpm
- listen.group = php-fpm
- listen.mode = 0660
+ listen.owner = php-fpm
+ listen.group = nginx-php
+ listen.mode = 0660
# Create the shared group
groupadd nginx-php
# Add both the PHP-FPM process user and the Nginx worker user to it
usermod -aG nginx-php www-data # Nginx worker user
usermod -aG nginx-php php-fpm # PHP-FPM process user (so it can own the socket)
# Reload PHP-FPM to recreate socket with new group
systemctl reload php8.x-fpm
# Verify
ls -la /var/run/php-fpm.sock
# srw-rw---- 1 php-fpm nginx-php 0 Jan 15 04:01 /var/run/php-fpm.sock
For Docker/container environments, the socket is shared via a named volume. The critical fix is ensuring UID/GID parity between containers:
# docker-compose.yml
services:
php-fpm:
image: php:8.2-fpm
- # No user mapping — defaults to root-owned socket
+ user: "1000:1001" # Match nginx container's GID
volumes:
- php_socket:/var/run/
nginx:
image: nginx:stable-alpine
+ user: "nginx"
volumes:
- php_socket:/var/run/
+ # Ensure nginx user (UID 101) is in GID 1001 in the image or via entrypoint
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of misconfiguration is 100% preventable at the pipeline level.
1. Ansible — Assert socket permissions in your role
# roles/php-fpm/tasks/assert_socket.yml
- name: Assert PHP-FPM socket has correct group ownership
ansible.builtin.stat:
path: /var/run/php-fpm.sock
register: fpm_sock
- name: Fail if socket group is not nginx-php
ansible.builtin.fail:
msg: "PHP-FPM socket group is {{ fpm_sock.stat.gr_name }}, expected nginx-php"
when: fpm_sock.stat.gr_name != 'nginx-php'
- name: Fail if socket mode allows world access
ansible.builtin.fail:
msg: "PHP-FPM socket mode {{ fpm_sock.stat.mode }} is too permissive"
when: (fpm_sock.stat.mode | int) > 33200 # Octal 0660 = decimal 432
2. Checkov / Dockerfile Linting
Add a custom Checkov check or use hadolint to flag chmod 777 on socket paths:
# .hadolint.yaml
ignore:
- DL3008
failure-threshold: warning
# Custom rule: flag any RUN chmod 7xx on /var/run
3. Serverspec / InSpec Integration Test
Run this as a post-deploy smoke test in your pipeline:
# spec/php_fpm_socket_spec.rb
describe file('/var/run/php-fpm.sock') do
it { should exist }
it { should be_socket }
it { should be_grouped_into 'nginx-php' }
it { should_not be_readable.by('others') }
it { should_not be_writable.by('others') }
end
4. OPA/Conftest for PHP-FPM Config
# policy/php_fpm.rego
package phpfpm
deny[msg] {
input.listen_mode == "0777"
msg := "CRITICAL: PHP-FPM listen.mode=0777 exposes FastCGI socket to all local processes"
}
deny[msg] {
input.listen_mode == "0666"
msg := "HIGH: PHP-FPM listen.mode=0666 is world-readable — use 0660 with group ACL"
}
Gate your www.conf template rendering in CI with conftest test --policy policy/ parsed_www_conf.json before any deploy.