Home Getting Started Security and threat model

Security and threat model

Last updated on May 16, 2026

Security and threat model

What we trust, and what we cannot. A plain-English read of the OrangeRails security model — what's in our threat model, what's out, and what assumptions a customer has to accept to use the service.


The one-sentence summary

OrangeRails holds ciphertext only. We cannot read your financial data, by the architecture, not by promise. A breach of our database leaks opaque bytes, not bank logins or transactions.


What we protect against

Threat How we handle it
Server-side breach (read). An attacker copies our database. All credentials, transaction memos, counterparty handles, and (on Stealth Sync sources) addresses + amounts are AES-256-GCM encrypted in the user's browser before they reach us. Server-side decryption is impossible without the user's vault password.
Operator insider abuse. An OrangeRails employee tries to peek at user data. Same answer. We can't decrypt either. Operator access to the production database doesn't yield plaintext.
Network MITM. An attacker on the path between user and server. All traffic is HTTPS with TLS 1.2+ over HTTP/2 or HTTP/3, certs via Let's Encrypt. The Connect widget verifies the parent window's origin before accepting an INIT message.
Compromised upstream source. Coinbase, Blink, etc., gets breached. The credential we hold for that source is encrypted at rest, so the attacker can't pivot from our DB to other accounts. But we can't undo the upstream breach itself.
Replay attacks. An old credential blob is replayed against our edge function. Credentials are bound to a single user + subaccount via RLS. Replay against a different subaccount fails.
Plaid-style data monetization. Any business model that requires us to read your data. Structurally impossible. No analytics on transaction contents. No "anonymized" dataset to sell. The only thing we know about a user is: when they synced, which provider, how many transactions (count only).

What we don't protect against

Threat Why it's out of scope
Compromised user device. Malware on your machine reads the vault password. If you lose your machine, you lose. We can't help with endpoint security.
Compromised vault password. Phished, brute-forced, or shoulder-surfed. Same. Recovery via Shamir 2-of-3 (Phase 1.5a) limits blast radius but doesn't prevent it.
Upstream sources collecting their own data. Plaid, Coinbase, Strike see what their own APIs return. Not our system; we never see what they see, but we also can't stop them from seeing it.
Statistical analysis of timing or metadata. "User synced 3 times today" leaks behavioral signal. We log minimum metadata for billing + ops. Documented in the privacy policy.
Quantum attacks on stored ciphertext. Long-term threat. PR #20 shipped hybrid X25519 + ML-KEM-768 + ML-DSA-65 for the keys that matter. Per-record AES-256-GCM is still classical; post-quantum work continues in Phase 4.

Cryptographic primitives

Layer Primitive
Vault password derivation Argon2id (parameters: 64 MiB memory, 3 iterations, 4 parallelism — tuned to ~1s on a 2026 laptop)
Master encryption key wrap Hybrid X25519 + ML-KEM-768
Per-record encryption AES-256-GCM with a fresh 12-byte IV per record
Co-admin signatures ML-DSA-65
Transport TLS 1.2+ via Cloudflare Pages

Key files: src/lib/vault.ts (KDF + AES wrappers), src/lib/crypto-fields.ts (per-table helpers).


What's in the database, by table

Table Plaintext columns Ciphertext columns
users id, email (auth.users)
subaccounts id, platform_id, external_user_id, created_at
connections id, subaccount_id, provider_type, created_at encrypted_credentials, encrypted_label
transactions id, connection_id, external_id, timestamp, direction, type, status encrypted_payload, encrypted_counterparty, encrypted_description
source_wallets id, connection_id encrypted_label
customer_vault_meta customer_id, salt, KDF params, KEM/sig public keys vault_verifier_ciphertext, enc_mek_ciphertext, kem_secret_wrapped, sig_secret_wrapped
customer_recovery_shares customer_id, share_index, threshold metadata share_ciphertext

Operators can see provider types, counts, and timestamps. They cannot see amounts, counterparties, descriptions, or credentials.


Stealth Sync (on-chain Bitcoin) specifics

Stealth Sync adds an additional layer for xpub-watch sources:

  • The xpub itself never leaves the browser. It's sealed before storage.
  • Address derivation happens in WASM inside the browser.
  • Block filter matching (BIP 158) also happens in the browser; filter files come from a public CDN with no auth and no logs.
  • Block bodies come from our own Bitcoin Core node, not a third party.
  • Each transaction OR persists is a sealed envelope; the server cannot tell which addresses appeared in which transactions.

See Stealth Sync — Architecture for the full deep dive.


Bug bounty + disclosure

  • Security email: [email protected]
  • PGP key: published at https://orangerails.com/.well-known/security.txt (Phase 1)
  • Coordinated disclosure: 90 days. We acknowledge within 48 hours, ship a fix or mitigation within 30 days for high-severity, 90 days otherwise.
  • Bug bounty program: planned for Phase 2 (β). HackerOne or Bugcrowd, $5,000/quarter initial budget.

Don't open public GitHub issues for security vulnerabilities. Email first.


Independent reviews

  • Cryptographic implementation review — planned for Phase 2 (β). Target firms: Trail of Bits, NCC Group, Cure53. Budget $30k–$80k.
  • SOC 2 Type 1 — planned for Phase 3 (1.0).
  • SOC 2 Type 2 — planned post-1.0.

The point of these reviews is that you don't have to trust our blog post — you get a third-party attestation. Reports will be published with findings + remediations.


What customers should do

  1. Pick a strong vault password. It's the root of the trust hierarchy. We cannot reset it.
  2. Save the Shamir recovery share (PDF/QR) we generate at vault setup. If you lose your password and don't have the recovery share, your encrypted data is gone.
  3. Keep your upstream credentials scoped read-only. Strike, Coinbase, etc. let you generate API keys with read-only scopes. Use them. OR doesn't need write access to anything.
  4. Self-host if you can't trust the operator. Apache 2.0, run it yourself, eliminate even the residual ops trust.