# PHP-FPM / FastCGI for the Web Server RustProxy 0.25 can execute configured PHP extensions through a long-running PHP-FPM pool while preserving the reusable Web Service architecture. ## Architecture ```text Request -> existing listener / vhost / location / ACL / auth -> Web Server -> Web Service |- static file `- configured PHP extension -> FastCGI -> PHP-FPM -> existing error/header/logging/metrics/transport pipeline ``` Vhosts and locations still reference only a Web Service. PHP settings remain part of that reusable service. ## Runtime model RustProxy implements the FastCGI protocol directly. It does not spawn `php`, `php-cgi` or shell processes for individual requests. Supported endpoint syntax: ```text unix:/run/php/php-fpm.sock tcp:127.0.0.1:9000 tcp:[::1]:9000 ``` Use the `unix:` prefix for a Unix-domain socket. Use `tcp:` followed by a literal loopback IP address and port for TCP. Hostnames and non-loopback addresses are rejected. Unix sockets are preferred for a local PHP-FPM installation; exposing FastCGI directly to an untrusted network is not supported by this configuration. ## Web Service configuration ```json { "index": ["index.html", "index.php"], "try_files": ["$uri", "$uri/", "/index.php?$query_string"], "php": { "enabled": true, "extensions": ["php"], "endpoint": "unix:/run/php/php-fpm.sock", "connect_timeout_ms": 1000, "request_timeout_ms": 30000, "max_concurrent_requests": 64, "pass_authorization": true, "max_request_body_bytes": 0, "max_response_header_bytes": 65536, "stderr_log_bytes": 4096, "custom_params": { "APP_ENV": "production" }, "php_values": { "memory_limit": "256M", "upload_max_filesize": "64M", "post_max_size": "64M" } }, "symlink_policy": "deny" } ``` The normal ordered `index` list handles `index.php`; there is no separate PHP-index option. ### Additional PHP/FastCGI options - `pass_authorization`: forwards the inbound `Authorization` header as `HTTP_AUTHORIZATION`. Disable it when the application does not need that header. - `max_request_body_bytes`: streaming request-body limit enforced before/during FastCGI forwarding. `0` means no additional RustProxy-side limit. A request exceeding the configured value receives `413 Payload Too Large`. - `max_response_header_bytes`: maximum bytes accepted before the CGI response-header terminator. The allowed range is 4096..1048576 bytes. - `stderr_log_bytes`: maximum PHP-FPM stderr bytes retained for one diagnostic request entry. `0` disables stderr capture; maximum 65536. - `custom_params`: trusted static FastCGI parameters supplied by the operator. - `php_values`: php.ini-style overrides encoded into one validated FastCGI `PHP_VALUE` parameter. ### Custom FastCGI parameter syntax In the administration UI use one entry per line: ```text APP_ENV = production APP_DEBUG = 0 TENANT = main ``` Names are normalized to uppercase and are limited to letters, digits and `_` (the first character must be a letter). Values are static: there is no request-variable interpolation in this field. RustProxy rejects core CGI parameters, `HTTP_*`, `PHP_VALUE` and `PHP_ADMIN_VALUE`, so custom values cannot replace `SCRIPT_FILENAME`, routing data, HTTP-derived fields or PHP control parameters. Up to 64 custom FastCGI parameters and 64 PHP INI overrides are accepted, with at most 128 KiB of combined custom parameter/value data. ### PHP INI (`PHP_VALUE`) syntax Use one `directive = value` entry per line: ```text memory_limit = 256M upload_max_filesize = 64M post_max_size = 64M max_execution_time = 30 ``` RustProxy validates the lines and combines them with newline separators into the FastCGI `PHP_VALUE` parameter. PHP-FPM/PHP still decides whether a directive is changeable at this scope. `PHP_ADMIN_VALUE` is intentionally not exposed. Control characters/newlines inside names or values are rejected. These custom parameters are stored as normal Web Service configuration. Do not put secrets there unless normal configuration storage is an acceptable location for them. ## `try_files` front controller A typical application can use: ```text $uri $uri/ /index.php?$query_string ``` Existing static files/directories are used first. The final path becomes the fallback and can therefore reach `index.php`. This resolution remains inside the already-selected Web Service and does not introduce a second vhost router. ## Bounded execution Each compiled Web Service owns a bounded PHP concurrency semaphore. `max_concurrent_requests` limits simultaneous FastCGI requests. RustProxy does not create an unbounded wait queue: if no execution slot is immediately available it returns `503 Service Unavailable` with `Retry-After`. Connection establishment and complete FastCGI request execution have independent configured time limits. A connect failure produces a gateway error; a request timeout produces a gateway timeout. The outer normal RustProxy error-page processing remains able to replace these responses. Request bodies are split into bounded FastCGI records rather than collected into one large buffer. When `max_request_body_bytes` is non-zero, the limit is checked both from `Content-Length` when available and while streaming chunked/unknown-length bodies. PHP-FPM stdout is parsed and delivered through a bounded response channel so application output can stream to the client. ## Secure script selection Only configured extensions are executable. Before PHP is selected, RustProxy resolves the request/`try_files`/index path using the Web Server filesystem confinement rules and verifies that it is a regular file below the document root. When PHP handling was selected: - PHP source is never returned as a fallback; - an unavailable PHP-FPM endpoint produces 502/504 behavior rather than source disclosure; - encoded traversal/separators and `..` remain rejected; - the current implementation requires `symlink_policy=deny`. The last restriction is deliberate: PHP-FPM opens `SCRIPT_FILENAME` independently, so RustProxy does not claim an `inside_root` symlink guarantee that the external process cannot enforce atomically. ## FastCGI parameters RustProxy supplies a controlled CGI/FastCGI parameter set including: - `REQUEST_METHOD` - `SCRIPT_FILENAME` - `SCRIPT_NAME` - `QUERY_STRING` - `REQUEST_URI` - `DOCUMENT_ROOT` - `SERVER_PROTOCOL` - `SERVER_NAME` - `SERVER_PORT` - `REMOTE_ADDR` - `REMOTE_PORT` when available - `HTTPS` - `GATEWAY_INTERFACE` - `SERVER_SOFTWARE` - `REDIRECT_STATUS` - `CONTENT_TYPE` - `CONTENT_LENGTH` - selected request headers as `HTTP_*` Request headers cannot become FastCGI `PHP_VALUE` or `PHP_ADMIN_VALUE` parameters. Operator-defined `custom_params` are allowed only through the validated Web Service configuration and cannot override core CGI/HTTP-derived fields. Operator-defined `php_values` are the only path that creates `PHP_VALUE`; `PHP_ADMIN_VALUE` is never generated. An inbound `Proxy` header is **never** translated into FastCGI `HTTP_PROXY` (httpoxy mitigation), regardless of the optional Web Service hardening switch. The **Block common attack probes** option additionally blocks high-confidence sensitive/probe paths before PHP-FPM dispatch. FastCGI output cannot control downstream `Content-Length`, transfer framing, connection headers or `Proxy-*` transport headers; RustProxy owns downstream response framing. `PATH_INFO` script/path splitting is not implemented in the current 0.25 development stage; applications should route explicitly to a real script, normally through `try_files`. ## Response parsing The FastCGI stdout parser understands standard CGI response headers, including `Status`. Hop-by-hop transport headers are stripped before returning the response through RustProxy. For `HEAD`, PHP-FPM can still execute normally but the client response body is empty. GET/POST/PUT and other methods can reach a PHP script once routing resolves to a configured executable extension. PHP stderr is bounded by `stderr_log_bytes` and written to the RustProxy diagnostic log; it is not sent to the client as page content. CGI response headers are separately bounded by `max_response_header_bytes`. ## Caching PHP output does not use static file validators or the static-file OS representation path. Application response headers and the normal vhost/location response-header pipeline control client/intermediary caching. The Web Service static `cache_rules` apply to local static file responses, not to PHP stdout. RustProxy does not silently place PHP responses into a new Web Server object cache. ## PHP installation RustProxy does not bundle PHP. Install and maintain PHP-FPM using the operating system/package ecosystem and enable only the extensions required by the application. Operational requirements: - a running PHP-FPM 8.x service supported by the operator's platform; - a dedicated Unix socket or loopback TCP listener reachable by the RustProxy service account; - document-root permissions usable by both RustProxy and the PHP-FPM worker identity as intended; - correct socket owner/group/mode when using Unix sockets; - PHP-FPM limits such as `pm.max_children`, application execution timeout and upload/body limits sized consistently with the RustProxy concurrency limit. Extensions such as PDO drivers, mbstring, intl, gd, curl or opcache are application dependencies, not RustProxy dependencies. ## Example PHP-FPM pool concept The exact paths/users depend on the operating system. For production, use a **separate PHP-FPM worker identity per site/application**. Never run PHP-FPM as `root` or as the `rustproxy` service account. A compromised CMS/plugin then must not inherit access to RustProxy's SQLite database, Secret Store, certificate private keys or backups. A local pool can expose a socket such as: ```ini user = site_example group = site_example listen = /run/php/rustproxy-example.sock listen.owner = www-data listen.group = rustproxy listen.mode = 0660 ``` The exact socket owner may differ by distribution; the important properties are that the PHP worker user is **not** `rustproxy`, and only RustProxy plus the pool manager can reach the socket. Configure the Web Service endpoint as: ```text unix:/run/php/rustproxy-example.sock ``` Keep `/var/lib/rustproxy` mode/private ownership inaccessible to every PHP pool. Prefer separate writable upload/data directories from executable PHP source, and prevent the application from creating executable PHP files in upload areas. A CMS-level arbitrary file upload or vulnerable plugin can otherwise become PHP code execution even when the reverse proxy itself is correct. Do not expose a PHP-FPM TCP listener to the public network. ## Verification targets The implementation includes source/unit coverage for: - PHP front-controller resolution through `try_files`; - request body delivery to a FastCGI fixture; - FastCGI response parsing; - connection failure returning a gateway error without PHP source disclosure; - bounded endpoint/time/concurrency configuration validation. Release-host acceptance should additionally exercise a real PHP-FPM runtime over Unix socket and loopback TCP and verify ACL/auth/custom errors/headers plus H1/H2/H3 client transports.