Adds a compose 'backup' sidecar: daily pg_dump of the inboxintel DB into ./backups (git-ignored), atomic write (.tmp -> rename), rotation after BACKUP_KEEP_DAYS (default 7). Previously there were NO backups — a bad migration or volume loss meant total data loss. Restore procedure documented in compose + SECURITY.md. Verified: compose config parses; a live pg_dump against the staging DB produced a valid dump over the compose network with the same image/credentials the sidecar uses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.5 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)
- 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: nightly
pg_dumprotation runs via the composebackupservice into./backups/(git-ignored) — keep that directory on an encrypted disk and copy it off-machine. Restore:docker compose exec -T postgres psql -U inboxintel -d inboxintel < backups/<file>.sql. - 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).
- 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=Requireto the connection string (audit L-4). - 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
AllowedHoststo your real hostname(s) (audit L-2). - Terminate TLS at the proxy; HSTS is enabled automatically outside Development.
- Provide
DataProtection:CertificatePathto encrypt the key ring. - Keep
deploy/.envreadable 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.