Files
teknav-bak/app/SECURITY.md
T
arsam e9cd12dda6 Import remaining project documentation and reference notes
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:33:04 +03:30

9.9 KiB

Security — Teknav

Summary of security measures across the stack.


Nginx

HTTP Security Headers

  • Strict-Transport-Security (HSTS) — 1 year (max-age=31536000; includeSubDomains)
  • Content-Security-Policy — self-only scripts/styles/fonts; data: and blob: for inline images; no CDN
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: SAMEORIGIN
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: camera=(), microphone=(), geolocation=()

Rate Limiting (tuned to avoid false 429s)

Zone Rate Burst Targets
general 300 r/min 200 SPA routes
api 300 r/min 200 All /api/*
auth 30 r/min 10 /api/v1/auth/login, /api/v1/auth/signup

Fastify applies its own per-IP counters on top (5 login fails / 15 min; 10 signup attempts / hour).

Blocked Paths

  • Dotfiles: .git, .env, .htaccess, etc.
  • Operator credential file: /ACCOUNTS.md
  • Upload directory: blocks .php/.html/.js/.sh/.py/.env/.conf execution

Proxy Hardening

  • Server and X-Powered-By headers stripped
  • server_tokens off
  • Connection timeouts: 30s client, 10s proxy connect, 60s proxy read
  • trustProxy constrained to ['127.0.0.1', '::1'] — only the local nginx proxy is trusted

Error Handling

  • 404 → SPA fallback (/index.html) for client-side routing
  • 429 → JSON {"error":"too_many_requests","retryAfter":60}
  • 5xx → JSON {"error":"service_unavailable"}
  • 400/401/403 from /api/* pass through as-is (JSON from Fastify)

Backend (Fastify)

Authentication

  • Argon2id password hashing (m=19 MiB, t=2, p=1)
  • Login accepts email or @username; password verification stays Argon2id-backed
  • Anti-enumeration: constant-time login regardless of whether the user exists
  • Redis-backed sessions — tek_sid cookie (HttpOnly, Secure, SameSite=Lax), 14-day TTL with sliding expiration
  • Session rotation — new session ID issued on login and 2FA completion to prevent session fixation
  • Session invalidation on password change — all sessions for a user are destroyed when their password is updated (via reset, profile, or admin)
  • CSRF — double-submit cookie pattern (tek_csrf); required on all mutating routes except /api/v1/auth/login, /api/v1/auth/signup, /api/v1/auth/forgot-password, /api/v1/auth/reset-password, /api/v1/auth/otp/send, /api/v1/auth/otp/verify, /api/v1/auth/2fa/verify, /api/v1/newsletter, /api/v1/analytics/events, /api/v1/errors, /api/v1/jobs, and OAuth callbacks (/api/v1/auth/oauth/*)
  • OAuth — Google and GitHub (authorization-code grant); state token stored in Redis (10-min TTL, one-time use); no client secret exposed to frontend

Rate Limiting (application layer)

  • Global Fastify rate limiting on public API traffic (600 requests / IP / min, Redis-backed, per-IP key)
  • Route-level exceptions for OAuth callbacks, health, and explicit public-mutating endpoints
  • Login: 5 failures / IP / 15 min → 429 + Retry-After
  • Signup: 10 attempts / IP / hour → 429 + Retry-After
  • Captcha generate: 20 / IP / min
  • Captcha verify: 10 / IP / min
  • Newsletter: 3 / IP / hour
  • 2FA verify: 5 attempts / IP+ticket / 5 min → 429
  • Forgot-password: 5 / IP / hour
  • Reset-password: 10 / IP / hour
  • Error reporting: 30 / IP / min
  • Semantic search: 10 / IP / min

Input Validation

  • Zod schemas on every route (including admin author CRUD)
  • Fastify JSON-Schema validation (AJV) on all body/querystring
  • Content length limits: wiki proposals (500 KB), glossary definitions (50 KB), comments (1000 chars), article body (no upper bound)

Role-Based Access Control

Role Access
reader View articles, save, react, manage own profile
reviewer + Article review queue
writer + Create/edit own articles
editor + Edit any article, manage categories/tags/authors/media/newsletter/series
admin + Delete articles, manage users, activity logs, metrics

Engagement Controls

  • Comment moderation supports bulk approve/delete actions for editors/admins and keeps replies sorted by upvotes.
  • Comments require an authenticated session; backend strips HTML and limits body length to 1000 chars.
  • Comment flagging is idempotent per user through a compound unique constraint.
  • Reading history, notifications, and writer follows are scoped to the current session.
  • Notification and admin SSE streams require normal cookie auth; nginx disables buffering/compression only for stream paths.

Credential Storage

  • 2FA login uses Kavenegar OTP (6-digit code via crypto.randomInt()), and the verify endpoint is ticket-based with max 3 attempts.
  • Staff credentials live in backend/prisma/credentials.json (committed, plaintext)
  • Operator-facing credential sheet lives in ACCOUNTS.md (gitignored and nginx-blocked)
  • seed.ts hashes them with Argon2id before DB insert — never stored plaintext in Postgres
  • The three writer-admin accounts use resetPasswordOnSeed: true until production rotation is complete
  • Rotate via the admin panel before go-live
  • After rotation, users can change their own password from profile settings; existing-password accounts must provide the current password
  • Public registrations: reader role, Argon2id-hashed password (or OAuth — no password stored)
  • Admin-created users receive a cryptographically random 24-byte password (not a predictable default)

Database (PostgreSQL)

  • Bind PostgreSQL to localhost only; do not expose it publicly
  • passwordHash — Argon2id only
  • User.status enum — active, suspended, pending; suspended users are rejected at login
  • OAuth fields (oauthProvider, oauthSubject) use a composite unique index to prevent duplicate linking
  • All queries via Prisma ORM — no raw SQL, no $queryRawUnsafe, no SQL injection surface

Redis

  • Bind Redis to localhost only; do not expose it publicly
  • AOF + RDB persistence
  • Used for: sessions, CSRF tokens, rate limiting counters, OTP codes, OAuth state, SSE pub/sub, visitor counters

Linux/systemd

  • Backend runs as the dedicated teknav system user, not root
  • teknav-backend.service reads secrets from /etc/teknav/backend.env
  • Upload writes are restricted to /var/lib/teknav/uploads
  • Timers handle scheduled publishing and daily database backups
  • Redis pub/sub backs SSE notifications and admin live counters; Redis must remain private to localhost
  • Port 3010 (Fastify) should be firewalled to localhost only; trustProxy only trusts 127.0.0.1 and ::1

Build

  • After schema changes, also run cd backend && npm run build and cd backend && npm test.

  • Zero CDN — scripts/check-no-cdn.mjs greps dist/ for external domains; build fails on any match

  • Production preflight — npm run preflight:prod checks env templates, nginx SSE/credential protection, required scripts, and generated-engine safety before deployment.

  • Strict endpoint tests — cd backend && TEST_REQUIRE_SERVER=1 npm test forces API contract tests against a running backend; without the env var, tests skip endpoint assertions if no backend is reachable.

  • Run npm audit and cd backend && npm audit before each deploy

  • Body size limit: 1 MB global, 25 MB on admin upload routes


Health And Readiness

  • GET /api/v1/health — returns minimal { ok: true/false } for public load balancers and monitors
  • GET /api/v1/ready — returns { ok: true } or HTTP 503 when PostgreSQL or Redis is unavailable
  • GET /api/v1/admin/health — full diagnostics (PostgreSQL connections, Redis memory, feature flags, upload disk, version, uptime); admin-only, requires authenticated session
  • Neither public endpoint exposes credentials, keys, DSNs, or user data.

Pre-Deploy Checklist

  • /etc/teknav/backend.env configured (SESSION_SECRET ≥ 32 chars, strong DB password)
  • TLS issued for www.teknav.ir and teknav.ir
  • nginx/nginx.conf updated with real server_name + cert paths
  • OAuth GOOGLE_CLIENT_ID/SECRET and/or GITHUB_CLIENT_ID/SECRET set if using OAuth
  • OAUTH_CALLBACK_BASE set to https://www.teknav.ir
  • SMTP vars set for password reset and email verification
  • SMS_PROVIDER=kavenegar and SMS_API_KEY set for phone OTP
  • Credentials in credentials.json rotated (via admin UI after first login)
  • ACCOUNTS.md is not committed and curl -I https://www.teknav.ir/ACCOUNTS.md returns 404/403
  • Remove resetPasswordOnSeed after rotating writer-admin passwords if future seeds must not reset them
  • npm run preflight:prod passes, with only accepted migration-readiness warnings
  • npm run build passes (no CDN refs)
  • cd backend && npm run build passes
  • cd backend && npm audit shows 0 vulnerabilities
  • sudo systemctl status teknav-backend is healthy
  • curl https://www.teknav.ir/api/v1/health → 200 {"ok":true}
  • curl https://www.teknav.ir/api/v1/ready → 200 when PostgreSQL and Redis are healthy
  • Test login at https://www.teknav.ir/login
  • Test user signup at https://www.teknav.ir/login → signup tab
  • Test authenticated comment, flag, history, follow, and notification flows
  • Test /api/v1/admin/stream while logged in as admin/editor
  • Verify curl https://www.teknav.ir/api/v1/admin/health requires authentication

Known Limitations

  • Email/SMS depend on production providers — verification routes are implemented, but delivery requires configured SMTP and Kavenegar credentials
  • unsafe-inline on style-src — required by Vite CSS-in-JS injection; harmless (affects styles, not scripts)
  • Session invalidation scans all Redis session keys — efficient for current scale; if session count grows significantly, consider a passwordChangedAt field approach instead
  • Global rate limit skipOnError: true — if Redis is down, rate limiting is bypassed (fail-open); this is intentional to avoid blocking all traffic on Redis failure