Files
anwar-pi-website/SECURITY.md

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 + emailVerified flag 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.ts getViewer() 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 consuming Viewer.
  • No password rules theater: one live hint (strength guidance) at signup; no confirm-password field (owner decision, recorded).
  • Lockout: login attempts on /api/users are rate-limited by src/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_PASSWORD leaked 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.env inside server modules only; never in 'use client' code (only NEXT_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.content field-level read rule — public iff status=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 admin gate; never hand-roll admin pages on the frontend.

4. Input handling & pipeline hardening

  • pickFields on the agent API strips privileged fields (access, readingMinutes, status except explicit publish: true opt-in on POST; PUT cannot set status).
  • 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/authorName identity 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 → gzipped pg_dump; rehearsed restore 2026-10-10 (temp DB, count check, drop). Backup artifacts stay on the VPS backups/ dir, never in git, never in the deploy rsync.
  • Test rows (smoke signups, notify rows) use @example.com addresses 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 --delete protects 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).