When wiring a PHP-FPM pool to a web server, when should it listen on a Unix socket versus TCP, and how is each secured?
answer
- same host vs across hosts or containers
- listen.owner, listen.group, listen.mode 0660
- permission denied becomes 502
- listen.allowed_clients only for TCP
- PHP_VALUE lets a client rewrite ini
basics
~20 sUse a Unix socket when the web server and PHP-FPM share a host, secured by listen.owner, listen.group and listen.mode; use TCP across hosts or containers, restricted with listen.allowed_clients and a firewall, because an exposed FastCGI port lets anyone run PHP.
solid answer
~50 sThe pool's `listen` directive takes either a path, giving a Unix domain socket, or an address and port for TCP. A **Unix socket** works only on the same host, skips the TCP stack, and is protected by file permissions: `listen.owner`, `listen.group` and `listen.mode` (default `0660`, owner defaulting to the user running the FPM master). If the web server's user cannot read and write it, every PHP request fails with a permission error and a 502. **TCP** is needed when the web server runs on another host or container; restrict it with `listen.allowed_clients` (TCP only) and bind to a private address, never a public one. An exposed FastCGI port is remote code execution: a client chooses `SCRIPT_FILENAME` and can pass `PHP_VALUE`/`PHP_ADMIN_VALUE` parameters that rewrite ini settings. Performance differences are small; the choice is about topology and security.
code
ini · 11 lines; same host as nginx: Unix socket the web server can open
[app]
listen = /run/php/$pool.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; separate web tier: private TCP address, allow-listed client
[api]
listen = 10.0.1.5:9000
listen.allowed_clients = 10.0.1.10go deeper
Recall that listen takes a socket path or an address and port, and that the web server must be able to connect to whichever you choose.
Explain listen.owner, listen.group and listen.mode for sockets, listen.allowed_clients for TCP, and why a wrong socket owner shows up as a 502.
Treat an exposed FastCGI port as remote code execution: explain SCRIPT_FILENAME and PHP_VALUE, bind privately, allow-list clients and firewall the port.
Choose the topology — co-located sockets, a shared-volume sidecar or a private network — from how the web and PHP tiers are deployed and scaled.
## What listen accepts Each PHP-FPM **pool** accepts FastCGI requests on the address given by its mandatory `listen` directive. The sample `www.conf` accepts four forms: | Form | Example | Meaning | |---|---|---| | `ip:port` | `listen = 127.0.0.1:9000` | TCP on one IPv4 address | | `[ipv6]:port` | `listen = [::1]:9000` | TCP on one IPv6 address | | `port` | `listen = 9000` | TCP on **all** addresses | | path | `listen = /run/php/app.sock` | Unix domain socket | The variable `$pool` expands to the pool name, so `listen = /run/php/$pool.sock` gives each pool its own socket. `listen.backlog` sets the queue of connections waiting for a free worker for either kind. ## Unix domain sockets A Unix socket is a file-system object. It only works between processes on the **same host** (or containers sharing a mounted directory), and it avoids the TCP/IP stack, ports and connection-state tables. Access is controlled by the socket file's ownership and mode: - `listen.owner` and `listen.group` — default to the user running the FPM master (usually root) and that user's group. - `listen.mode` — default `0660`: read and write for owner and group only. - `listen.acl_users` / `listen.acl_groups` — POSIX ACLs; when set, `listen.owner` and `listen.group` are ignored. On Linux the connecting process needs read and write permission, so the web server's user (for example `www-data` or `nginx`) must be the owner, in the group, or listed in the ACL. The classic failure: a hand-written pool with no `listen.owner`, so the socket is `root:root 0660`, the web server gets *permission denied* on connect, and every PHP page returns **502 Bad Gateway**. Set `listen.owner` and `listen.group` explicitly in every pool file you write. ## TCP sockets TCP is required when the web server and FPM are on **different hosts** or in separate containers without a shared volume. It costs a little more per request and, under very high rates, can run into ephemeral-port and TIME_WAIT limits on the web-server side, but for most sites the difference is not measurable. What matters is exposure: 1. **Bind narrowly.** `listen = 9000` binds every interface. Prefer a private address such as `10.0.1.5:9000`, or `127.0.0.1:9000` on one host. 2. **`listen.allowed_clients`** — a comma-separated list of client IPs allowed to connect. It only makes sense with TCP; left blank, **any** address is accepted. 3. **Firewall** the port as well; FPM's own list is a second line, not the only one. ## Why an exposed FastCGI port is critical FastCGI has no authentication. Whoever can connect sends the request parameters, and PHP-FPM trusts them: - `SCRIPT_FILENAME` decides **which file** PHP executes — any PHP file on the server's disk that the pool's user can read and that passes `security.limit_extensions`. - `PHP_VALUE` and `PHP_ADMIN_VALUE` parameters are parsed as ini settings for that request — the second at system level — so an attacker can change settings such as `auto_prepend_file` and turn a request into arbitrary code execution. So a reachable FPM port is equivalent to handing out a shell as the pool user. That is why the sample file binds to `127.0.0.1` and why internet-facing FPM ports are a well-known audit finding. ## Choosing - **Same host, one web server:** Unix socket, with `listen.owner`/`listen.group` set to the web server's user and group. - **Several pools on one host:** one socket per pool, `/run/php/$pool.sock`. - **Web tier and PHP tier on separate machines or containers:** TCP on a private network, `listen.allowed_clients`, firewall rules. - **Container sidecars sharing a volume:** a Unix socket in the shared volume works and removes the network exposure. ## Diagnosing a broken connection Every one of these shows up to the user as **502 Bad Gateway**; the web server's error log tells them apart: - **Connection refused** — nothing listens on that TCP address and port: FPM is down, or the pool listens elsewhere. - **No such file or directory** — the socket path in the web server differs from the pool's `listen`, or FPM is not running (it creates the socket when it starts). - **Permission denied** — the socket exists but its owner, group or mode excludes the web server's user. After changing `listen`, reload FPM and update the web server's FastCGI target to match.
- After adding a new pool, nginx logs a permission-denied error on the socket and returns 502. What do you check?The socket file's owner, group and mode. Without `listen.owner`/`listen.group`, FPM creates it owned by the master's user (usually root) with mode `0660`, so the web server's user cannot connect. Set `listen.owner` and `listen.group` to the web server's user and group, or use `listen.acl_users`, then reload PHP-FPM.
- Why is `listen = 9000` riskier than `listen = 127.0.0.1:9000`?A bare port binds on all addresses, so any host that can route to the machine can speak FastCGI to the pool unless a firewall or `listen.allowed_clients` stops it. A FastCGI client picks `SCRIPT_FILENAME` and can send `PHP_VALUE`/`PHP_ADMIN_VALUE` ini overrides, which amounts to remote code execution.
saying these in an interview costs you the question
- A Unix socket can be used by a web server on another machine
- listen.allowed_clients also restricts access to a Unix socket
- FastCGI authenticates the web server, so an open port is harmless
- TCP is always much slower, so it should never be used
- The default socket permissions let any local user connect