16 KiB
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
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. Each Web Service may
use local PHP settings or reference a shared php_fpm_profiles entry from the
normal revisioned configuration; its local settings are retained on detach.
Shared PHP-FPM profiles (0.29.4)
In Web Server → Edit Web Service → PHP-FPM, select Local configuration or a named profile. Manage PHP-FPM profiles opens a modal with an expandable editor for every profile and an Add profile action. A profile contains the same PHP execution, FastCGI and PHP_VALUE/PHP_ADMIN_VALUE settings as local configuration. Changes to a shared profile affect all attached services after validation and apply. In-use profiles cannot be deleted.
Configuration example:
{
"php_fpm_profiles": [{
"id": "php82", "name": "PHP 8.2 FPM",
"php": {"enabled": true, "endpoint": "unix:/run/php/php8.2-fpm.sock"}
}],
"web_services": [{
"id": "site", "name": "Site", "document_root": "/var/www/site",
"php_profile_id": "php82"
}]
}
Unspecified PHP fields inherit the safe serde defaults. A service without
php_profile_id uses its own php object as in earlier versions.
Runtime model
RustProxy implements the FastCGI protocol directly. It does not spawn php, php-cgi or shell processes for individual requests.
Supported endpoint syntax:
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
{
"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
path_info: default-off secure script/path splitting. When enabled,/index.php/foo/barmay resolve the existingindex.phpscript and pass/foo/barasPATH_INFO.pass_authorization: forwards the inboundAuthorizationheader asHTTP_AUTHORIZATION. Disable it when the application does not need that header.max_request_body_bytes: streaming request-body limit enforced before/during FastCGI forwarding.0means no additional RustProxy-side limit. A request exceeding the configured value receives413 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.0disables stderr capture; maximum 65536.custom_params: trusted static FastCGI parameters supplied by the operator.php_values: php.ini-style overrides encoded into one validated FastCGIPHP_VALUEparameter.
Custom FastCGI parameter syntax
In the administration UI use one entry per line:
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:
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 available separately through the validated administrator INI overrides for trusted PHP-FPM endpoints. 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:
$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.
Roundcube 1.7 static.php / PATH_INFO (0.29.5)
Roundcube serves CSS, JavaScript and images through URLs such as
/static.php/skins/elastic/styles/styles.min.css?s=123. These paths are
PHP-FPM requests to the existing public_html/static.php script, not
static filesystem lookups. Configure the selected Web Service (or attached
shared PHP-FPM profile) with php.enabled: true, php.path_info: true, and
php.extensions: ["php"]. Point its root to the Roundcube public_html tree.
You can keep the normal Web Service try_files ($uri, $uri/,
/index.php?$query_string). RustProxy 0.29.5 resolves a valid PHP script plus
PATH_INFO after security checks and rewrites, before falling back through
try_files. No regex rewrite or extra Web Service is required for these URLs
when the main vhost selects this Web Service.
For /static.php/skins/elastic/images/favicon.ico?s=123, FastCGI receives:
SCRIPT_NAME=/static.php
SCRIPT_FILENAME=<document_root>/static.php
PATH_INFO=/skins/elastic/images/favicon.ico
REQUEST_URI=/static.php/skins/elastic/images/favicon.ico?s=123
QUERY_STRING=s=123
PHP-FPM still needs filesystem access to static.php and the Roundcube
installation outside public_html that script reads. The PHP PATH_INFO
option is explicit and remains off by default for unrelated services.
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_METHODSCRIPT_FILENAMESCRIPT_NAMEPATH_INFOwhenpath_infois enabled and a trailing path was securely split from an existing PHP scriptQUERY_STRINGREQUEST_URIDOCUMENT_ROOTSERVER_PROTOCOLSERVER_NAMESERVER_PORTREMOTE_ADDRREMOTE_PORTwhen availableHTTPSGATEWAY_INTERFACESERVER_SOFTWAREREDIRECT_STATUSCONTENT_TYPECONTENT_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 splitting is optional and disabled by default. When enabled, RustProxy searches for an existing configured PHP script prefix inside the document root, keeps SCRIPT_FILENAME/SCRIPT_NAME on that script, and supplies only the remaining suffix as PATH_INFO. REQUEST_URI remains the original request target.
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. When global/vhost caching is enabled, explicitly cacheable
PHP-FPM responses may enter the shared RustProxy cache; responses with
Set-Cookie, Cache-Control: private or no-store cannot be stored. In
particular, do not enable forced caching of authenticated WordPress pages
without auditing the origin response policy. HTTP/2 and HTTP/3 :authority
requests populate the HTTP_HOST FastCGI parameter, retaining the original
host/port from the incoming request when provided.
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:
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:
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.
Per-Web-Service PHP administrative INI overrides (0.29.2)
php.php_admin_values is a validated operator-managed string map sent in a single
PHP_ADMIN_VALUE FastCGI parameter. Example:
"php": {
"enabled": true,
"path_info": true,
"endpoint": "unix:/run/php/php-fpm-mail.sock",
"php_admin_values": {
"open_basedir": "/var/www/webmail/:/tmp/",
"display_errors": "Off"
}
}
Use separate Web Services for distinct per-location PHP settings, then assign those
services to the corresponding Custom Locations. Each Web Service can refer to the
same trusted PHP-FPM socket. The setting does not isolate worker process
identities or file permissions. Never expose PHP-FPM FastCGI to untrusted clients.
HTTP headers cannot supply PHP_ADMIN_VALUE, and php_values must not repeat
an admin directive. Control characters and excessive values are rejected.
Prometheus metrics use only configured Web Service IDs, never script paths,
URLs, PHP values, or dynamic user labels. rust_proxy_php_requests_total and
rust_proxy_php_responses_total{class=...} count responses after headers have
been received (including proxy-generated failures); the cumulative header
latency metric intentionally excludes streamed body time.
The same snapshot also exports rust_proxy_web_server_requests_total,
rust_proxy_web_server_responses_total{class=...} and
rust_proxy_web_server_response_header_duration_microseconds_total for all
Web Server requests (PHP and non-PHP).