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

+3 -3
View File
@@ -65,9 +65,9 @@ The `rustproxy_session` cookie is HttpOnly, SameSite=Strict and Path=/ with the
administration adds the Secure attribute; behind a trusted HTTPS-terminating reverse proxy
the external forwarded scheme controls that flag. A protected SSH/VPN tunnel remains valid,
while direct remote administration should use HTTPS plus firewall/private-network policy.
Direct IP/localhost Host values must use the actual administration port. A DNS name explicitly
listed in `administration.allowed_hosts` represents the browser-facing authority and may use
the trusted reverse proxy's external port. Foreign Origin/cross-site requests are rejected.
Direct IP/localhost Host values must use the actual administration port. A DNS name explicitly listed in `administration.allowed_hosts`, or automatically
derived from the validated `administration.public_origin`, represents the
browser-facing authority and may use the trusted reverse proxy's external port. Foreign Origin/cross-site requests are rejected.
There is no permissive CORS mode.
Auth request bodies are limited to 4096 bytes.
+5 -4
View File
@@ -9,16 +9,17 @@ new bind address and allow explicit DNS hostnames. A persisted non-loopback bind
accepted when HTTPS is enabled. The legacy startup flags remain only for deliberately
plain remote HTTP. Public vhost `/docs` paths are not captured by the admin listener.
When the dashboard is published through a trusted reverse proxy, an explicit
`administration.allowed_hosts` DNS entry is matched against the external authority rather
than the private admin socket port. `X-Forwarded-Host` is trusted only from loopback or a
When the dashboard is published through a trusted reverse proxy, the hostname of
the optional `administration.public_origin` is automatically allowed for panel
Host validation. Any extra `administration.allowed_hosts` DNS entry is also
matched against the external authority rather than the private admin socket port. `X-Forwarded-Host` is trusted only from loopback or a
configured trusted proxy; the external `X-Forwarded-Proto` additionally requires the
forwarded-proto trust setting except for loopback. Origin/CSRF checks and Secure cookies use
that external origin. Direct IP/localhost access still requires the real administration port.
### Publishing the panel through RustProxy itself
0.16.0 supports the explicitly opted-in same-process topology where a public HTTPS vhost proxies to the local administration listener. The reserved `rustproxy_session` cookie is passed through only when all of these conditions are true: the browser-facing request is TLS, its DNS hostname is explicitly present in `administration.allowed_hosts`, the selected backend directly addresses the configured administration socket by IP, and that backend has `accept_local_listener_risk=true`. The control-plane route is forced out of shared cache.
0.16.0 supports the explicitly opted-in same-process topology where a public HTTPS vhost proxies to the local administration listener. The reserved `rustproxy_session` cookie is passed through only when all of these conditions are true: the browser-facing request is TLS, its DNS hostname either matches the pinned `administration.public_origin` or is explicitly listed in `administration.allowed_hosts`, the selected backend directly addresses the configured administration socket by IP, and that backend has `accept_local_listener_risk=true`. The control-plane route is forced out of shared cache.
This is a narrow exception to the normal isolation rule. For every ordinary backend RustProxy still removes `rustproxy_session` before forwarding requests and strips an upstream attempt to set that cookie in responses. DNS backend names are not accepted for the special self-admin cookie path. This prevents a normal application backend from reading or replacing the administration session.
+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.
+2 -1
View File
@@ -31,6 +31,7 @@ This is the canonical index for the current project documentation. The authentic
- [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish.
- [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing.
- [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone.
- [0.31.4 administration public Origin and settings](IMPLEMENTATION_0.31.4.md) - opt-in HTTPS origin, one-field DNS allowlist, additional hosts in advanced settings, legacy deduplication, CSRF and remaining deployment checks.
- [0.31.3 mobile session / invalid-Origin recovery](IMPLEMENTATION_0.31.3.md) - deterministic BFCache and visibility session coordination, 403 diagnostics, recovery and remaining acceptance.
- [0.31.1 mobile panel bootstrap](IMPLEMENTATION_0.31.1.md) - panel script readiness, versioned assets, failure recovery and acceptance tests.
- [0.31.0 security audit](SECURITY_AUDIT_0.31.0.md) - current API/panel/BAN/PURGE attack-surface review, hardening and remaining runtime verification.
@@ -82,7 +83,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [IMPLEMENTATION_0.31.3.md](IMPLEMENTATION_0.31.3.md); [IMPLEMENTATION_0.31.1.md](IMPLEMENTATION_0.31.1.md) is retained as the prior mobile milestone; [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md) is retained as the previous milestone; [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [IMPLEMENTATION_0.31.4.md](IMPLEMENTATION_0.31.4.md); [IMPLEMENTATION_0.31.3.md](IMPLEMENTATION_0.31.3.md) is retained as the previous mobile recovery milestone; [IMPLEMENTATION_0.31.1.md](IMPLEMENTATION_0.31.1.md) is retained as the prior mobile milestone; [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md) is retained as the previous milestone; [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
+66
View File
@@ -114,12 +114,78 @@ accepted only when `administration.tls` is true. Plain remote HTTP remains avail
"tls": true,
"certificate_id": "admin-example",
"allowed_hosts": ["admin.example.com"],
"public_origin": null,
"session_days": 30,
"panel_compression": {"zstd": true, "br": true, "gzip": true}
}
}
```
### Fixed public origin for a reverse-proxied panel (0.31.4)
For a **panel** reached through a trusted reverse proxy, set the optional
`administration.public_origin` in **Settings → Administration** to the exact
browser-facing HTTPS origin (for example `https://panel.example.test`).
**Do not duplicate that hostname in `administration.allowed_hosts`.** Its DNS
hostname is implicitly included in the runtime panel Host allowlist. **Restart
RustProxy**; this is a restart-bound access-control setting. Omit it (`null`) for
direct administration or when forwarded headers are canonical. This setting
is not a wildcard, does not accept HTTP or a URL path, and requires a DNS
hostname rather than an IP address. SQLite schema 9 remains unchanged.
The advanced **Additional allowed DNS hostnames** field controls independent,
extra hostnames only. Older configurations containing both values are
accepted; on editing/saving the redundant duplicate is omitted from the
persisted list. If `public_origin` changes or is cleared, its old hostname is
no longer implicitly allowed (unless explicitly retained as an additional host).
Example for the default **private loopback** administration listener behind an
HTTPS-terminating proxy:
```json
{
"administration": {
"allowed_hosts": [],
"public_origin": "https://panel.example.test"
}
}
```
The last hop must connect from loopback or an existing explicitly trusted proxy
CIDR. If it is not loopback, configure **only the actual proxy peer CIDR** under
Forwarding → Trusted proxies; be aware the general trusted-proxy setting also
applies to public traffic and must never include untrusted clients. The
administration listener should not be reachable directly from external clients.
Do **not** grant trust to public networks to work around Origin failures.
The pinned URL, not an element of `X-Forwarded-Proto`, defines the panel
scheme/authority. A trusted, bounded proxy chain `https, http` (HTTPS on the
browser side, HTTP on an internal hop) is permitted in this explicit mode.
An `http` first hop, malformed/too-long protocol chain, duplicate/conflicting
`X-Forwarded-Host`, different `Origin`, `Sec-Fetch-Site: cross-site`, and missing
CSRF protection for state-changing requests still result in HTTP 403. The
public proxy/vhost data plane is **not changed**, except that an already opted-in
self-admin vhost can recognize the pinned hostname for its reserved panel session
cookie; other upstreams continue to strip it. Credential-bearing certificate
automation retains its original TLS/forwarding checks.
Prefer fixing the proxy's forwarding headers rather than configuring the pin
when practical. For a TLS-only public entry, the closest trusted hop should
**replace**, not append, forwarded headers using known values, for example:
```nginx
proxy_set_header Host panel.example.test;
proxy_set_header X-Forwarded-Host panel.example.test;
proxy_set_header X-Forwarded-Proto https;
```
Only use that `https` setting on an entry that actually verifies external TLS;
never trust an arbitrary inbound `X-Forwarded-Proto`. If multiple intermediate
proxy hops must append protocol values, the pinned mode provides an explicit
server-side comparison target without accepting a client-supplied origin as
authoritative. Confirm deployment using a real mobile browser and the safe
`administration origin rejected` logs before production rollout.
**Administration → Panel content compression** (next to the admin listener/TLS settings)
controls Zstd, Brotli and gzip for the authenticated HTML document, JavaScript,
CSS and language bundles. Each browser request is negotiated using
+27 -1
View File
@@ -1,4 +1,30 @@
# Verification report - RustProxy 0.31.3
# Verification report - RustProxy 0.31.4
## 0.31.4 reissue - origin protection and one-field administration public URL (2026-10-11)
- **Reissue verification (2026-10-11):** rebased the complete previously prepared simplified-config source onto release identifier `0.31.4` in the manifest, existing resolved-lock snapshot package entry, OpenAPI, source/version checks, documentation and deterministic ZIP metadata. Merged the panel Origin and one-field config release notes; no backend validation logic changes were introduced during this renumbering.
- **PASS — executed for this reissue:** `version_contracts.py`, `docs_check.py`, `langs_check.py`, `check_source.py`, `release_smoke.py`, `security_contracts.py`, `dependency_upgrade_0312.py`, `rust_module_contracts.py`, `system_info_no_subprocess_contracts.py`, `source_module_contracts.py`, `memory_contracts.py`, `web_server_contracts.py`, `admin_surface_audit_0310.py`, `security_dependency_contracts.py`; 15 standalone Node frontend/simulation scripts (including the updated original 53-case and expanded 61-case administration simulations and 9 UI scenarios); `go test ./...` for `docker-agent`; `bash -n dev.sh` and OpenAPI JSON parsing.
- **Not executed for this reissue:** full Rust compile/test/release (`cargo` and `rustc` absent), real mobile Safari/Chrome or live reverse-proxy verification. Passing Node simulations is **not** proof of compiled Rust behavior. The inherited Cargo.lock requires verification and/or re-resolution on the declared Rust 1.99+ toolchain.
- **Scope:** panel administration settings/config and narrow self-admin cookie opt-in only. `allowed_hosts` stores only additional DNS hostnames; the HTTPS pin contributes its hostname to the active/desired Host allowlist without persisting a duplicate.
- **Compatibility:** older configurations with a duplicate hostname remain valid; validation canonicalizes the in-memory config and the next save omits the duplicate without an automatic SQLite migration; changing/removing the pin does not silently retain a stale implicit hostname.
- **Security:** Host, single forwarded Host, trusted reverse proxy, first-hop HTTPS, CSRF and `Origin`/cross-site guard rules are unchanged. No SQLite schema migration, TLS listener or other vhost route changes.
- **PASS — source/release gates:** `version_contracts.py`, `rust_module_contracts.py`, `check_source.py`, `release_smoke.py`, `docs_check.py`, `langs_check.py`, `security_contracts.py`, `admin_surface_audit_0310.py`, `security_dependency_contracts.py`, and `dependency_upgrade_0312.py`; SQLite schema-9/upgrade smoke and the source/HTML/Reader/translation checks also passed.
- **PASS — focused simulated regressions:** `admin_public_origin_0314_config_sim.mjs`: **61/61 local Node HTTP guard-simulation assertions**, comprising 37 cases and 24 repeated session GETs. Invalid forwarded-proto on 3/12 unpinned GETs produced 403; with a pinned HTTPS public URL and **no duplicate DNS allowlist entry**, the same sequence produced 0/12. This simulation is not compiled RustProxy and cannot prove actual mobile/proxy headers.
- **PASS — real frontend helper/UI checks:** `admin_panel_config_0314_ui_test.mjs`: **9/9 settings-normalization scenarios** plus form/HTML/locale/OpenAPI consistency; `panel_cache_http_0310_ui_test.mjs` also executes the updated real administration form callback with a duplicated legacy hostname and verifies it is omitted on save.
- **PASS — other standalone checks:** all 14 bundled Node UI/simulation scripts, memory model (300,000 simulated PURGEs), 2,000 HTTP requests to a **Python mock peer** (not RustProxy), and `cd docker-agent && go test ./...`.
- **Harness and compiler limitations:** an initial `./dev.sh test` run exposed a legacy UI-test harness that lacked the new form helper; the harness was corrected and passed separately. A subsequent monolithic run exceeded the execution time limit after the memory/load-model stage, so no complete `./dev.sh test` success is claimed. `./dev.sh check` exited **127** because `cargo`/`rustc` are not installed. New Rust tests in `src/settings/tests.rs` and `src/admin/tests/part_11.rs` were **not compiled/executed**. The inherited Cargo.lock still needs resolution on Rust 1.99+; live mobile and real reverse-proxy acceptance remain OPEN.
### Earlier 0.31.4 validation baseline (2026-10-11)
- **User scope:** admin panel only; public vhosts, proxy forwarding, streams and database migrations are unchanged. The 0.31.3 BFCache/session resume and visible retry protections are retained.
- **Source correction:** optional `administration.public_origin` pins a browser-facing HTTPS panel origin to a configured DNS allowlist; it takes effect after restart and only with a trusted last-hop proxy. It accepts bounded `X-Forwarded-Proto: https, http` without relying on an ambiguous forwarded-proto element for the expected origin. Without the pin, 0.31.3's strict handling remains.
- **Security:** actual Host is validated, forwarded Host must be single/matching, the protocol's first hop must be HTTPS, and Origin, cross-site and CSRF checks remain. Untrusted peers and credential-bearing certificate automation retain their previous scheme validation. The earlier build required explicit `allowed_hosts`; the reissue now derives the pinned hostname without wildcard trust.
- **Node HTTP guard simulation (not compiled RustProxy): PASS, 53/53 assertions**, including 29 individual scenarios and 24 repeated session GETs: without the pin and with intentionally malformed XFP every fourth request, **3/12 GETs returned 403**; with an explicit pin and otherwise identical simulated headers, **0/12 returned 403**. `tools/checks/admin_public_origin_0314_sim.mjs` checks relevant code anchors and operates a local Node HTTP test peer. This does not confirm production headers or backend runtime behavior.
- **PASS (2026-10-11):** `./dev.sh test` passed every available Rust-independent stage: source-module boundaries, version/contracts, SQLite schema-9 migrations and upgrade, security source/dependency checks, 300,000 PURGE-model iterations, 2,000 real HTTP requests to a **Python test peer**, all bundled Node/browser UI regressions (including BFCache/resume), 0.31.4 HTTP simulation, documentation and both language packs. Exit **127** at the missing Rust/Cargo toolchain guard only. The simulated peer is **not** a running RustProxy binary. `./dev.sh check` likewise exits 127 due to missing Cargo.
- **PASS:** `(cd docker-agent && go test ./...)`, `node --check tools/checks/admin_public_origin_0314_sim.mjs`; source-package size/integrity checks are performed separately.
- **Rust regression tests added, NOT EXECUTED:** `src/settings/tests.rs` and `src/admin/tests/part_11.rs` cover pin validation, restart, strict opt-out, session Secure cookie, CSRF, foreign Origin, cross-site, host spoofing and untrusted peers.
- **Remaining release gate OPEN:** the sandbox has no Rust/Cargo/rustfmt and the inherited lockfile remains unresolved against 0.31.2+ dependencies. Before production use: resolve on Rust 1.99+, format, clippy, test/build, then capture actual mobile Safari/Chrome and reverse-proxy traffic and run the acceptance matrix in [IMPLEMENTATION_0.31.4.md](IMPLEMENTATION_0.31.4.md). No claim is made that the original field `auth.invalid_origin` was directly observed or eliminated.
## 0.31.3 - invalid-Origin resume and mobile panel recovery (2026-10-11)