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

@@ -0,0 +1,17 @@
# روی ۲۴۹: /etc/nginx/snippets/zareian-front-headers.conf — هدرهای امنیتیِ math.tadrisino2.ir.
# در هر locationی که خودش add_header دارد دوباره include شود: nginx هدرهای سطحِ server را در چنین locationی ارث نمی‌دهد.
#
# CSP فقط منابعِ خودمان را می‌پذیرد:
# - script: فایل‌های خودِ سایت، و تنها اسکریپتِ درون‌خطیِ index.html (کلاسِ in-app برای اسپلش) با هشش. اگر آن
# اسکریپت در فرانت عوض شود، هش را از نو بسازید (python: base64(sha256(متنِ بینِ <script> و </script>)))؛
# وگرنه فقط همان اسکریپت اجرا نمی‌شود و کنسول «Refused to execute inline script» می‌گوید.
# - connect: API. با عوض شدنِ دامنه‌ی API (config.js) این‌جا هم عوض شود (docs/operations.md «عوض کردنِ دامنه»).
# - worker blob: کتابخانه‌ی confetti یک Worker از blob می‌سازد.
# - style 'unsafe-inline': <style>ِ index.html و pay.html و استایلِ درون‌خطیِ React.
# HSTS بی includeSubDomains: زیردامنه‌های دیگرِ tadrisino2.ir مالِ تیم‌های دیگرند.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-utBc36Q6M7NacvbHQCaMGCgm3osZ627xGQGm/tnSEXM='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://mathapi.tadrisino2.ir; worker-src 'self' blob:; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header Strict-Transport-Security "max-age=31536000" always;