Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
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:andblob:for inline images; no CDNX-Content-Type-Options: nosniffX-Frame-Options: SAMEORIGINReferrer-Policy: strict-origin-when-cross-originPermissions-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/.confexecution
Proxy Hardening
ServerandX-Powered-Byheaders strippedserver_tokens off- Connection timeouts: 30s client, 10s proxy connect, 60s proxy read
trustProxyconstrained 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_sidcookie (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.tshashes them with Argon2id before DB insert — never stored plaintext in Postgres- The three writer-admin accounts use
resetPasswordOnSeed: trueuntil 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:
readerrole, 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 onlyUser.statusenum —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
teknavsystem user, not root teknav-backend.servicereads 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;
trustProxyonly trusts127.0.0.1and::1
Build
-
After schema changes, also run
cd backend && npm run buildandcd backend && npm test. -
Zero CDN —
scripts/check-no-cdn.mjsgrepsdist/for external domains; build fails on any match -
Production preflight —
npm run preflight:prodchecks env templates, nginx SSE/credential protection, required scripts, and generated-engine safety before deployment. -
Strict endpoint tests —
cd backend && TEST_REQUIRE_SERVER=1 npm testforces API contract tests against a running backend; without the env var, tests skip endpoint assertions if no backend is reachable. -
Run
npm auditandcd backend && npm auditbefore 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 monitorsGET /api/v1/ready— returns{ ok: true }or HTTP 503 when PostgreSQL or Redis is unavailableGET /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.envconfigured (SESSION_SECRET≥ 32 chars, strong DB password)- TLS issued for
www.teknav.irandteknav.ir nginx/nginx.confupdated with realserver_name+ cert paths- OAuth
GOOGLE_CLIENT_ID/SECRETand/orGITHUB_CLIENT_ID/SECRETset if using OAuth OAUTH_CALLBACK_BASEset tohttps://www.teknav.ir- SMTP vars set for password reset and email verification
SMS_PROVIDER=kavenegarandSMS_API_KEYset for phone OTP- Credentials in
credentials.jsonrotated (via admin UI after first login) ACCOUNTS.mdis not committed andcurl -I https://www.teknav.ir/ACCOUNTS.mdreturns 404/403- Remove
resetPasswordOnSeedafter rotating writer-admin passwords if future seeds must not reset them npm run preflight:prodpasses, with only accepted migration-readiness warningsnpm run buildpasses (no CDN refs)cd backend && npm run buildpassescd backend && npm auditshows 0 vulnerabilitiessudo systemctl status teknav-backendis healthycurl 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/streamwhile logged in as admin/editor - Verify
curl https://www.teknav.ir/api/v1/admin/healthrequires authentication
Known Limitations
- Email/SMS depend on production providers — verification routes are implemented, but delivery requires configured SMTP and Kavenegar credentials
unsafe-inlineonstyle-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
passwordChangedAtfield 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