Server hardening: nginx origin locked to ArvanCloud + CSP, demo logins removed, docs

- ops/nginx/: the two nginx sites as they now run on 249. Only ArvanCloud
  edges reach the app (direct hits on the IP get 403, so Arvan's DDoS/WAF
  can't be skipped); the frontend gets CSP (own sources only, the one inline
  script by hash), X-Frame-Options, nosniff, Referrer-Policy,
  Permissions-Policy, HSTS; API body limit 100M -> 20M plus HSTS. Checked from
  outside and in Chrome (all tabs and an island quiz, logged in): no CSP errors.
- Demo accounts (users 11-15, 25) and TEST_LOGIN_PHONES removed from the server;
  the health warning for fixed-code logins now has a test and says how to
  test instead (an OTPCode made in the admin).
- SMS daily cap on the server is 2000 (launch of ~600 users).
- CLAUDE.md: never run tests with --parallel on macOS, always --noinput
  (both hang forever without an error).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 19:17:11 +03:30
parent 2a03dc81b5
commit ba4ecff29e
9 changed files with 196 additions and 117 deletions

View File

@@ -70,8 +70,8 @@ paid on the server on 2026-10-03); orders without the `question_credits`/`hearts
## Before any real user: read `docs/security-before-production.md`
The server is deliberately left in demo mode for the internship presentation: seven
fixed-code demo logins exist, demo users sit in the production DB, and the leaderboard and `/admin/` are public (`/api/health` shows only ok/error without `X-Internal-Key`; Swagger/ReDoc/schema need an admin session since 2026-10-08). (The token signing key is no longer the
Since 2026-10-13 there are no fixed-code logins and no demo users on the server (`TEST_LOGIN_PHONES`
empty; users 11–15 and 25 deleted; test logins are an `OTPCode` made in the admin). Still open: the leaderboard and `/admin/` are public (`/api/health` shows only ok/error without `X-Internal-Key`; Swagger/ReDoc/schema need an admin session since 2026-10-08). (The token signing key is no longer the
committed one: replaced 2026-10-07, see the status block below.) Each is listed there with its risk, why it was left, and the
fix. **Do not treat this deployment as production-ready until every item there is closed**,
and when one is closed, move it to that file's «حفاظ‌هایی که هست» table with the test that holds it (the date lives in the commit message; the owner wants docs to describe the current state only).
@@ -82,11 +82,20 @@ and when one is closed, move it to that file's «حفاظ‌هایی که هست
source venv/bin/activate
python manage.py runserver
python manage.py migrate
python manage.py test # all tests
python manage.py test accounts # single app
python manage.py test accounts.tests.SomeTestCase.test_something # single test
python manage.py test --noinput # all tests (~60 s, serial)
python manage.py test --noinput accounts # single app
python manage.py test --noinput accounts.tests.SomeTestCase.test_something # single test
```
**Never run the suite with `--parallel` on this Mac, and always pass `--noinput`.** Both hang forever
without an error (2026-10-13, cost 30+ minutes): the test DB is a SQLite *file* (`.test-db.sqlite3`,
needed by the real-concurrency tests), and on macOS Django's spawned parallel workers fail with
`unable to open database file` while the parent waits for them indefinitely. Separately, a test DB left
behind by a killed run makes Django ask «Type 'yes' to delete the old test database» on stdin; in a
background shell nobody answers and it waits forever. `--noinput` deletes it. Serial runs take ~60 s
for the whole suite, so there is nothing to gain. If a run prints nothing for more than ~3 minutes it
is stuck, not slow: kill it (`pkill -f "manage.py test"`) and check the log for the two causes above.
Local dev requires `DJANGO_DEBUG=1` (enables Swagger auth via `DEV_MASTER_OTP`/`devOtp` in responses). `DEBUG` defaults to `False`. Production also requires `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS` (comma-separated), and ideally `CACHE_URL` (Redis). Without it the app still boots on per-process `LocMemCache` and `/api/health` warns — throttles and admin-driven cache invalidation are then per worker. On the server since 2026-10-08 it points at our own Redis (`zareian-redis.service`, port 6390, localhost, password-protected, 256 MB `allkeys-lru`, no persistence), never the company's Redis on 6379. Its outage is logged and rate limits stop applying (fail-open); not tested under real Redis failure on the server.
No pytest; uses Django's built-in test runner. No linter/formatter configured.
@@ -451,6 +460,22 @@ preferred setup, because there is then no key to leak. **Never let the server ru
`django-insecure-…` fallback from `config/settings.py`: that string is committed here, so
anyone who can read this repo can forge a token for any user.**
## بازبینیِ پروداکشن و سخت‌کردنِ سرور — ۲۰۲۶-۱۰-۱۳ (بک‌اند `e3bddb3` و بعد)
چک‌لیستِ سینیور: `~/Desktop/گزارشات Tadrisino/گزارش-۳۴/` (HTML و PDF؛ ساز در scratchpad، نه ریپو).
- **جعلِ IP بسته شد:** `config.middleware.TrustedProxyMiddleware` فقط XFFِ رسیده از رنج‌های آروان را می‌پذیرد
(`TRUSTED_PROXY_NETWORKS`؛ X-Real-IP را nginx می‌گذارد). پیش از آن درخواستِ مستقیم به IP با XFFِ چرخان همه‌ی سقف‌های
IP-محور را دور می‌زد (روی سرور بازتولید شد).
- **nginx (دو سایتِ ما، نسخه در `ops/nginx/`)**: فقط IPهای آروان (مستقیم ۴۰۳)، CSP و هدرهای امنیتی روی فرانت، سقفِ بدنه‌ی
API ۲۰ مگابایت، HSTS. بکاپِ قبلی: `.deploy-backups/nginx-*-before-20261013-nginx-hardening`. سنجیده: از بیرون، و اپ در Chrome
(همه‌ی تب‌ها و آزمونِ جزیره، با ورود) بی هیچ خطای CSP. نسنجیده: WebViewِ اندروید و `pay.html` تا درگاه.
- **حساب‌های دمو پاک شدند** (۱۱–۱۵، ۲۵) و `TEST_LOGIN_PHONES` خالی؛ بکاپ `.deploy-backups/db-before-demo-removal-20261013-190632.pgdump`
و `env-before-demo-removal-20261013-190632`. سقفِ پیامکِ روزانه ۲٬۰۰۰. اعتبارِ کاوه‌نگار آن روز ۲۵۳٬۸۴۱ ریال (شارژ: سینیور).
- **CPUِ ماشین:** `metabase` و `celerybeat`ِ Artina (مالِ دیگران) بی‌وقفه ری‌استارت می‌شوند (۱۷۶ و ~۱۲۰۰ بار در ساعت) و
حدودِ ۳ از ۸ هسته را می‌گیرند. به آن‌ها دست نزن؛ درخواست از ادمینِ سرور.
- `/etc/nginx` را classifierِ حالتِ خودکار فقط با تأییدِ صریحِ مالک و گام‌به‌گام (بکاپ → نصب و `nginx -t` → reload) اجازه داد.
## کافه‌بازار: اشتراک، تخفیفِ پویا، تمدیدِ خودکار — روی سرور از ۲۰۲۶-۱۰-۱۲ ۱۷:۵۴ (بک‌اند `8077ab4`، فرانت `fffdeac`)
- **محصول‌های پیشخان** (پرسشِ API با توکنِ ساختگی، ۱۰۰۰۰–۱۰۶۹۹): فقط `10342` (یک‌ماهه، تأییدِ سینیور)، `10343` (۹۰ روزه)،
@@ -535,8 +560,8 @@ anyone who can read this repo can forge a token for any user.**
۱۱۲ درخواست در ثانیه، p99 ۰٫۶۷ ثانیه، وقتی CPUِ ماشین ~۸۰٪ مشغول بود. ثبتِ جواب ۱۵٫۴→۱۱ ms، `/api/home` ۱۲٫۴→۸٫۶ ms.
- **برآورد (نه سنجیده):** ۱۰۰۰ تا ۱۵۰۰ بازیکنِ هم‌زمان (فرض: یک درخواست در ۸ ثانیه)، ۴ تا ۶ هزار آنلاین. سقفِ واقعی CPUِ
مشترک است، نه حافظه یا دیتابیس؛ worker بیشتر روی همین ماشین ظرفیت نمی‌سازد، CPUِ دیگران را می‌گیرد.
- **سقف‌های تنظیمی:** پیامک ۳۰۰ در روز برای کلِ سامانه و دستیار ۳۰۰۰ سوال در روز — هر دو برای روزِ لانچ کم‌اند و
**تصمیمِ مالک/سینیور** است (`OTP_SMS_GLOBAL_DAILY_MAX`، `AI_GLOBAL_DAILY_MAX` در `.env`؛ هیچ‌کدام الان در `.env` نیست).
- **سقف‌های تنظیمی:** پیامکِ کلِ سامانه روی سرور ۲٬۰۰۰ در روز (`OTP_SMS_GLOBAL_DAILY_MAX` در `.env`، از ۲۰۲۶-۱۰-۱۳ به
تصمیمِ مالک برای لانچِ ۶۰۰ نفره؛ پیش‌فرضِ کد ۳۰۰) و دستیار ۳۰۰۰ سوال در روز (`AI_GLOBAL_DAILY_MAX`، در `.env` نیست).
هر IP: ۱۰۰ درخواستِ کد و ۳۰۰ ثبتِ کد در ساعت (تصمیمِ مالک، برای کلاس روی یک وای‌فای).
- **پیشنهادشده، ساخته نشده:** جلسه‌ی آزمون به سبکِ دولینگو (سوال و کلید با هم دانلود، سنجش روی گوشی، ارسالِ یک‌جا
و سنجشِ دوباره در سرور) بارِ بازی را حدوداً نصف می‌کند؛ کلید روی گوشی می‌رود، پس تصمیمِ محصولی است. کلِ محتوا
@@ -1013,10 +1038,8 @@ migrationها اعمال شدند، سه‌یار و `nextLessonId` از بیر
سه چیزِ کوچک که در همان روز اضافه شد و دانستنشان لازم است:
- **`TEST_LOGIN_PHONES`** — فهرستِ «شماره:کد» در `.env` که ورود بدونِ کاوه‌نگار را
ممکن می‌کند. از ۲۰۲۶-۰۹-۲۳ شب، به تصمیمِ صاحبِ پروژه، روی سرور **۷ شماره‌ی دمو**
دارد (`09120000101` تا `09120000107`). کدها فقط در راهنمای ارائه‌ی خودِ صاحبِ پروژه‌اند،
نه در ریپو. جزئیات در `docs/operations.md`.
- **`TEST_LOGIN_PHONES`** — فهرستِ «شماره:کد» برای ورود بی‌پیامک. **از ۲۰۲۶-۱۰-۱۳ روی سرور خالی است**
(تصمیمِ مالک: کدِ ثابت نکته‌ی امنیتی است) و حساب‌های دمو پاک شدند؛ `docs/operations.md` «ورود بی‌پیامک».
- **RAG روی سرور روشن است** (همان شب): `OPENAI_API_KEY`/`QDRANT_API_KEY` در `.env`
سرور، `/api/health` می‌دهد `qdrant: true, openai: true`. هر سؤالِ کاربرِ واقعی از
اعتبارِ GapGPT خرج می‌کند؛ سقفِ ۱۵ در روز برای هر کاربر آن را محدود نگه می‌دارد.