Skip to content

feat(server): TLS fingerprint (JA4/JA3) from a trusted proxy header (0.3.0) - #3

Merged
BackStacked merged 1 commit into
mainfrom
claude/project-thread-9efqn4
Oct 15, 2026
Merged

BackStacked merged 1 commit into
mainfrom
claude/project-thread-9efqn4

Conversation

@BackStacked

Copy link
Copy Markdown
Contributor

Requested by Himanshu · project thread

Before: the server side knew a request's IP, user agent and the client's device signals, but nothing about the TLS client itself.

After: readTlsFingerprint (and security.tlsFingerprint on every adapter) returns the JA4/JA3 fingerprint that the TLS-terminating proxy put in a configured header, e.g. { kind: "ja4", value: "t13d1516h2_8daaf6152771_e5627efa2ab1" }, or null.

A TLS fingerprint comes from the ClientHello, so no browser code can collect it; the proxy computes it. The header gets the same trust model as X-Forwarded-For: it is read only when trustedHops >= 1 and tlsFingerprintHeader is set. Missing, malformed, oversized or appended-to ("a, b") values give null, never an error.

How: new src/server/tls.ts (parseTlsFingerprint, readTlsFingerprint); requestSecurity, fetchSecurity, expressSecurity, fastifySecurity take tlsFingerprintHeader. README documents the header and how to set it in nginx (JA4 module), Envoy, CloudFront and Cloudflare, and adds the row to "What is collected". 3 new checks (47 total), including a real Fastify app with and without a trusted proxy. Version 0.3.0. Direct Node TLS termination is not included: Node does not expose the raw ClientHello, and the api runs behind a proxy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VT1kmGY9yJ2fLxetUn2D5c


Generated by Claude Code

…0.3.0)

readTlsFingerprint reads the fingerprint the TLS-terminating proxy puts in a
configured header, only when trustedHops >= 1 (same trust model as
X-Forwarded-For). The adapters expose it as security.tlsFingerprint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VT1kmGY9yJ2fLxetUn2D5c
@BackStacked BackStacked self-assigned this Oct 15, 2026
@BackStacked
BackStacked merged commit 8f017a1 into main Oct 15, 2026
1 check passed
@BackStacked
BackStacked deleted the claude/project-thread-9efqn4 branch October 15, 2026 20:52
BackStacked added a commit to mgpl-diagnostics/api that referenced this pull request Oct 25, 2026
…NT_HEADER) (#9)

<!-- claude-projects-attribution: {"github_login":"BackStacked"} -->
_Requested by **Himanshu** · [project
thread](https://claude.ai/code/project/chan_016JXeBZ2UxSi4N5VU2C26fQ?thread=cmsg_016JXeBZ2UxSi4N5VU2C26fQXQfucx3itZeBWpxHXi3jWG)_

Before: login and refresh bound a session to the client's device signals
only.

After: with `TLS_FINGERPRINT_HEADER=x-ja4` (and `TRUST_PROXY` ≥ 1), the
JA4/JA3 TLS fingerprint the proxy computed is passed to auth-kit at
`login` and `refreshSession`, stored as a SHA-256 hash on the session,
and a change on refresh is audited as `auth.tls_fingerprint_changed`.
`auth.device_mismatch` also records whether the TLS fingerprint matched.
Nothing changes in the GraphQL schema or the client.

A TLS fingerprint comes from the ClientHello, so only the TLS terminator
can compute it. The header is read with security-kit's trusted-proxy
rule, the same as X-Forwarded-For, and the api refuses to start with the
header set but no trusted proxy. A missing fingerprint is never an
error, and a change never blocks (browser updates and TLS-inspecting
networks change it).

How: `src/config/env.ts` (new variable and its check), `src/server.ts`
(`fastifySecurity({ tlsFingerprintHeader })` → context),
`ctx.tlsFingerprint` passed by the auth controller and service. Docs in
06 (proxy setup table), 09, 12, `.env.example`. 4 new end-to-end checks
(hash only stored, change audited, header ignored without the setting,
startup refusal); `TEST_BACKENDS=pg npm test` passes locally with the
kits built from source (112 checks).

**Draft until the kits are published:** needs security-kit 0.3.0
(mgpl-diagnostics/security-kit#3) and auth-kit 1.3.0
(mgpl-diagnostics/auth-kit#8), both merged. After publishing them, run
`npm install` here to update `package-lock.json` and push; CI will then
go green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01VT1kmGY9yJ2fLxetUn2D5c

---
_Generated by [Claude
Code](https://claude.ai/code/session_01VT1kmGY9yJ2fLxetUn2D5c)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants