Add JA4/TLS fingerprint debug endpoint at /_ts/debug/ja4#646
Add JA4/TLS fingerprint debug endpoint at /_ts/debug/ja4#646
Conversation
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Thanks for adding the temporary JA4/TLS debug endpoint and route coverage. The implementation is straightforward and CI is green, but before this ships I think the endpoint should be explicitly gated so it cannot become a public same-origin fingerprint reflection API by default.
Additional finding folded into body
Document the debug endpoint and config gate
If this endpoint is retained behind a new trusted-server.toml debug flag, please also document the flag and endpoint behavior in the API/config docs. The docs should make clear that this is temporary/debug-only, disabled by default, what fields are returned (ja4, h2_fp, cipher, tls_version, user-agent, ch-mobile, ch-platform), the fallback values, and that the response uses Cache-Control: no-store, private.
aram356
left a comment
There was a problem hiding this comment.
Summary
Adds a GET /_ts/debug/ja4 endpoint to the Fastly adapter for inspecting JA4/H2/TLS fingerprint metadata and Client Hints. CI is green and the route-level test correctly exercises dispatch. Two blocking concerns remain before this can ship: the endpoint is reachable unauthenticated by default (carried over from the prior review round), and its Cache-Control: no-store, private header is silently overridable by operator [response_headers] because the response flows through finalize_response.
Blocking
🔧 wrench
- Public-by-default fingerprint reflection oracle:
/_ts/debug/ja4is not protected by basic-auth in the default config and reflects values JS cannot otherwise read directly (main.rs:217). Gate behind a debug flag, restrict to staging, or move under an admin path. Cache-Controloverridable by operator[response_headers]: handler-setno-store, privateis replaced byfinalize_responseif operators setCache-Controlin[response_headers](main.rs:134 → main.rs:349-351). Handle the route alongside/healthbeforeroute_requestruns.
Non-blocking
♻️ refactor
- Unit test duplicates the route-level test: identical assertions; route-level test is strictly stronger (main.rs:359-411 vs route_tests.rs:253-317).
🤔 thinking
- Defensive
Varyheader:User-Agent, Client-Hints, and TLS metadata vary the response — useful backstop if the override onCache-Controlis fixed (main.rs:133-136).
🌱 seedling
- Track removal: PR description and code call this "temporary" but neither has a removal trigger or follow-up issue. Add a
// TODO: remove after JA4 evaluation completes — see #645and consider a scheduled cleanup PR.
📝 note
- Bypasses platform abstraction for fields that have one:
tls_protocolandtls_cipherare available viaruntime_services.client_info()(main.rs:115-116). JA4/H2 are Fastly-specific so direct calls are unavoidable for those; mixing in the abstraction for fields that have one would be more consistent.
⛏ nitpick
- Use
header::CACHE_CONTROLin test assertions instead of"cache-control"(main.rs:376, route_tests.rs:282). - Extract magic strings to
consts:"unavailable","not sent","none"and label strings are duplicated across production and both tests (main.rs:113-130).
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
- wasm32-wasip1 release build: PASS
| ); | ||
|
|
||
| Response::from_status(fastly::http::StatusCode::OK) | ||
| .with_header(header::CACHE_CONTROL, "no-store, private") |
There was a problem hiding this comment.
🔧 wrench — Cache-Control is silently overridable by operator [response_headers]
The handler sets Cache-Control: no-store, private, but the response then flows through finalize_response, whose final loop unconditionally calls response.set_header(key, value) for every operator-configured [response_headers] entry (crates/trusted-server-adapter-fastly/src/main.rs:349-351). If an operator configures Cache-Control = "public, max-age=..." (a common edge default), our no-store, private is silently replaced and a sensitive fingerprint response can land in shared caches.
The route-level test does not catch this because create_test_settings() does not configure [response_headers]. The finalize_response doc says "operators can intentionally override any managed header" — that contract is acceptable for cosmetic headers, not for cache-control on a debug endpoint that exposes TLS metadata.
Fix: Handle this route the same way /health is handled — before route_request/finalize_response (top of main(), after init_logger):
if req.get_method() == Method::GET && req.get_path() == "/_ts/debug/ja4" {
build_ja4_debug_response(&req).send_to_client();
return;
}That also avoids paying the cost of building the auction orchestrator and integration registry for a simple debug probe, and pairs naturally with the staging-only gate suggested in the other 🔧.
There was a problem hiding this comment.
Fixed — handler moved to main() after get_settings() but before build_orchestrator(), same pattern as /health. Calls send_to_client() directly and returns, bypassing finalize_response entirely. Both the enabled and disabled (404) cases are handled there.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
I reviewed the current branch again and am only leaving findings that do not appear to duplicate the existing pending review comments. These are non-blocking cleanup/documentation accuracy items.
Summary
GET /_ts/debug/ja4endpoint to the Fastly adapter to surface JA4 TLS fingerprint, H2 fingerprint, cipher suite, TLS version, user-agent, and selected Client Hints for browser fingerprint evaluation/_ts/debug/ja4router pathChanges
crates/trusted-server-adapter-fastly/src/main.rsbuild_ja4_debug_response, registeredGET /_ts/debug/ja4, and added unit coverage for fallback response fieldscrates/trusted-server-adapter-fastly/src/route_tests.rs/_ts/debug/ja4dispatch, content type, and fallback body valuesCloses
Closes #645
Test plan
cargo fmt --all -- --checkcargo test --workspacecargo clippy --workspace --all-targets --all-features -- -D warningscargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1cargo test,vitest,integration tests,browser integration tests,format-docs,format-typescript,cargo fmt, and CodeQLfastly compute serveChecklist
unwrap()in production code - useexpect("should ...")logmacros (notprintln!)