# Security & Privacy

This document describes what data the **finances** image touches, where it
sends it, and what guarantees the architecture provides. Everything here is
verifiable against the source (and CI-enforced — see
`scripts/check-egress-allowlist.ts`).

## The model

Your tenant runs in **your own Railway account** (or any Docker host). The
operator (the person who ships the image) has:

- **No credentials** to your Postgres database
- **No SSH access** to your container
- **No API token** that bypasses your authentication
- **No telemetry channel** receiving data from your instance

The image is pulled from `ghcr.io/timwernerdxb/finances`. You can pin to a
specific version (`:v1.2.3`) and audit the source for that tag before
allowing your Railway service to update.

## Outbound network destinations

The image makes outbound HTTP requests to **only** these hosts. Every
non-public destination requires you to provide your own API key —
the image makes zero requests to anything you didn't enable.

CI fails the build (`scripts/check-egress-allowlist.ts`) if any new host
appears in source without an allowlist entry + an update to this file.

### Bank aggregators (your keys)

| Host | Used for | Gated by |
|------|----------|----------|
| `api.pluggy.ai` | Brazilian bank feeds | `PLUGGY_CLIENT_ID` + `PLUGGY_CLIENT_SECRET` |
| `api.teller.io` | US bank feeds | `TELLER_APP_ID` + `TELLER_CERT` + `TELLER_KEY` |
| `cdn.teller.io` | Teller Connect widget JS (browser-side) | (loaded only when /accounts page rendered) |
| `api.enablebanking.com` | EU PSD2 bank feeds | `ENABLE_BANKING_APP_ID` + key files |
| `enablebanking.com` | OAuth redirect domain | (auth flow only) |
| `www.lunchflow.app` | Lunchflow bank feeds | `LUNCHFLOW_API_KEY` |
| `www.saltedge.com` | SaltEdge bank feeds (legacy, optional) | `SALTEDGE_*` keys |

### LLM (your key)

| Host | Used for | Gated by |
|------|----------|----------|
| `api.anthropic.com` | Inbox email parsing (extracts trades, holdings, distributions from broker emails) | `ANTHROPIC_API_KEY` |

If you don't set the key, the inbox parser is disabled — emails still arrive
but stay unparsed in the review queue.

### Email transport (your key)

| Host | Used for | Gated by |
|------|----------|----------|
| `api.resend.com` | Transactional email (reminders, reports) | `RESEND_API_KEY` |
| Your SMTP host | Fallback for email | `SMTP_HOST` + `SMTP_USER` + `SMTP_PASS` |
| Your WhatsApp bot URL | Optional WhatsApp notifications + Outlook fallback | `WHATSAPP_BOT_URL` + `WHATSAPP_BOT_TOKEN` |

### Public market data (no credentials, no PII)

| Host | Used for | What's sent |
|------|----------|-------------|
| `query1.finance.yahoo.com` | Equity/commodity price quotes | Ticker symbol only |
| `query2.finance.yahoo.com` | Ticker search | Search string only |
| `open.er-api.com` | FX rates | None — public endpoint |

### Browser-rendered fonts

| Host | Used for |
|------|----------|
| `fonts.googleapis.com` | Inter/system font CSS in HTML pages |

This is loaded by the user's browser, not the server. Self-host the font
file if you want to remove this dependency.

## What we explicitly do NOT do

- **No telemetry.** The image sends nothing to any operator-controlled
  domain. Search the source for `paired.net` — only matches are the
  WebAuthn RP ID default (a string passed to the browser, not fetched)
  and an MCP test client default (which you override with `FINANCE_API_URL`).
- **No anonymous analytics.** No Plausible, no PostHog, no GA, nothing.
- **No error reporting.** No Sentry, no Bugsnag. Errors stay in your
  Railway logs.
- **No phone-home update check.** Updates happen via your Railway
  service polling the public Docker registry — not by our server
  pushing to yours.

## Verifying the guarantees yourself

### Run the egress check on your own clone

```bash
git clone <your fork>
cd finances
npx tsx scripts/check-egress-allowlist.ts
```

Output: every host found in source. Cross-reference against the table
above. If anything new appears, the test fails the build.

### Block our domain in your firewall

Railway lets you add an outbound network policy. Block any operator
domain (e.g. `paired.net`) — the app continues to function. That's
empirical proof, not a promise.

### Pin a specific image and audit it

Instead of `:stable`, set your Railway service to `:v1.2.3`. The image
digest is frozen forever. Read the source for tag `v1.2.3` (in the
audit-mirror repo, or your own fork). Verify the running image's digest
matches what GitHub Actions produced for that commit.

When you want to update, read the diff first, then bump the tag.

### Verify image provenance with cosign

Once cosign signing is wired into the GitHub Actions release workflow:

```bash
cosign verify ghcr.io/timwernerdxb/finances:v1.2.3 \
  --certificate-identity-regexp '.*timwernerdxb.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
```

This proves the image came from your repo's GitHub Actions on a
specific commit, not a tampered build.

## Credentials you control

Every secret the app needs is set as an env var in your own Railway
project — never in the operator's account:

- `API_TOKEN` — your tenant's API auth token (auto-generated at deploy time)
- `DATABASE_URL` — your Postgres (in your Railway VPC)
- `WEB_USERNAME` + `WEB_PASSWORD_HASH` — bootstrap admin login
- `BOOTSTRAP_MANAGER_EMAIL` — first manager user
- All bank aggregator keys (Plaid / Teller / Pluggy / EnableBanking / Lunchflow)
- `ANTHROPIC_API_KEY` — for inbox parsing (optional)
- `RESEND_API_KEY`, `SMTP_*`, `WHATSAPP_*` — for outbound notifications (optional)
- `WEBAUTHN_RP_ID` + `WEBAUTHN_ORIGIN` — your domain for passkey 2FA
- `LOGIN_ALLOWED_IPS` — optional IP allowlist for login
- `OPENAI_API_KEY` — *not used* (Anthropic only)

## Reporting a vulnerability

Found a bug that breaches any of the guarantees above? Email the operator
directly — please don't open a public issue. We'll fix it, ship a
patched image, and document the fix.
