This commit is contained in:
Mateusz Gruszczyński committed 2026-10-11 13:56:30 +02:00
1 parent 95c55da2ee
commit d942c1a35d
42 files changed
+1265 -109

No files matched your search

+151
View File
@@ -0,0 +1,151 @@
# 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.