Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
187 lines
9.9 KiB
Markdown
187 lines
9.9 KiB
Markdown
# 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
|