Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ CORS_ORIGINS=http://localhost:3000
# sessions see the real client IP: false/0 = none (use the socket address), true = 1 (nginx or a load
# balancer), 2 = CDN + load balancer. Entries left of the trusted proxies are ignored (clients can forge them).
# TRUST_PROXY=false
# [optional] Header in which the TLS-terminating proxy sends the client's TLS fingerprint (JA4, or JA3),
# e.g. x-ja4. Needs TRUST_PROXY >= 1 and a proxy that SETS (overwrites) the header on every request
# (docs/06-auth-and-permissions.md#tls-fingerprint). Stored hashed on the session; a change is audited.
# TLS_FINGERPRINT_HEADER=x-ja4
# [optional] Largest request body in bytes (bank-statement CSV imports count against it).
# BODY_LIMIT_BYTES=2000000
# [optional] GraphiQL in the browser and schema introspection. Default: on in development, off in production.
Expand Down
2 changes: 1 addition & 1 deletion docs/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ service layer.
| [invoice-kit](https://github.com/mgpl-diagnostics/invoice-kit) | Invoice PDFs and HTML, invoice arithmetic |
| [ledger-kit](https://github.com/mgpl-diagnostics/ledger-kit) | Our own double-entry engine: ledgers, vouchers, books reports |
| [finance-kit](https://github.com/mgpl-diagnostics/finance-kit) | GST, GSTR-1/3B data, receivables, settlements, cash and bank, lock dates, exports |
| [security-kit](https://github.com/mgpl-diagnostics/security-kit) | Security headers on every response, the client IP behind proxies (`TRUST_PROXY`); in the client, the device signals |
| [security-kit](https://github.com/mgpl-diagnostics/security-kit) | Security headers on every response, the client IP and the TLS fingerprint behind proxies (`TRUST_PROXY`, `TLS_FINGERPRINT_HEADER`); in the client, the device signals |
| [client](https://github.com/mgpl-diagnostics/client) | Not used by the API; the frontend SDK that calls it |

## Design choices
Expand Down
20 changes: 20 additions & 0 deletions docs/06-auth-and-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ sends one. The client collects the signals with [security-kit](https://github.co
ends the user's other sessions; `resetPassword`, an administrator's `setUserPassword` and `disableUser`
end all of them.

### TLS fingerprint

A TLS fingerprint (JA4, or the older JA3) describes the client's TLS library from its ClientHello. No
client code can collect it: whatever terminates TLS computes it. In production that is the reverse proxy,
which sends it in a request header; set `TLS_FINGERPRINT_HEADER` to that header's name (e.g. `x-ja4`).

| Step | Where |
|---|---|
| The proxy sets the header | security-kit README, "TLS fingerprint": nginx with a JA4 module (`proxy_set_header X-JA4 $http_ssl_ja4;`), Envoy (`%TLS_JA4_FINGERPRINT%`), CloudFront (`CloudFront-Viewer-JA4-Fingerprint`), Cloudflare Bot Management (`cf.bot_management.ja4`) |
| The api reads it, only through trusted proxies | security-kit's `fastifySecurity({ trustedHops, tlsFingerprintHeader })` → `request.security.tlsFingerprint` (`src/server.ts`), then `ctx.tlsFingerprint` |
| login and `refreshSession` pass it to auth-kit | `src/controllers/auth.controller.ts` |
| Stored as a SHA-256 hash on the session | auth-kit `Session.tlsFingerprintHash` |
| On refresh, a different one is audited | `auth.tls_fingerprint_changed` (both hashes); `auth.device_mismatch` records `tls: same / changed / null` |

The header has the same trust model as X-Forwarded-For: it is ignored unless `TRUST_PROXY` ≥ 1 (the api
refuses to start with `TLS_FINGERPRINT_HEADER` but no trusted proxy), and the proxy must overwrite any
value a client sent. A missing or malformed fingerprint is never an error. A change never ends the
session, whatever `DEVICE_BINDING` says: browser updates and TLS-inspecting networks change it too. It is
not in the GraphQL schema; clients send nothing new.

### Password reset

`requestPasswordReset(email, orgCode?)` always returns `true`, whether or not the account exists, and
Expand Down
1 change: 1 addition & 0 deletions docs/09-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ variables already set in the real environment win over the file.
| `INTROSPECTION` | on in development | Off adds `NoSchemaIntrospectionCustomRule` |
| `BODY_LIMIT_BYTES` | `2000000` | Request body limit (bank CSV imports count) |
| `TRUST_PROXY` | `false` | How many proxies in front of the API append to `X-Forwarded-For`: `true` = 1 (a load balancer), `2` for CDN + load balancer. Login limits and sessions then see the client IP; entries left of the trusted proxies (which any client can send) are ignored |
| `TLS_FINGERPRINT_HEADER` | empty (off) | Header the TLS-terminating proxy sets to the client's JA4 (or JA3) TLS fingerprint, e.g. `x-ja4`. Read only through `TRUST_PROXY` proxies (refused at startup without one); stored hashed on the session, a change audited ([06-auth-and-permissions.md](06-auth-and-permissions.md#tls-fingerprint)) |
| `MGPL_SAMPLES` | on in development | Demo report/invoice samples in `documents` |
| `SESSION_IDLE_MS` | `43200000` (12 h) | |
| `SESSION_MAX_AGE_MS` | `2592000000` (30 days) | |
Expand Down
1 change: 1 addition & 0 deletions docs/12-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ report-kit's resvg is a native module: build the image on the same platform it r
| ☐ | `CORS_ORIGINS` lists exactly the app origins (no `*`) |
| ☐ | `GRAPHIQL` and `INTROSPECTION` off (the defaults in production) |
| ☐ | `TRUST_PROXY=true` behind one load balancer, `TRUST_PROXY=2` behind a CDN plus a load balancer (login limits use the client IP) |
| ☐ | Optional: the proxy sets `x-ja4` to the client's JA4 TLS fingerprint and `TLS_FINGERPRINT_HEADER=x-ja4` ([06-auth-and-permissions.md](06-auth-and-permissions.md#tls-fingerprint)) |
| ☐ | `MGPL_SAMPLES` off (the default in production) |
| ☐ | Signature files mounted read-only; `MGPL_SIGNATURE_*_FILE` point at them |
| ☐ | `PASSWORD_RESET_URL` points at the app's reset page, and a mailer is wired (below) |
Expand Down
2 changes: 1 addition & 1 deletion docs/LOCAL-AGENT-BRIEF.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Done when: no repo imports `@mgpl-diagnostics/db-kit/graphql` (`gh search code "

Follow [12-deployment.md](12-deployment.md). In particular:

- `REDIS_URL` for more than one server; `CORS_ORIGINS` set to the app origins; `TRUST_PROXY=true`.
- `REDIS_URL` for more than one server; `CORS_ORIGINS` set to the app origins; `TRUST_PROXY=true`; optionally `TLS_FINGERPRINT_HEADER=x-ja4` with a proxy that sets it (docs/06).
- Real signature images in a secret volume, `MGPL_SIGNATURE_HEADER_FILE` / `MGPL_SIGNATURE_NONHEADER_FILE`.
Never commit them.
- `JWT_SECRET` from your secret store (≥ 32 random characters: `openssl rand -base64 48`), the same on every API server and worker.
Expand Down
4 changes: 2 additions & 2 deletions kits.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"kits": [
{
"name": "security-kit",
"ref": "v0.2.0"
"ref": "v0.3.0"
},
{
"name": "mail-kit",
Expand All @@ -15,7 +15,7 @@
},
{
"name": "auth-kit",
"ref": "v1.2.0"
"ref": "v1.3.0"
},
{
"name": "db-kit",
Expand Down
Loading
Loading