12 KiB
RustProxy 0.25.0 pre-production security audit
Date: 2026-10-04
This audit is a source/configuration/dependency review of the current 0.25.0 development tree. It is not a completed penetration test because the audit environment has no Rust/Cargo toolchain and therefore cannot build or start the RustProxy binary. Production acceptance remains blocked until the runtime gates at the end of this document are executed on an isolated release host.
Result
No source-reviewed issue remains that is known to provide an unauthenticated direct path to RustProxy process takeover. Additional runtime/fuzzing gates remain open, so this is not production approval. Concrete pre-production issues found during this review were fixed in the same 0.25.0 development tree:
- The Debian one-line bootstrap and reference unit exposed plain-HTTP administration on
0.0.0.0:9090by default. They now default to127.0.0.1:9090; plain remote HTTP requires explicit opt-in and Administration HTTPS remains the preferred remote mode. - Recursive source packaging could include a local
scripts/.../.env. Exact.envfiles are now excluded from release archives while.env.exampletemplates remain included. - FastCGI
HTTP_PROXY/httpoxy protection depended on optional Web Service hardening. ClientProxyis now unconditionally discarded before FastCGI parameter generation. - PHP-FPM CGI output could supply downstream framing headers.
Content-Length, transfer/connection framing, TE/Trailer andProxy-*transport headers are now discarded so RustProxy owns downstream framing. .htaccesscompatibility could read a symlinked control file during Apply..htaccessfiles are now opened with no-follow semantics, symlink control files are rejected and recursive discovery verifies that scanned directories remain below the canonical Web Service root.- PHP-FPM CGI
Status:accepted informational codes such as101. FastCGI output is now restricted to final HTTP status codes200..=599, preventing an external PHP worker from trying to drive protocol-switch semantics through the normal HTTP response path. - The Debian unit used
UMask=0027; it is now0077, matching the reference installer unit so newly created process files default to owner-only access. - Web Service CMS presets previously changed only the preset description until the separate Apply button was pressed. Selecting a non-Custom preset now immediately materializes the ordinary form fields, and a behavioral JS regression test verifies that WordPress index/
try_files/PHP/cache values survive the Save serialization path.
The reference/generated systemd units were also aligned with the stronger Debian sandbox baseline (PrivateDevices, ProtectKernelLogs, namespace/realtime restrictions and native syscall architecture).
Tests executed
Passed in the audit environment:
python3 scripts/check_source.pypython3 scripts/web_server_contracts.pypython3 scripts/security_contracts.pypython3 scripts/security_dependency_contracts.py(offline minimum-version gates for RustSec items reviewed on 2026-10-04)node scripts/web_service_ui_test.mjs(preset selection + Save serialization regression)python3 scripts/memory_contracts.pypython3 scripts/docs_check.pypython3 scripts/langs_check.pypython3 scripts/release_smoke.pynode --checkfor all administration JavaScriptbash -nfor managed shell scriptsgo test ./...fordocker-agent- deterministic source archive generation plus assertion that no exact
.envis packaged systemd-analyze security --offline=yesfor the reference and Debian units: exposure score 3.3 (OK) in the audit environment; both now useUMask=0077- targeted source review of administration authentication/CSRF/Host-Origin handling, backend SSRF/address policy, HTTP framing limits, Web Server path confinement,
.htaccess, PHP-FPM, backup/secret storage, command execution and unsafe/FFI surfaces - hard-coded secret scan for common private-key/token patterns: no production secret found; documentation placeholders only
- manual review of the locked major network/security dependencies against current RustSec advisories, plus an offline regression deny-list for the reviewed 2026 malicious-crate campaign
./dev.sh test runs every non-Rust phase successfully and then stops because cargo/rustc are unavailable. scripts/admin_audit.py was also attempted, but no compiled/running RustProxy administration listener exists in this workspace, so its live probes are blocked/unexecuted, not passed.
Dependency review
Important locked versions reviewed manually:
rustls 0.23.45- includes the fix for RUSTSEC-2026-0285 (>=0.23.45)rustls-webpki 0.103.15- above the RUSTSEC-2026-0104 fix (>=0.103.13)quinn-proto 0.11.18- above RUSTSEC-2026-0037 (>=0.11.14) and RUSTSEC-2026-0185 (>=0.11.15) fixesh2 0.4.19- above the RUSTSEC-2026-0258 fix (>=0.4.16)bytes 1.12.1- above the RUSTSEC-2026-0007 fix (>=1.11.1)time 0.3.55- above the RUSTSEC-2026-0009 fix (>=0.3.47)sqlx 0.8.6- above the RUSTSEC-2024-0363 fix (>=0.8.1)tungstenite 0.29.0- above the historical RUSTSEC-2023-0065 fix (>=0.20.1)ring 0.17.14- above the RUSTSEC-2025-0009 fix (>=0.17.12)
This manual review is not a substitute for cargo audit/OSV scanning of the complete resolved graph on the release host.
Security boundaries reviewed
Administration
- default bind is loopback; persisted non-loopback configuration requires Administration TLS;
- private routes require a browser session; mutations additionally require exact Origin,
X-RustProxy-Request: 1and a session-bound CSRF token; - cookies are HttpOnly/SameSite=Strict and become Secure on Administration HTTPS;
- passwords use Argon2id and password work has bounded workers;
- unknown users receive equivalent password-verification work;
- certificate API credentials are scoped and require HTTPS for non-loopback requests.
Residual operational risks: browser sessions can be configured for 7-30 days, there is no 2FA/role model yet, and the global login throttle can be used for availability lockout if a remote admin listener is exposed. Keep administration private or tightly firewalled and use the shortest session lifetime for production.
Backend SSRF / routing
- backend origins are HTTP/HTTPS only;
- metadata/link-local/special targets remain blocked; private/loopback addresses require explicit opt-in;
- local RustProxy listener targeting has an additional explicit risk gate;
- DNS answers are validated before use and a validated address is used without a second DNS lookup.
Production requirement: enable tls_verify=true for HTTPS origins whenever the backend network is not fully trusted. Verification-off remains a deliberate compatibility default and permits an on-path attacker to impersonate that origin.
Web Server / filesystem
- decoded and encoded traversal/separator ambiguity is rejected;
- Unix file access uses document-root descriptors and
openat; default symlink policy usesO_NOFOLLOW; - only regular files/directories are served; PHP source extensions are denied when PHP-FPM is unavailable;
.ht*files are protected;.htaccessparsing is bounded, control files are opened/rejected with no-follow symlink handling, and unsupported access/handler directives fail closed;- directory listings re-open entries through the same confinement rules.
PHP-FPM
FastCGI is restricted to Unix sockets or loopback TCP, script selection is rooted in the Web Service document root and symlink-following PHP mode is rejected. Client Proxy cannot create CGI HTTP_PROXY; CGI response framing cannot override RustProxy downstream framing, and informational/101 CGI statuses are rejected. Operator-defined FastCGI parameters cannot override core CGI/HTTP fields or PHP_VALUE/PHP_ADMIN_VALUE; php.ini overrides use a separate validated PHP_VALUE map. Optional request-body, response-header and stderr limits bound PHP connector resource usage.
The most important production boundary is external to RustProxy: run each PHP-FPM pool as a dedicated non-root, non-rustproxy user. A compromised CMS/plugin must not be able to read /var/lib/rustproxy, SQLite, the Secret Store, TLS keys or backups. Separate writable upload directories from executable PHP source where the application permits it.
Build / update supply chain
The one-line Debian bootstrap uses HTTPS but clones the moving repository head and then invokes repository deployment scripts with root privileges. That is convenient for development but is not a strong production provenance boundary: compromise of the repository/default branch at install time would become host compromise. Production installation should use a reviewed immutable commit/tag or the audited source archive, verify its SHA-256 out of band, build with --locked as an unprivileged builder, and only then run the reviewed installation step. The exact .env files are excluded from generated source archives.
Resource / protocol DoS
- public connection and inflight request counts are bounded;
- HTTP/1 header count/buffer and header-read time are bounded;
- HTTP/2 concurrent streams/header list/windows and connection lifetime/drain controls are bounded;
- WebSocket connections retain the public connection lease and have inactivity limits;
- FastCGI concurrency/timeouts and traffic analytics queues/cardinality are bounded;
- cache/transformation background work has bounded queues/permits.
A real hostile load test is still required before production because source bounds do not prove allocator/kernel behavior under mixed H1/H2/H3/WebSocket/PHP traffic.
One residual DoS gap is intentionally recorded rather than hidden: RustProxy currently streams request bodies but has no general per-vhost/location byte ceiling equivalent to client_max_body_size. Connection/inflight/body-idle/FastCGI timeout bounds prevent an unbounded in-process request-body buffer, but a client can still spend bandwidth and backend/PHP upload resources with a very large body. Before exposing public CMS upload endpoints, set conservative PHP/application limits (post_max_size, upload_max_filesize) and add/test a RustProxy request-body byte limit.
Production blockers / release-host gates
Do not treat this audit as production approval until all of these pass on the exact release artifact:
- Install stable Rust/Cargo and run
./dev.sh doctor,./dev.sh check,./dev.sh test,cargo test --lockedand a release build. - Run
cargo audit(and preferably OSV/Trivy or equivalent) against the exactCargo.lock; resolve vulnerabilities and review unmaintained/yanked transitive crates. - Start the exact release binary under the target systemd unit and run
scripts/admin_audit.pyagainst loopback. - Run active malformed HTTP tests for duplicate/ambiguous framing, oversized headers, slow headers/bodies, oversized request bodies, H2/H3 stream floods, WebSocket idle/connection exhaustion and Range edge cases.
- Fuzz/property-test
routing_path, rewrite/try_files,.htaccess, CGI header parsing and backup restore parsers. - Run filesystem escape tests using symlink races, nested links, renames and mount/bind-mount edge cases on the production kernel/filesystem.
- Exercise a real PHP-FPM pool with hostile headers/body sizes, disconnects, timeouts and an intentionally compromised test PHP application; verify that the PHP user cannot read RustProxy private state.
- Perform sustained mixed-protocol load/soak testing while monitoring RSS, FDs, tasks, cache memory and latency.
- Verify firewall policy: public only intended frontend TCP/UDP ports; admin 9090 loopback/private only unless Administration HTTPS is intentionally exposed; PHP-FPM never public. For CMS uploads, enforce tested request-body limits in PHP/application and preferably RustProxy before public launch.
- Back up and restore on an isolated host, then rotate/revoke restored sessions and credentials.
Until those runtime gates pass, status remains pre-production / runtime security verification open.