152 lines
8.0 KiB
Markdown
152 lines
8.0 KiB
Markdown
# 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.
|