How would you run two PHP applications under different Unix users behind one Nginx, using PHP-FPM pools?
answer
- one pool file per application
- user and group per pool
- one socket per pool, web server in group
- php_admin_value for open_basedir and logs
- master must run as root
basics
~20 sDefine one PHP-FPM pool per application, each with its own user and group, its own listen socket that the Nginx user can open, and locked per-app settings via php_admin_value; each Nginx server block passes PHP requests to its app's socket.
solid answer
~40 sCreate a pool file per application, such as `[shop]` and `[blog]`, each with `user`/`group` set to that application's Unix account, so a compromise or bug in one runs with no rights over the other's files. Give each pool its own socket, `listen = /run/php/$pool.sock`, and set `listen.owner`/`listen.group` so the Nginx user can connect while the socket stays `0660`. Lock per-application settings with `php_admin_value`: `open_basedir` to the app's directory, its own `error_log`, `session.save_path` and upload temp dir, since shared session and temp directories would leak across users. Size `pm` and `pm.max_children` per pool. The FPM master must run as root to switch users. In Nginx, each server block's PHP location passes to its own socket, and file permissions let each pool user read its code and write only its upload and cache directories.
go deeper
Recall that each PHP-FPM pool can run as its own Unix user and that each app should have its own pool and socket.
Explain the pool file: user/group, a socket the web server can open, per-pool php_admin_value settings and per-pool pm limits.
Close the leaks pools do not close by themselves: shared session and temp directories, code permissions, open_basedir, and a master that must run as root.
Decide when pool-level isolation is enough and when tenants need separate FPM instances, containers or hosts.
## The goal Two applications — say a shop and a blog — on one server behind one Nginx. If both run under the same Unix user, a vulnerability in the blog (an upload bug, an outdated plugin) gives an attacker read and write access to the shop's code, configuration and database credentials. PHP-FPM **pools** let each application run under its **own Unix user** with its own socket and settings, so the damage stays inside one application's permissions. ## Step 1: a pool file per application Pools are sections in files included by `php-fpm.conf` (commonly `pool.d/*.conf`). Each section name becomes the pool name, available as `$pool` inside the section. ```ini [shop] user = shop group = shop listen = /run/php/$pool.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 20 pm.start_servers = 4 pm.min_spare_servers = 2 pm.max_spare_servers = 6 php_admin_value[open_basedir] = /srv/shop:/tmp/shop php_admin_value[upload_tmp_dir] = /tmp/shop php_admin_value[session.save_path] = /var/lib/php/sessions/shop php_admin_value[error_log] = /var/log/php/shop-error.log php_admin_flag[log_errors] = on ``` The `[blog]` pool mirrors it with its own user, paths and limits. ## Key directives and why - **`user` / `group`** — the account the workers switch to after being forked. This only works when the FPM master runs as **root**; the master itself does not serve requests. If the group is omitted, the user's group is used. Running a pool as root requires starting FPM with `--allow-to-run-as-root` and should never be done. - **`listen` per pool** — separate sockets mean Nginx chooses the application explicitly; a request for the blog can never land in a shop worker. - **`listen.owner` / `listen.group` / `listen.mode`** — the socket must be connectable by the **Nginx user**, not by the pool user. Owning it by `www-data` with `0660` lets Nginx in and keeps other local users out. - **`php_admin_value` / `php_admin_flag`** — per-pool ini settings the application cannot override with `ini_set()`: 1. `open_basedir` restricts PHP's file functions to listed directories. 2. Separate `session.save_path` and `upload_tmp_dir` stop one app reading the other's session files and uploads in a shared `/tmp` or session directory. 3. Separate `error_log` keeps logs readable by the right team. - **`pm.*` per pool** — each application gets its own worker cap, so a traffic spike on the blog cannot take the shop's workers. ## Step 2: file ownership on disk Pool users only isolate if the file permissions agree: 1. Code owned by a deploy account or the pool user, **readable** by that pool user and not by the other one (for example directories `0750` with the pool's group). 2. **Writable** only where needed — uploads, cache — and nowhere in the code tree. 3. Nginx needs read access to **static files** it serves directly, typically through group membership or a separate public directory; it does not need to read the PHP sources, which FPM opens. ## Step 3: Nginx server blocks Each virtual host passes PHP requests to its own pool socket, and the script path it sends must be the path FPM sees: ```nginx server { server_name shop.example.test; root /srv/shop/public; location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php/shop.sock; } } ``` The blog's server block is identical except for its `root` and `fastcgi_pass unix:/run/php/blog.sock`. ## Checking the result - `ps -o user,cmd -C php-fpm` (the binary name varies by distribution) shows workers titled `pool shop` running as `shop`, and `pool blog` running as `blog`. - A test script in the blog that tries to read `/srv/shop/.env` should fail under `open_basedir` and file permissions. - `php-fpm -t` validates the pool files before a reload. ## Mistakes that undo the isolation - **World-readable secrets.** A `.env` or config file with mode `0644` is readable by the other pool's user; secrets need `0640` or tighter with the right group. - **Shared writable directories.** A common `/tmp`, upload or cache directory lets one application plant or read the other's files. - **Code writable by the pool user.** If the workers can modify the code tree, any upload bug becomes persistent code execution; keep code owned by a deploy account. - **One account reused.** Copying a pool file and forgetting to change `user` quietly puts both applications back under one identity. ## Limits of this isolation Pools separate **users and PHP settings**, not the kernel: both applications still share the CPU, memory, the FPM master and the Nginx process. For stronger isolation — separate PHP versions, resource limits, untrusted tenants — use separate FPM instances, containers or hosts.
- Why is open_basedir set with php_admin_value rather than php_value?Settings from `php_admin_value` are locked: the application cannot change them with `ini_set()`, and per-directory `.user.ini` files cannot override them. With `php_value`, the setting stays changeable wherever the directive's INI mode allows, so a `.user.ini` file written by a compromised application could replace it.
- The pools run as different users, yet the blog can still read the shop's sessions. What was missed?Both pools probably use the same `session.save_path` (and possibly the same temp directory) with permissions that let both users read files there. Give each pool its own `session.save_path` and `upload_tmp_dir` through `php_admin_value`, owned by that pool's user.
saying these in an interview costs you the question
- The socket should be owned by the pool user so Nginx cannot read it
- Setting user in the pool works even when the master runs unprivileged
- Separate pools also give each app its own memory and CPU limits from the kernel
- Both apps can share one socket if they have different document roots
- php_value is enough to enforce open_basedir