Tier 4 Onset T5-1 Infra Report — Phase 2 public exposure¶
Status: STOP — Ryan review required before T5-1-iOS
Date: 2026-08-30
Authorization: Ryan accepted Phase 1 verdict SAFE TO EXPOSE; authorized Phase 2 with gateway JWT hardening + scan/search auth fix folded in
Phase 1 reference: ONSET_DB_PUBLIC_EXPOSURE_RLS_SECURITY_PASS.md
Verdict¶
PHASE 2 COMPLETE — PUBLIC ENDPOINT LIVE (CF PROXIED)¶
Patient-facing Supabase is reachable at https://onset-api.theonset.app through Cloudflare (orange cloud) → onset-edge tunnel → Kong on VM 119. Public smoke passed, including cross-user RLS denial over the public URL.
This does NOT authorize iOS pointer flip, archive, or submit. That is T5-1-iOS, separately authorized.
Executive summary¶
| Item | Result |
|---|---|
| Public hostname | onset-api.theonset.app (product domain; avoids db in URL) |
| CF proxy in path | Yes — server: cloudflare, cf-ray present on all public probes |
| TLS | Valid (Cloudflare edge cert; HTTPS 200 on /auth/v1/health) |
| Gateway JWT hardening | 18 patient-facing functions → per-function JWT at edge runtime |
| Shared-secret / webhook functions | 7 remain gateway JWT off; in-function auth documented below |
| scan/search-supplement 500-before-auth | Fixed — auth before env; anon → 401 (not 500) |
| Anon PostgREST (public) | 0 rows on all tested patient tables |
| Cross-user denial (public) | PASS — hello 23 checkins; alex 0; no leak |
| Edge functions anon (public) | 401 on all patient-facing probes |
| Storage (public) | Private bucket empty/denied anon; public templates 200 |
iOS Production.xcconfig |
Not flipped (pairing documented for T5-1-iOS) |
1. Public endpoint topology¶
Internet
→ Cloudflare (proxied, WAF/rate-limit layer)
→ onset-edge tunnel (UUID fe2fa2de-d776-485f-85ed-84415f0c3493)
→ onset-app VM 118 / 192.168.1.101 (cloudflared)
→ http://192.168.1.102:8000 (onset-db Kong / supabase-envoy)
| Component | Value |
|---|---|
| Public URL | https://onset-api.theonset.app |
| DNS | CNAME → fe2fa2de-d776-485f-85ed-84415f0c3493.cfargotunnel.com (proxied) |
| CF zone | theonset.app (7607d8b8d46f14a6c903558e97eb555b) |
| Tunnel | onset-edge on onset-app (192.168.1.101) |
| Origin | onset-db Kong :8000 (192.168.1.102) |
| Stack public URL env | API_EXTERNAL_URL + SUPABASE_PUBLIC_URL → https://onset-api.theonset.app |
cloudflared ingress (/etc/cloudflared/config.yml on onset-app):
ingress:
- hostname: onset-api.theonset.app
service: http://192.168.1.102:8000
- hostname: clinic.theonset.app
service: http://192.168.1.101:3000
- service: http_status:404
Untouched (per constraints): hosted Supabase rollback net, clinic sovereign stack internals, other product edges.
Automation scripts (repo):
| Script | Purpose |
|---|---|
scripts/onset/t5-1-harden-functions.sh |
Per-function JWT + scan/search auth order + public URL env (VM 119) |
scripts/onset/t5-1-add-onset-api-tunnel.sh |
Add onset-api ingress to cloudflared (VM 101) |
scripts/onset/t5-1-dns-onset-api.sh |
Proxied CNAME via CF API |
scripts/onset/t5-1-public-smoke.sh |
Public smoke + cross-user denial |
2. Edge function gateway JWT decisions¶
Self-hosted Supabase keeps global FUNCTIONS_VERIFY_JWT=false in .env (Kong does not JWT-check all functions). Defense in depth is implemented in volumes/functions/main/index.ts via ONSET_FUNCTIONS_VERIFY_JWT — a comma-separated slug list that enables JWT verification at the edge runtime before the worker handler runs.
Env on VM 119:
ONSET_FUNCTIONS_VERIFY_JWT=daily-insight,client-event,scan-supplement-label,scan-supplement,search-supplement,resolve-branded-product,popular-branded-products,parse-lab,delete-account,apple-link,google-link,last1-link,last1-unlink,last1-connect-assertion,generate-protocol,lab-intelligence,scan-label,simulate-protocol-lab
2.1 verify_jwt = true (gateway JWT ON) — 18 patient-facing functions¶
| Function | Rationale |
|---|---|
daily-insight |
Patient app; user JWT required |
client-event |
Patient telemetry; user JWT required |
scan-supplement-label |
Patient supplement scan; user JWT required |
scan-supplement |
Patient supplement scan; user JWT required |
search-supplement |
Patient supplement search; user JWT required |
resolve-branded-product |
Patient catalog lookup; user JWT required |
popular-branded-products |
Patient catalog; user JWT required |
parse-lab |
Patient lab parse; user JWT required |
delete-account |
Patient account deletion; user JWT required |
apple-link |
Patient identity link; user JWT required |
google-link |
Patient identity link; user JWT required |
last1-link |
Patient Last1 link; user JWT required |
last1-unlink |
Patient Last1 unlink; user JWT required |
last1-connect-assertion |
Patient Last1 connect; user JWT required |
generate-protocol |
Patient protocol generation; user JWT required |
lab-intelligence |
Patient lab intelligence; user JWT required |
scan-label |
Patient label scan; user JWT required |
simulate-protocol-lab |
Patient protocol simulation; user JWT required |
Public smoke (anon POST): all above return 401 Missing authorization header over https://onset-api.theonset.app.
2.2 verify_jwt = false (gateway JWT OFF) — 7 functions + WHY¶
| Function | Why gateway JWT must stay off | In-function auth (confirmed) |
|---|---|---|
admin-dashboard |
Clinic admin portal uses x-onset-admin shared secret, not user JWT |
Rejects when x-onset-admin ≠ ADMIN_DASHBOARD_SECRET |
auth-send-email |
GoTrue webhook caller; verifies standardwebhooks signature on payload, not Bearer JWT |
Webhook.verify(body, headers) with hook secret |
fitness-extract |
Clinic protocol pipeline; x-protocol-import-secret header auth |
Secret header checked before processing |
protocol-extract |
Same clinic import pipeline | x-protocol-import-secret |
protocol-import |
Same clinic import pipeline | x-protocol-import-secret |
practitioner-notifications |
Clinic notification dispatch; x-notify-secret header |
Rejects 401 when secret mismatch |
hello |
Dev/demo Supabase scaffold; uses publishable/secret apiKey modes, not patient JWT path | Not on iOS patient path; requires apiKey |
These functions are clinic-side, webhook, or internal — enabling gateway JWT would break legitimate callers that authenticate with shared secrets or webhook signatures instead of user Bearer tokens.
3. scan-supplement / search-supplement resolution¶
Decision: Fixed auth ordering (not excluded from public path). Both are patient-facing and remain in ONSET_FUNCTIONS_VERIFY_JWT.
Problem (Phase 1): Env-var guard (ANTHROPIC_API_KEY, etc.) ran before auth check → anon callers got 500 Server misconfigured instead of 401.
Fix: Moved Bearer JWT + getUser() validation before env check in both:
volumes/functions/scan-supplement/index.tsvolumes/functions/search-supplement/index.ts
Verification (LAN + public, anon POST):
| Function | Before | After |
|---|---|---|
scan-supplement |
500 | 401 |
search-supplement |
500 | 401 |
4. Public smoke results¶
Probe host: https://onset-api.theonset.app (real public path, not LAN IP)
Date: 2026-08-30 UTC
4.1 Cloudflare proxy confirmation¶
Response headers on /auth/v1/health:
Direct-origin bypass was not used for smoke — all traffic through CF edge IPs (104.21.14.156, 172.67.203.205).
4.2 Auth health¶
| Path | HTTP | Body |
|---|---|---|
/auth/v1/health |
200 | {"version":"v2.189.0","name":"GoTrue",...} |
4.3 PostgREST anon (no JWT)¶
| Table | Rows (anon) |
|---|---|
practitioner_patients |
0 |
shared_checkins |
0 |
shared_lab_results |
0 |
shared_adherence |
0 |
data_share_events |
0 |
4.4 Cross-user denial (public endpoint) — proof that matters¶
Re-run of Phase 1 test over public URL (not LAN Kong):
| User | shared_checkins rows |
Leak check | |
|---|---|---|---|
| Patient A | hello@unicornryda.com |
23 | All rows owned by hello's UID |
| Patient B | alex.rivera@demo.theonset.app |
0 | No hello rows visible |
Result: CROSS_USER_PUBLIC=PASS
4.5 Edge functions (anon POST)¶
| Function | HTTP | Notes |
|---|---|---|
daily-insight |
401 | Gateway JWT |
client-event |
401 | Gateway JWT |
scan-supplement |
401 | Fixed ordering + gateway JWT |
search-supplement |
401 | Fixed ordering + gateway JWT |
delete-account |
401 | Gateway JWT |
4.6 Storage¶
| Check | HTTP | Result |
|---|---|---|
Public template templates/posts/1.png |
200 | Intentionally public |
Private bucket list clinic-support-attachments (anon) |
200 body [] |
No object leak |
5. iOS pointer pairing (T5-1-iOS — NOT flipped)¶
When Ryan authorizes T5-1-iOS, flip Production.xcconfig only:
| Key | T5-1-iOS target value |
|---|---|
SUPABASE_HOST |
onset-api.theonset.app |
SUPABASE_URL |
https://onset-api.theonset.app |
SUPABASE_ANON_KEY |
BWS ONSET_DB_PROD_SUPABASE_ANON_KEY (R940 JWT anon key from stack init; same value as VM 119 .env ANON_KEY) |
Anon key source: Bitwarden Secrets Manager project Onset → ONSET_DB_PROD_SUPABASE_ANON_KEY. Public by design (ships in app binary); do not inline in git docs (GitGuardian false-alarm noise). No rotation required.
Pre-flip smoke (T5-1-iOS): curl https://onset-api.theonset.app/auth/v1/settings from public internet → apple: true (R940 GoTrue + TU6S85XABQ key).
Still on hosted (unchanged): Release Production.xcconfig → emxrmjysvxeboavhatry.supabase.co until T5-1-iOS.
6. Phase 2 vs Phase 1 delta¶
| Gap (Phase 1) | Phase 2 resolution |
|---|---|
| No public hostname | onset-api.theonset.app live |
| CF proxy not in path | Proxied CNAME + cf-ray confirmed |
| Gateway JWT off all functions | 18 patient functions JWT at edge runtime |
| scan/search 500-before-auth | Auth-first ordering |
| Cross-user test LAN-only | Re-run over public URL — PASS |
7. STOP — next authorized step¶
| Step | Status |
|---|---|
| T5-1 Phase 1 RLS pass | Done — SAFE TO EXPOSE |
| T5-1 Phase 2 infra | Done — Ryan accepted; public endpoint live |
| T5-1-iOS Step 1 | In progress — Staging TestFlight public journey test (TIER_4_ONSET_T5-1_IOS_STEP1_PREP.md) |
| T5-1-iOS Step 2 (Production flip + archive) | Blocked until Step 1 journey PASS |
| T5-1-iOS Step 3 (upload + submit) | Blocked until Step 2 Ryan review |
Do not: flip Production.xcconfig, production archive, or submit until Step 1 device journey passes.