siftlode/docs/deployment-plan.md

210 lines
10 KiB
Markdown
Raw Normal View History

# Siftlode — Public Deployment Plan (v1)
Status: **approved (planning)** — 2026-06-13. Goal: share the current Siftlode
build with one trusted tester (both users in Hungary), on safe public
infrastructure, with a clean and trustworthy Google sign-in experience.
This is a planning document. No implementation is started until each phase is
explicitly greenlit.
---
## 1. Confirmed decisions
| Topic | Decision |
|---|---|
| Google verification level | **Production (unverified)** — not full verification for now |
| Sign-in / scopes | **Incremental authorization via an onboarding wizard** (see §2) |
| Hosting | **All on the asgard VPS** (UI + backend + DB) |
| Cloudflare in front | **No** for now (invite-only, unadvertised URL → tiny attack surface) |
| Repo visibility to tester | **Private repo + read access** for the friend |
| Security review timing | **Hybrid**: baseline audit early (folded into Phase A), focused review before publish |
### Why not full verification (yet)
The "Google hasn't verified this app" warning is removed **only** by full
verification (homepage on a verified domain, privacy policy + ToS, app logo,
demo video, scope justification, ~10-day review). For a 2-person test that is
overkill. **Production-unverified** keeps a single one-time click-through
warning per user but removes the painful Testing-mode behaviour (7-day refresh
token expiry → weekly re-login). Full verification stays as an optional later
phase (§6).
Sources: Google "Sensitive scope verification", "Verification requirements"
(support 13464321), "Manage App Audience" (15549945, the 7-day rule),
"Unverified apps" (7454865).
---
## 2. Onboarding wizard — incremental scopes (core design)
**Key insight:** `openid email profile` are **non-sensitive** scopes. An app
that requests only these shows **no unverified warning at all**, and the grant
**does not expire after 7 days even in Testing mode**. The sensitive
`youtube.readonly` (and the optional `youtube` write) are what trigger the
warning.
Therefore:
1. **Base login** requests only `openid email profile` → user signs in with a
clean, familiar Google screen, no warning, lands in the app.
2. **Onboarding wizard** (first login, also reachable later from settings)
walks the user through *optional, explained, one-at-a-time* grants:
- **Step — read access (`youtube.readonly`)**: explains it is needed to read
your subscriptions and build the feed; pre-warns about the Google
"unverified app" screen and how to proceed (Advanced → Continue). Button →
incremental consent. Skippable.
- **Step — write access (`youtube`)**: optional; needed to unsubscribe from
within Siftlode; same pre-warning. Button → incremental consent. Skippable.
3. App **degrades gracefully**: no read scope → no feed (UI explains why); no
write scope → no unsubscribe button.
**Why this design:** the tester consciously grants each permission, with a
rationale, and can revoke on Google's side anytime — the cleanest answer to "I
don't fully trust an AI-written app." It is also Google's recommended
"incremental authorization" / "narrowest scope" best practice, which makes a
future verification submission cleaner.
### Code implications (Phase A)
- `backend/app/auth.py`: split current `READ_SCOPES` into `BASE_SCOPES`
(`openid email profile`, used at `/auth/login`) and the YouTube scopes
(granted via incremental upgrade). Reuse/extend the existing `/auth/upgrade`.
- Add `has_read_scope()` alongside `has_write_scope()`.
- Scope-gate the feed/import endpoints; expose `/api/me/scopes` so the frontend
knows when to show the wizard / which cards to render.
- Frontend: wizard UI + explanatory copy + pre-warning about the Google screen.
---
## 3. Infrastructure — all on asgard
```
User browser ──HTTPS──> Caddy (asgard :443, siftlode.b1fr0st.eu)
│ reverse_proxy 127.0.0.1:8080
siftlode-api (FastAPI + built SPA, Docker)
│ internal docker network only
siftlode-db (Postgres 16 — never published)
```
- **DNS**: `porkbun add b1fr0st.eu A siftlode 88.218.78.254` (+ AAAA). Agent-owned
via `/usr/local/bin/porkbun` on the PVE host.
- **TLS**: Caddy DNS-01 (porkbun plugin), same as forge/pages — auto cert for
`siftlode.b1fr0st.eu`.
- **Caddy**: new vhost block `siftlode.b1fr0st.eu { reverse_proxy 127.0.0.1:8080 }`
inheriting the existing security-header set (HSTS, X-Frame-Options, etc.).
- **OAuth redirect URI**: `https://siftlode.b1fr0st.eu/auth/callback`
`OAUTH_REDIRECT_URL`. Registered in the Google OAuth client.
- **Stack**: dedicated prod compose at `/srv/siftlode/`; app bound to
**127.0.0.1:8080 only** (behind Caddy, never a public port); Postgres on the
internal docker network only (no host port).
### Resource limits (critical on the 3.3 GB box)
- `siftlode-api`: `mem_limit: 768m`, `cpus: 1.0`, `pids_limit: 256`,
`no-new-privileges`, `cap_drop: ALL`, read-only rootfs + tmpfs `/tmp`,
non-root user.
- `siftlode-db`: `mem_limit: 512m`, `cpus: 0.75`, tuned
`shared_buffers`/`work_mem`.
---
## 4. Security — layered defence
Baseline already in place on asgard (from infra notes): UFW (80/443/2222/222
only), fail2ban (sshd + caddy + forgejo, with home-IP DDNS auto-whitelist),
sysctl hardening, Docker daemon hardening (no-new-privileges, userland-proxy
off, icc off, log caps), auditd, key-only SSH, swap.
| Threat | Mitigation |
|---|---|
| **Bot registration spam** | Strongest control already built: invite/allowlist-only. Set `ALLOWED_EMAILS` to exactly the two emails → no open registration; double gate (Google login + email allowlist). Do **not** open registration during the test. |
| **App breach / OAuth token theft** | Tokens already encrypted at rest (Fernet). Strong prod `SECRET_KEY` / `TOKEN_ENCRYPTION_KEY`. Verify session cookie flags (Secure/HttpOnly/SameSite); audit OAuth `state`/CSRF; check SSRF in the YouTube client. |
| **Server kill (resource exhaustion / OOM)** | Container hard-limits above; Caddy per-IP rate limit on `/auth/*` and API; Postgres connection cap. |
| **DDoS** | Honest limit: a small VPS can't absorb a large volumetric DDoS. Realistic layers: Caddy rate-limit + fail2ban on Caddy access log (present). Cloudflare free declined for now (re-evaluate if attacked). |
| **AI-written code flaws** | Dedicated security review of the codebase (§5). |
- **Backups**: extend the existing `asgard-snapshot` timer with a `pg_dump` of
the siftlode DB (rides the nightly tar.zst; pulled to the home array by
`pull-asgard-backups`).
- **Monitoring**: Prometheus blackbox probe for `siftlode.b1fr0st.eu` +
container-up alert, mirroring the Forgejo probes.
---
## 5. Trust-building for the tester
- Minimal default scope (the wizard) → zero YouTube access and zero warning at
sign-in.
- Private repo **read access** for the friend → he can inspect exactly which
scopes are requested and what the app does with tokens.
- Permissions granted voluntarily, step by step, with rationale; revocable on
Google's side anytime (the wizard says so).
- Optional: self-host — the app is fully Dockerized; the friend could run his
own instance (ultimate trust option; offered, not required).
---
## 6. Deploy pipeline (Forgejo Actions, near one-click)
The runner already runs **on the same asgard host** as the app:
1. Push to a **private Forgejo repo** at `forge.b1fr0st.eu` (Siftlode is
currently local-git only).
2. **Actions workflow** on tag push: multi-stage Docker build → push to the
built-in registry (`forge.b1fr0st.eu/peter/siftlode:<tag>`).
3. **Deploy step** in the same workflow: `cd /srv/siftlode && docker compose pull
&& docker compose up -d` (local, since the runner is on asgard); migrations
run via the entrypoint's `alembic upgrade head`.
Result: `git tag vX && git push --tags` → live in minutes.
---
## 7. Work packages (phased)
**Phase 0 — baseline security audit (do early; fold fixes into Phase A)**
- OAuth flow, token storage, session cookies, secret handling, dependency audit
(`pip-audit` + `npm audit`), Caddy headers. Since Phase A refactors auth,
findings here are fixed during that refactor (avoids re-auditing churned code).
**Phase A — required for sharing**
1. Onboarding wizard + scope split (§2): `auth.py` base/incremental scopes,
`has_read_scope()`, scope-gated endpoints, `/api/me/scopes`, wizard UI + copy.
2. Public **landing/about**, **Privacy Policy**, **Terms of Service** pages
(reachable without login). Privacy Policy references the YouTube ToS
(https://www.youtube.com/t/terms) and Google Privacy Policy, includes the
Limited Use disclosure.
3. Prod config: strong secrets, `OAUTH_REDIRECT_URL`, `ALLOWED_EMAILS` = the two
emails, set `YOUTUBE_API_KEY` (decouples backfill from user tokens),
`FRONTEND_ORIGIN` empty in prod.
4. Google Cloud Console: OAuth consent screen (app name, logo, support email,
authorized domain `b1fr0st.eu`, privacy/ToS/homepage links), declare scopes,
**Publish app → Production**, verify domain in Search Console (TXT added via
the porkbun CLI by the agent).
5. Infra: DNS record, Caddy vhost, `/srv/siftlode/` compose stack with limits,
backup hook, monitoring probe. Forgejo repo + Actions pipeline.
**Phase B — focused security review before publish**
- Review the new/changed code (wizard, new pages) right before the tester gets
a live token.
**Phase C — optional, later**
- Full Google verification (demo video + scope justification) if the warning
must be removed entirely or 100+ users are expected.
---
## 8. Open / external steps (need the user or browser)
- Google Cloud Console actions (OAuth client, consent screen, publish, Search
Console) are behind login — the user clicks them per the agent's instructions,
or the agent drives via Claude-in-Chrome.
- DNS + asgard-side infra + deploy pipeline are agent-handled.
## Notes / assumptions
- YouTube video playback streams from Google (embedded player), not through our
server → server bandwidth ≈ SPA bundle (cached) + JSON API; the asgard 5 TB/mo
cap is a non-issue for 2 users.
- YouTube Data API quota is 10,000 units/day per project (app already manages a
9,000 budget) — ample for 2 users.