Files
RustProxy/docs/IMPLEMENTATION_0.31.4.md
T
2026-10-11 13:56:30 +02:00

8.0 KiB

RustProxy 0.31.4 - Administration-panel Origin pinning and simplified HTTPS settings

Scope

Only the administration panel/control-plane authentication and its settings, OpenAPI, translations and regression tests change. No public vhost/proxy routing, backend forwarding, stream, session-store or database schema behavior is altered. Credential-based certificate API automation remains on its existing validation path. The 0.31.3 BFCache, manual retry, SSE/polling and startup error protection remains unchanged.

Evidence and failure mode

0.31.3's external_admin_scheme rejects a trusted X-Forwarded-Proto: https, http value with auth.invalid_origin and reason external_scheme before checking a session, even for GET /api/auth/session without an Origin header. This may appear intermittently if a multi-hop reverse proxy sometimes appends rather than replaces the header. The previous Node.js HTTP reconstruction reproduced this with 3/12 intentionally malformed GETs; it does not prove the real user's reverse proxy behaves that way.

Patch design

administration.public_origin is an optional, explicit HTTPS origin:

  • Requires one canonical DNS hostname; that hostname is derived for the effective panel Host allowlist without repeating it in administration.allowed_hosts. The setting forbids credentials, userinfo, path, query, fragment, HTTP and ambiguous input.
  • Is active only after restart and only when the immediate TCP peer is loopback or is already in the configured trusted-proxy list. It does not implicitly trust another proxy or the browser's Host/forwarding headers.
  • Keeps checking the actual request Host against the administration listener policy. If a forwarded Host exists, it must be a single authority and match the pinned host and effective port. Malformed/userinfo/duplicate values are rejected.
  • For the pinned panel, accepts a bounded (maximum eight hops) protocol chain only if the first hop is literally https and every hop is http or https. This permits https, http but not http, http, https, ftp, empty values, or arbitrarily long chains. Missing XFP is allowed only because the explicit administrator pin defines the external HTTPS identity.
  • Uses the configured origin for the browser Origin comparison, correct Secure session cookie and the existing CSRF checks. Existing Sec-Fetch-Site: cross-site blocking is unchanged.
  • Does not alter the validation path when the pin is absent, the peer is untrusted, or certificate-automation credentials are present.

On every rejection, the existing bounded, credential-redacted diagnostics remain available. New pinned_origin_mismatch identifies a disagreement with the explicit pin. Do not log cookies, CSRF tokens or Authorization headers.

Simplified hostname configuration (included in this 0.31.4 reissue)

Operator change

For the panel behind a trusted reverse proxy, Settings -> Administration now needs only the Public panel HTTPS origin such as https://panel.example.test. The DNS hostname comes from this validated administrator setting; it need not (and should not) be repeated in administration.allowed_hosts. Additional DNS hostnames remain in an optional Advanced section. Only those extra names are stored in the allowlist.

Backend validation also canonicalizes any duplicate hostname in memory. The settings page suppresses this value when loading an older configuration and omits it on the next save. Other explicitly allowed hosts remain. Removing/changing the pin does not automatically retain the old hostname as an extra allowlist entry. For legacy duplicated values, this normalization occurs only when the administrator saves; there is no silent SQLite mutation on startup.

Configuration example

{
  "administration": {
    "public_origin": "https://panel.example.test",
    "allowed_hosts": []
  }
}

This field still requires an HTTPS URL with one DNS hostname and an optional port; userinfo, fragment, path, HTTP, IP literals and localhost remain invalid. Changing either access-control field needs a RustProxy restart.

Implementation and security

  • Administration::validate normalizes the public URL independently of the optional additional DNS list. Administration::include_public_origin_host generates an effective runtime/desired Host allowlist once on startup / status read, without adding to the persisted configuration or accepting a wildcard. The originally saved allowlist is unchanged.
  • admin_host_allowed and configured_admin_public_authority continue to check the actual Host and the trusted last-hop proxy. Forwarded Host remains single and identical to the pin. Only a bounded, first-hop HTTPS X-Forwarded-Proto chain can be used with the explicit pin; no arbitrary Origin or Sec-Fetch-Site: cross-site or missing CSRF is permitted.
  • The explicitly opted-in self-admin vhost case may pass the reserved session cookie when the incoming HTTPS host equals the pin; it still requires a backend targeting the administration listener directly by IP and the prior accept_local_listener_risk opt-in. No other proxy traffic changes.
  • No new configuration key or SQLite migration. Older allowed_hosts lists remain accepted, preserving backwards compatibility. Existing 0.31.3 mobile/BFCache recovery is untouched.

Tests and acceptance gate

New Rust source tests also cover pin-only saved configuration, derived runtime allowlist, additional legacy entries, changes/removal, invalid URL input, same-origin session and Secure cookie, foreign Origin, untrusted peers, forwarded-host spoofing and CSRF. Node.js runs a focused real settings-helper test and a local HTTP simulation of the guard; these do not execute the Rust server. Exact results are recorded in TEST_REPORT.

Production acceptance remains OPEN until Rust 1.99+ builds/tests pass with a resolved Cargo.lock; a trusted reverse proxy and real mobile Chrome/Safari are checked for repeated BFCache resumes, session expiry and negative Origin/Host/CSRF cases. Never weaken the trusted-proxy list or disable Origin validation to avoid auth.invalid_origin.

Test coverage

Rust source tests in src/settings/tests.rs and src/admin/tests/part_11.rs cover config parsing/normalization, persisted legacy defaults, restart gating, trusted/foreign hosts, unknown/duplicate headers, cross-site metadata, HTTP first hops, genuine same-origin session GET with multi-hop metadata, HTTPS Secure session cookie, CSRF enforcement and opt-out behavior.

tools/checks/admin_public_origin_0314_sim.mjs and tools/checks/admin_public_origin_0314_config_sim.mjs perform additional HTTP guard simulations, not compiled RustProxy execution. tools/checks/admin_panel_config_0314_ui_test.mjs checks the settings UI and form behavior. Record its result in docs/TEST_REPORT.md only after it is run.

Required production acceptance

  1. bash scripts/resolve_dependencies_0312.sh on a Rust 1.99+ builder with network/local audited dependencies; inspect regenerated lockfile.
  2. ./dev.sh check && ./dev.sh test && ./dev.sh release and Go tests.
  3. Set an exact HTTPS panel origin (no duplicate DNS allowlist entry), restart, and verify with a trusted real edge proxy. Keep RustProxy administration listening on an isolated/private socket. Verify the first trusted hop sanitizes headers.
  4. On iOS Safari and Android Chrome, perform 20+ visibility/BFCache resumes, full refreshes, sessions renewed/expired, and a deliberate temporary 403. Check HTTP 200 for valid /api/auth/session and Secure cookie; no reload loops, blank screens, request floods, or leaked SSE/polling tasks.
  5. Inject cross-origin, duplicate/foreign forwarded Host, downgraded protocol, missing CSRF and untrusted-source requests; they must still fail. Confirm certificate API automation and public vhosts behave identically to 0.31.3.

The original production cause cannot be declared fixed without representative mobile/reverse-proxy request logs. This artifact is not compiler-verified unless the release-host gate above passes.