Files
Inboxintel/SECURITY.md
T
cesnimda 8bab8b54de
CI / backend (pull_request) Successful in 51s
CI / frontend (pull_request) Successful in 11s
Security / secrets (pull_request) Successful in 4s
Security / dependencies (pull_request) Successful in 53s
feat(security): audit batch C — data posture doc, retention, DP key protection
Implements AUDIT_REPORT.md H-3 (doc + retention), M-3, L-2/L-4 guidance:
- SECURITY.md: the deliberate data-at-rest posture (plaintext bodies + why, volume
  encryption + backup guidance, DB TLS note, AllowedHosts note, production checklist,
  delete-my-data procedure).
- Opt-in local data retention: DataRetention:{PurgeTrashedAfterDays,PurgeAllAfterDays}
  (0 = disabled, no behaviour change) + daily RetentionWorker purging only the LOCAL
  copy (Gmail untouched). 3 tests lock disabled-is-noop and purge-scoping.
- M-3: optional X.509 protection for the Data Protection key ring
  (DataProtection:CertificatePath/Password) so keys are no longer necessarily
  plaintext next to the ciphertext they protect.

Full suite: 51/51 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 03:32:09 +02:00

64 lines
4.3 KiB
Markdown

# 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](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):
```json
"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.