10 KiB
Subfeed — Public Deployment Plan (v1)
Status: approved (planning) — 2026-06-13. Goal: share the current Subfeed 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:
- Base login requests only
openid email profile→ user signs in with a clean, familiar Google screen, no warning, lands in the app. - 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 Subfeed; same pre-warning. Button → incremental consent. Skippable.
- Step — read access (
- 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 currentREAD_SCOPESintoBASE_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()alongsidehas_write_scope(). - Scope-gate the feed/import endpoints; expose
/api/me/scopesso 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, subfeed.b1fr0st.eu)
│ reverse_proxy 127.0.0.1:8080
▼
subfeed-api (FastAPI + built SPA, Docker)
│ internal docker network only
▼
subfeed-db (Postgres 16 — never published)
- DNS:
porkbun add b1fr0st.eu A subfeed 88.218.78.254(+ AAAA). Agent-owned via/usr/local/bin/porkbunon the PVE host. - TLS: Caddy DNS-01 (porkbun plugin), same as forge/pages — auto cert for
subfeed.b1fr0st.eu. - Caddy: new vhost block
subfeed.b1fr0st.eu { reverse_proxy 127.0.0.1:8080 }inheriting the existing security-header set (HSTS, X-Frame-Options, etc.). - OAuth redirect URI:
https://subfeed.b1fr0st.eu/auth/callback→OAUTH_REDIRECT_URL. Registered in the Google OAuth client. - Stack: dedicated prod compose at
/srv/subfeed/; 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)
subfeed-api:mem_limit: 768m,cpus: 1.0,pids_limit: 256,no-new-privileges,cap_drop: ALL, read-only rootfs + tmpfs/tmp, non-root user.subfeed-db:mem_limit: 512m,cpus: 0.75, tunedshared_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-snapshottimer with apg_dumpof the subfeed DB (rides the nightly tar.zst; pulled to the home array bypull-asgard-backups). - Monitoring: Prometheus blackbox probe for
subfeed.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:
- Push to a private Forgejo repo at
forge.b1fr0st.eu(Subfeed is currently local-git only). - Actions workflow on tag push: multi-stage Docker build → push to the
built-in registry (
forge.b1fr0st.eu/peter/subfeed:<tag>). - Deploy step in the same workflow:
cd /srv/subfeed && docker compose pull && docker compose up -d(local, since the runner is on asgard); migrations run via the entrypoint'salembic 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
- Onboarding wizard + scope split (§2):
auth.pybase/incremental scopes,has_read_scope(), scope-gated endpoints,/api/me/scopes, wizard UI + copy. - 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.
- Prod config: strong secrets,
OAUTH_REDIRECT_URL,ALLOWED_EMAILS= the two emails, setYOUTUBE_API_KEY(decouples backfill from user tokens),FRONTEND_ORIGINempty in prod. - 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). - Infra: DNS record, Caddy vhost,
/srv/subfeed/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.