5.7 KiB
5.7 KiB
SECURITY — anwarpy.ai
Threat model: a solo-writer content site. The assets are the content, the member trust (ADR-0005), and the pipeline key. Not a fintech app — but every rule below is load-bearing.
1. Authentication
- Payload auth (email + password). Signup writes
verification_token+emailVerifiedflag at creation (ADR-0005 trust model); verification/resend routes:/api/auth/verify-email,/api/auth/resend-verification. Forgot/reset via Payload's built-in flow. - Session seam:
src/lib/viewer.tsgetViewer()is the only place components read identity. If auth provider changes (anticipated: Google OAuth or Clerk — owner decision 2026-10-11), it changes behind this seam; callers keep consumingViewer. - No password rules theater: one live hint (strength guidance) at signup; no confirm-password field (owner decision, recorded).
- Lockout: login attempts on
/api/usersare rate-limited bysrc/middleware.ts; the 429 message stays honest («too many attempts» fidelity — step 14 evidence). No CAPTCHA (owner decision, recorded 2026-10-10). - Verification emails go through Cloudflare Email REST (ADR-0003, never SMTP). Transport down → the UI/API says so in Arabic («خدمة البريد غير مربوطة بعد.»); it does not pretend success.
2. Environment variables & secrets
| Var | Scope | Rule |
|---|---|---|
DATABASE_URI / DB_PASSWORD |
server | compose builds the URI; never commit, never echo |
PAYLOAD_SECRET |
server | signing/encryption; rotate = invalidate sessions |
AGENT_API_KEY |
server | pipeline auth; header x-agent-key |
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, MAIL_FROM |
server | mail transport |
LAUNCH_MODE |
server | launch policy (SECURITY-relevant: gates paid flows) |
NEXT_PUBLIC_SITE_URL |
public | canonical/og origin |
STRIPE_SECRET_KEY, STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET |
server | Phase 3 |
SEED_TEST_USERS |
dev only | never set in prod |
Rules:
.env*files are gitignored and excluded from rsync deploys; prod secrets live only in/root/anwarpi/.env.production.- Never print secret values into transcripts, docs, videos, or evidence. Known incident: 8 chars of prod
DB_PASSWORDleaked in a session transcript on 2026-10-10 → owner to rotate; rotation is the standing remedy for any partial leak. - Read secrets at runtime via
process.envinside server modules only; never in'use client'code (onlyNEXT_PUBLIC_*there).
3. RBAC & content access
- Two tiers:
role: 'admin'(Ahmed,/admin+ agent pipeline ownership) and everyone else (member/public). No roles table beyond that until a real need. - Content ACL is the crown jewel (ARCHITECTURE §3):
Posts.contentfield-level read rule — public iffstatus=published && (access=free || freeEarlyAccess()). VIP previews (H2 list, dek) are the only VIP fields served to logged-out users when billing is on. - Files:
GET /api/files/[id]enforces per-file access before streaming; the download gate and the article page must agree (one decision, ARCHITECTURE §3). - Admin UI access = Payload's built-in
admingate; never hand-roll admin pages on the frontend.
4. Input handling & pipeline hardening
pickFieldson the agent API strips privileged fields (access,readingMinutes,statusexcept explicitpublish: trueopt-in on POST; PUT cannot setstatus).scanForSecrets/scanAgentPayload(src/lib/secret-scan.ts): POST and PUT scan the real text (Lexical →lexicalToText, or the raw string) for AWS AKIA keys, PEM blocks,sk-API keys, GitHub tokens (ghp_…), generic credential shapes → 422 with pattern NAMES only. Never echo the matched text, never log it.- String
content→ 400 (Lexical JSON required); no silent placeholder substitution. - Comment bodies: textarea, moderated (pending until approved), author forced server-side from the session — client-supplied
author/authorNameidentity is ignored/overwritten (ADR-0002). Display names are a snapshot, not a live join, so renamed users can't retroactively forge old comments. - All user-facing error strings are fixed Arabic sentences from the route handlers — errors never reflect raw input back (no echo = reflected-input hygiene).
5. Transport & external calls
- Everything behind Traefik with TLS on anwarpy.ai; www → apex 308.
- Outbound: Cloudflare Email REST with scoped token; Stripe webhook verifies signature (
STRIPE_WEBHOOK_SECRET) before trusting the body; agent API requires the key header (Authorization Bearer → 401). - The db container publishes no ports; prod DB access for maintenance = SSH tunnel (AGENTS.md rulebook recipe), never a public 5432.
6. Operational security
- Backups:
web/scripts/backup-db.sh→ gzippedpg_dump; rehearsed restore 2026-10-10 (temp DB, count check, drop). Backup artifacts stay on the VPSbackups/dir, never in git, never in the deploy rsync. - Test rows (smoke signups, notify rows) use
@example.comaddresses and are deleted after verification — prod counts (users=6,launch_notifications=0) are the post-cleanup ground truth. - Media/files volumes are excluded from rsync (
no --deleteprotects them). - Incident rule: any suspected leak (secret in transcript/log/URL) → rotate the credential first, then investigate. Recording the incident in docs is required; pretending it didn't happen is not.
7. What NOT to add (recorded decisions)
- No CAPTCHA, no aggressive rate limits beyond login lockout (owner, 2026-10-10).
- No WAF/Cloudflare proxy changes without owner approval — the domain's DNS/proxy is Ahmed's console.
- No secrets in client bundles; no
NEXT_PUBLIC_for anything except the site URL. - No new auth provider without routing it through
viewer.ts(see §1).