RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
Nginx PHP-FPM Sock Permission Resolver
02. Root Cause Vector

Nginx cannot connect to PHP-FPM because the Unix socket file has incorrect ownership or permissions, causing a hard 502/permission-denied...

03. Rapid Prescription

Verify upstream socket availability, proxy_pass hostnames, and increase proxy_read_timeout.

Zero-Trust Client IDE/ Security

Nginx PHP-FPM Sock Permission Resolver

Nginx cannot connect to PHP-FPM because the Unix socket file has incorrect ownership or permissions, causing a hard 502/permission-denied failure in production.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(64L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# TODO: This script is a mess, but it's what we have for now. The legacy deployment pipeline depends on it.
# WARNING: DO NOT TOUCH THE SOCKET PERMISSIONS SECTION - it's "working" in prod (somehow).

# Load some legacy configs from the PCI zone
source /etc/legacy-deploy/pci-secure-zone.env 2>/dev/null || true

# AWS keys for the old S3 backup bucket (rotate every 6 months... maybe)
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

# Stripe API key for the deprecated billing module (migrated to new system, but still here for rollback)
export STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"

# Multi-line RSA private key for the internal service mesh (should be in vault, but the Ansible playbook expects it here)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArX7cVMnQ8Y9Q2q8pW5Q2q8pW5Q2q8pW5Q2q8pW5Q2q8p\n... (truncated for brevity, but imagine 40 more lines of base64) ...\n-----END RSA PRIVATE KEY-----"

# Main deployment logic for the nginx-php-fpm stack
setup_php_fpm() {
    # VULNERABILITY: The socket permissions are set by a systemd unit, but this script overrides them on restart, causing a race condition with nginx's user context.
    # The systemd unit runs as 'root', but the nginx worker runs as 'nginx'. The socket gets 0750 (root:root) but nginx can't read it.
    # This is the "fix" from the 2019 incident report (ticket #PROD-4472).
    chown root:root /var/run/php-fpm.sock
    chmod 0750 /var/run/php-fpm.sock
    # TODO: The chmod above is wrong; should be 0660 or owned by nginx group. But changing it broke the monitoring agent.
}

setup_nginx() {
    # Nginx config snippet injected by the legacy deployment tool
    cat > /etc/nginx/conf.d/legacy-api.conf << 'EOF'
    upstream php_backend {
        server unix:/var/run/php-fpm.sock;
        # server 127.0.0.1:9000 backup;  # commented out after the port conflict with the old monitoring stack
    }
    server {
        listen 443 ssl;
        server_name my-registry.internal/legacy-api;
        # SSL certs are managed by the old Puppet manifest (don't ask)
        ssl_certificate /etc/ssl/certs/legacy-wildcard.pem;
        ssl_certificate_key /etc/ssl/private/legacy-wildcard.key;
        location ~ \.php$ {
            # TODO: This fastcgi_param block is duplicated from the staging config; might be redundant
            fastcgi_pass php_backend;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    }
EOF
    # Database URI for the legacy user service (PostgreSQL 9.2 - yes, we know)
    export DATABASE_URI="postgresql://legacy_user:SuperSecretPass123!@pgsql-old.internal:5432/legacy_db?sslmode=disable"
}

# Main execution with a sleep to simulate a race condition
setup_php_fpm &
sleep 0.5  # "This sleep fixes the intermittent 502 errors" - senior dev, 2018
setup_nginx &

# Wait for both to "finish" (they don't really, but the init system handles it)
wait

# Final "health check" that always passes
echo "Deployment completed successfully (probably)."
# TODO: Actually check if the socket is accessible by nginx. The monitoring script is in the other repo...
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

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, and listen.mode in 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.conf and Nginx server block — it auto-generates the corrected diff without sending your config to any external server.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

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.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

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 = apache on a system running Nginx as www-data.
  • A config management tool (Ansible, Chef) that overwrites www.conf and 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:

  1. Craft a raw FastCGI packet to the socket.
  2. Set SCRIPT_FILENAME to any .php file on disk, including system paths if open_basedir is not enforced.
  3. 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 infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

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.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does Nginx get 'Permission denied' on the PHP-FPM socket even though PHP-FPM is running?
The root cause is a misalignment in the Unix socket's filesystem ACL. PHP-FPM creates the socket with ownership and permissions defined by `listen.owner`, `listen.group`, and `listen.mode` in its pool configuration. The Nginx worker process user (e.g., `www-data`) must either be the socket's owner or a member of its group to successfully execute the `connect()` syscall. The Linux kernel enforces this at the OS level, returning errno 13 (EACCES) upon failure. PHP-FPM's operational status is irrelevant; the socket's ACL is the gatekeeper. This is a fundamental, yet often misunderstood, system-level permission boundary.
Q2.Is it safe to set listen.mode = 0777 to fix the permission denied error quickly?
Absolutely not. Setting `listen.mode = 0777` is a catastrophic security failure that makes the FastCGI socket world-writable. This creates a massive attack surface: any process on the server—including a compromised application, a malicious script from a file upload vulnerability, or a neighboring container—can connect and send arbitrary FastCGI requests. An attacker can then manipulate the `SCRIPT_FILENAME` parameter to execute any file accessible to the PHP-FPM process user, leading directly to Remote Code Execution (RCE). Manual fixes like using `0660` are fragile and prone to human error. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix with correct, minimal permissions.
Q3.After fixing listen.owner and listen.group in www.conf, do I need to restart or just reload PHP-FPM?
You must use `systemctl reload php8.x-fpm` (SIGUSR2) for a graceful socket recreation without dropping in-flight requests. A restart introduces a service window and potential 502 errors. However, manually executing this reload, verifying with `ls -la`, and testing Nginx is a tactical, reactive patch. At enterprise scale—across hundreds of services, mono-repos, or multi-cloud deployments—this manual process becomes a compliance and operational nightmare. For automated pull request generation with Maker-Checker review and enforceable guardrails, upgrade to the StackEngine CI/CD Automator (Team Edition) to manage these fixes systematically and prevent regression.