# 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 ```json { "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](TEST_REPORT.md). 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.