You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Security: 0xdevabir/FraudLens
Security
docs/SECURITY.md
Security — threat model, personal data and retention
What FraudLens protects, from whom, and how far each protection goes. Each
threat is marked done (built and tested), partial (built, with a gap
named) or not done. The controls themselves are described in
PLATFORM.md §7 and INGEST.md.
1. Assets
Asset
Where
Why it matters
Transactions and decisions
Postgres transactions, decisions
Customers' payments; a changed decision moves or holds money
Wallet state (frozen, flagged)
Postgres wallets, freeze_requests, wallet_flags
A freeze stops a customer's money; an unfreeze releases a mule's
Cases, notes and verdicts
Postgres cases, case_events
Evidence, and the labels the next model is trained on
Customer scam reports
Postgres customer_reports
Free text a customer wrote, possibly with names and numbers
Audit log
Postgres audit_log (append-only)
The record of who saw and did what
Token signing keys
FRAUDLENS_JWT_SECRET or the JWT keyring
Whoever has one can be any user
Partner HMAC keys
the ingest keyring
Whoever has one can inject transactions as that bank
Internet to the edge. Browsers and partners reach Caddy over TLS
(make tls). Caddy replaces any X-Forwarded-For a client sends.
Edge to the API. A private Docker network; the API believes forwarded
addresses only from FRAUDLENS_TRUSTED_PROXIES.
Caller to the API. Every request is authenticated: a bearer token with a
role, or a partner's HMAC signature on the ingest endpoints. Nothing else.
API to its stores. Postgres and Redis on the private network, with
passwords; their ports are bound to 127.0.0.1 on the host.
API outwards. Decision callbacks to partner URLs set by an operator, and
(off by default) case-note wording from a language model.
3. Threats (STRIDE)
Spoofing
Threat
Mitigation
Status
Guessing an analyst's password
Argon2id; five failures in five minutes lock the username from that address; a per-address limit
done
A forged or altered token
HS256 with a key of at least 32 characters (production guard); kid must name a known, unretired key
done
A stolen token
8-hour expiry, sign-out revokes it in Redis (fails closed), the user is re-read on every request
partial: no binding to a device, no refresh-token rotation
A forged ingest request
HMAC-SHA256 over timestamp, nonce, method, path and body; constant-time comparison
done (test_ingest.py)
Someone posing as FraudLens to a partner's callback endpoint
Callbacks are signed with the partner's own key, verified by the SDK
done
Spoofing the client address to dodge the lockout
X-Forwarded-For is read only from configured proxies, right to left; 0.0.0.0/0 is refused
done (test_security.py)
The demo sign-in left on
Off by default, refused in production, every use audited
done
Tampering
Threat
Mitigation
Status
A request changed in transit
TLS at the edge; ingest bodies are signed whatever the transport
partial: TLS ends at the proxy, the private hops are plain
Rewriting the audit trail
A trigger refuses UPDATE, DELETE, TRUNCATE on audit_log
partial: a database superuser can drop the trigger; no external copy
Changing a decision or a freeze
Roles; freezes need a second person (§5 of PLATFORM.md); every step audited
done
Injection through input
Every body validated, unknown fields refused, identifiers match a pattern, SQL parameterised, 1 MB body cap (2 MB at the proxy)
done
Poisoning the models through analyst verdicts
Retraining registers a challenger only; promotion is a separate, manual step with a shadow comparison
partial: no automatic check for a run of suspicious verdicts
Swapping the model or policy files
Loaded once at start-up from the artifacts directory and the image
not done: no signature on artifacts
Repudiation
Threat
Mitigation
Status
"I never froze that wallet"
Every case action, freeze step, flag, login and profile view is in the append-only audit log with the user and request id
done
"I never looked at that number"
A full identifier is shown only after POST /audit/reveals
partial: the API returns full numbers to reviewers (PLATFORM.md §10)
"The bank never sent that payment"
Only a holder of the partner's key could have signed it
partial: the transaction is stored, but not the key id, MsgId or signature it came with
Information disclosure
Threat
Mitigation
Status
Reading data with too broad a role
admin sees metrics and audit, not customer data; service scores but cannot read alerts or cases
done
Customer IP addresses
Reduced to the /24 or /48 at the edge; the address is never stored, logged or returned
done
Numbers and e-mails in scam reports
Masked on the way out
partial: pattern-based, names in words get through
Scam messages a customer checks
Classified and discarded; not stored
done
Evidence sent to a language model
Off by default; identifiers masked before it leaves
done
Error messages revealing internals
One error shape; validation errors never echo the value; signature refusals never show the expected value
done
Secrets in the repository or the image
.env and backend/secrets/ ignored by git; *_FILE variables read Docker/Kubernetes secrets; keyrings written with mode 0600
done
Secrets in the environment of the process
*_FILE and a provider hook avoid it
partial: only a directory provider ships, not a vault client
Reasons for a decision leaking to a fraudster
/v1/score and callbacks return the action, not the reasons
done
A captured backup
—
not done: no encryption at rest beyond the disk's own
Denial of service
Threat
Mitigation
Status
Flooding the login or customer endpoints
Per-address and per-wallet rate limits in Redis
done
Flooding the stream
503 backlog_full above the backlog limit; batches capped at 500
done
Replaying a signed ingest request
Nonce kept in Redis for twice the ±5 minute window; TxId deduplicated for 7 days
done
Large bodies
2 MB at the proxy, 1 MB in the API
done
A partner's slow or dead callback endpoint
5 s timeout, backoff, 8 attempts, then a dead list; delivery is off the request path
done
Redis down
Token checks fail closed (503); scoring needs it for the stream
partial: no Redis replica
One scorer process
—
see PLATFORM.md §10
Elevation of privilege
Threat
Mitigation
Status
A service account reading cases
A role check on every route
done
An analyst approving their own freeze
Two-person rule in the database workflow
done
A partner key used for other partners or the console
Partner keys are accepted only on /v1/ingest, and JWT keys only for tokens
done
A leaked key staying valid
keys retire cuts one off at once; rotation keeps old keys only for a grace period
partial: rotation is manual, not scheduled
Server-side request forgery via callbacks
Callback URLs are set by operators in the keyring, https only in production, redirects not followed
partial: no check of where the name resolves
4. Personal data
Data
Kept
Shown to
Wallet, agent and device numbers
In full, in transactions and cases
Analysts and supervisors (masked on screen until a recorded reveal)
Amounts, times, districts, channels
In full
Analysts and supervisors
Customer IP address
Only its network (/24, /48)
Nobody: used by the policy only
Scam-report text
As written
Reviewers, with numbers and e-mails masked
Scam messages checked by a customer
Not kept
—
Names
Not collected
—
Analysts' accounts
Username, role, Argon2id hash
Admins
Nothing personal goes to the language model unmasked, and nothing goes to it at
all unless it is switched on.
5. Retention
The policy below is what FraudLens is designed for. Enforcing it is not built:
there is no scheduled job that deletes or anonymises rows. The Redis entries are
the exception: they expire by themselves.
Data
Kept for
Then
Transactions and decisions
5 years (the Money Laundering Prevention Act 2012 asks for at least 5 years of transaction records)
Wallet and device numbers replaced by a keyed hash; amounts and times kept for models
Cases, case events, verdicts, freeze requests
5 years after the case is closed
As above
Scam reports
2 years
Text deleted, category kept
Audit log
5 years
Exported and archived, then deleted
Redis: ingest nonces
10 minutes
Expire
Redis: idempotency answers
24 hours
Expire
Redis: seen TxIds
7 days
Expire
Redis: revoked tokens
Until the token would have expired
Expire
Redis: callback dead list
Last 10,000
Trimmed
Model training data
The synthetic dataset; no real customer data is used
—
A customer who asks for their data gets what is held under their wallet number;
deleting it before the period ends is refused where the law requires keeping it.
6. Not done, in order of what to build next
TLS on the private hops (API to Postgres and Redis), or a service mesh.
A secrets-manager client behind the provider hook, and scheduled rotation.
A retention job implementing §5.
Signed model and policy artifacts, checked at start-up.
The audit log shipped to write-once storage outside the database.