8bab8b54de
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>
64 lines
4.3 KiB
Markdown
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.
|