Files
Inboxintel/SECURITY.md
cesnimda 2b19bddf7b
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 11s
Deploy Staging / deploy (push) Successful in 24s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 52s
CI / backend (pull_request) Successful in 49s
CI / frontend (pull_request) Successful in 10s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 55s
feat(security): audit batch C — data posture, retention, DP keys (#22)
2026-07-02 10:13:07 +02:00

4.3 KiB

InboxIntel — Security & Data Posture

The deliberate security posture of this application, so operators know exactly what is and isn't protected. Complements AUDIT_REPORT.md (point-in-time audit) and docs/discovery/multi-provider/06-security-model.md (future multi-user design).

Deployment model this posture assumes

Self-hosted, single-operator instance: the person running the server is the person whose mailbox is synced. All services (API, Postgres, frontend) run in Docker on the operator's own machine; Postgres and the API bind to loopback/compose-internal only; TLS terminates at the reverse proxy.

What is protected, and how

Asset Protection
Google OAuth refresh/access tokens Encrypted at rest (ASP.NET Data Protection, AES); never logged; never sent to the browser
Data Protection key ring Optionally encrypted with an operator-supplied X.509 certificate — set DataProtection:CertificatePath/CertificatePassword. Without it, keys sit in plaintext on the /keys volume and anyone with volume access can decrypt stored tokens. Recommended for any shared host.
App session HttpOnly/SameSite=Lax/Secure cookie · sliding 7 d with an absolute 30 d cap (Auth:AbsoluteSessionDays)
Login/abuse Rate limiting: global 300 req/min per user (or IP when anonymous); auth 10/min; export/unsubscribe/AI 20/min (RateLimiting:*)
Cross-user access EF global query filters on every tenant-scoped entity (incl. the EmailLabel join) — tested
Outbound fetches (unsubscribe etc.) SafeHttpGuard SSRF allowlisting (DNS-rebinding-safe) + redirects disabled
Untrusted email content in the UI Rendered only as escaped React text (no dangerouslySetInnerHTML); search highlights use non-HTML sentinels; SPA ships CSP with script-src 'self'
Secrets Never committed (deploy/.env* git-ignored; pre-commit + CI gitleaks scans); no passwords stored at all (OAuth-only login)

What is deliberately NOT protected (accepted risks — read this)

  1. Email bodies are stored in plaintext in Postgres. Full-text and semantic search index the body; encrypted columns cannot be indexed this way. On the assumed single-operator deployment, the database lives on the operator's own disk, so the threat this would mitigate (a third party reading the DB files) reduces to "someone with access to your machine" — mitigate it at the layer that actually works:
    • Use full-disk or volume encryption on the host (BitLocker/LUKS) — strongly recommended.
    • Encrypt backups of the pgdata volume the same way.
    • Before any multi-user deployment, revisit per the multi-provider security design (host admins must not be able to read members' mail — plaintext bodies break that promise).
  2. DB connection is not TLS — Postgres is only reachable on the compose-internal network / loopback. If you ever move Postgres to another host, add SSL Mode=Require to the connection string (audit L-4).
  3. No CSRF tokens — SameSite=Lax cookies + strict CORS + JSON-only bodies make classic CSRF impractical; revisit if either changes (audit L-1).

Data retention (opt-in)

By default the local mailbox copy is kept indefinitely. Two knobs enable automatic purging of the local copy only (your actual Gmail is never touched):

"DataRetention": {
  "PurgeTrashedAfterDays": 0,   // e.g. 30 — purge local copies of trashed mail after 30 days
  "PurgeAllAfterDays": 0        // e.g. 730 — keep at most ~2 years of mail locally
}

0 disables a knob. A daily background worker applies them. For a full "delete my data" operation: stop the stack and remove the pgdata + keys volumes (docker compose down -v), and revoke the app's access in your Google account.

Production checklist (beyond compose defaults)

  • Set AllowedHosts to your real hostname(s) (audit L-2).
  • Terminate TLS at the proxy; HSTS is enabled automatically outside Development.
  • Provide DataProtection:CertificatePath to encrypt the key ring.
  • Keep deploy/.env readable only by the service user; rotate the DB password if exposed.
  • Dependency + secret scanning run in CI on every PR (required checks).

Reporting

Single-operator project — if you find a vulnerability, open a private issue or contact the repository owner directly.