Compare commits
8 Commits
670513b462
...
43e02adf37
| Author | SHA1 | Date | |
|---|---|---|---|
| 43e02adf37 | |||
| e25ffb5575 | |||
| eb03b467d4 | |||
| f2c57c1ad1 | |||
| f6d779255d | |||
| 4cd859b5fa | |||
| dd98f16d12 | |||
| bd329f83ae |
@@ -39,6 +39,9 @@ DB_PORT=5432
|
|||||||
# اسلاتِ تکثیری که سرورِ بکاپ از آن کپیِ زنده میگیرد (ops/pgbackup/). روی سرور: zareian_247.
|
# اسلاتِ تکثیری که سرورِ بکاپ از آن کپیِ زنده میگیرد (ops/pgbackup/). روی سرور: zareian_247.
|
||||||
# خالی روی Postgres = کپیِ بیرونی نداریم و /api/health هشدار میدهد. روی لپتاپ خالی.
|
# خالی روی Postgres = کپیِ بیرونی نداریم و /api/health هشدار میدهد. روی لپتاپ خالی.
|
||||||
OFFSITE_WAL_SLOT=
|
OFFSITE_WAL_SLOT=
|
||||||
|
# سرورِ اختصاصی (۲۱۶): فایلی که tadrisino-offsite-sync زمانِ آخرین ارسالِ بکاپِ رمزشده به ۲۴۷ را در آن مینویسد.
|
||||||
|
# روی ۲۱۶: /var/lib/tadrisino-backup/offsite-synced-at (بر OFFSITE_WAL_SLOT مقدم است). روی لپتاپ خالی.
|
||||||
|
OFFSITE_SYNC_STATUS_FILE=
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
# ۲) دامنهها — دو آدرس، بقیه از آنها ساخته میشوند
|
# ۲) دامنهها — دو آدرس، بقیه از آنها ساخته میشوند
|
||||||
|
|||||||
15
CLAUDE.md
15
CLAUDE.md
@@ -63,10 +63,18 @@ other people's home directories (`aria`, `artina`, `behdad`, `zahra`, `git`, `gi
|
|||||||
other nginx site. `metabase` and Artina's `celerybeat` crash-loop and eat ~3 of 8 cores; that is a
|
other nginx site. `metabase` and Artina's `celerybeat` crash-loop and eat ~3 of 8 cores; that is a
|
||||||
request to the server admin, not something to fix.
|
request to the server admin, not something to fix.
|
||||||
|
|
||||||
**`78.38.35.247` (backup server)**, same account and password: ours is only `~/zareian-pgbackup/`
|
**`78.38.35.247` (backup server)**, same account and password: ours is only `~/zareian-pgbackup/` (249's
|
||||||
and the `zareian-pg-*` / `zareian-applog-pull` units. It also runs other teams' services and its
|
live copy), `~/zareian-pgbackrest-216/` (216's backups, encrypted on 216; 247 never holds the key), our lines in
|
||||||
|
`~/.ssh/authorized_keys` and `~/.pgpass`, `~/.ssh/zareian_*`, and the `zareian-pg-*` / `zareian-applog-pull` units. It also runs other teams' services and its
|
||||||
own `postgresql@14-main`, which we never use.
|
own `postgresql@14-main`, which we never use.
|
||||||
|
|
||||||
|
**`78.38.35.216` (hostname `mathapp`, new dedicated VM, not yet serving users)**, account `zareian` (sudo; the
|
||||||
|
senior and the company owner share `mathapp-admin`, also sudo; no other logins): the whole machine is ours. The app runs as the non-sudo user `mathapp` under `/home/mathapp/`; same unit names as on 249.
|
||||||
|
Both accounts use the password on the `AdminUser-Password` line of the password file (the `Password` line under
|
||||||
|
`NewServer` is older). State and the cut-over order:
|
||||||
|
`docs/operations.md` «سرورِ اختصاصیِ جدید». Until cut-over, 249 stays the live server and every rule below applies
|
||||||
|
there unchanged; on 216, rules 1, 5, 6, 8 and 10 still apply.
|
||||||
|
|
||||||
### Rules (not negotiable)
|
### Rules (not negotiable)
|
||||||
|
|
||||||
1. **Read-only first.** Before changing anything, look: `systemctl status`, `ls`, `git -C … log -1`,
|
1. **Read-only first.** Before changing anything, look: `systemctl status`, `ls`, `git -C … log -1`,
|
||||||
@@ -114,7 +122,8 @@ session. Take only the password line, everything after the first `: ` (stripping
|
|||||||
once sent the whole line and SSH refused it):
|
once sent the whole line and SSH refused it):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sed -n '/^Password/{s/^[^:]*: *//;p;}' ~/Desktop/'رمز سرور.txt' > "$SCRATCH/.pw" && chmod 600 "$SCRATCH/.pw"
|
sed -n '/^Password/{s/^[^:]*: *//;p;q;}' ~/Desktop/'رمز سرور.txt' | tr -d '\r\n' > "$SCRATCH/.pw" && chmod 600 "$SCRATCH/.pw" # 249/247 (first Password line)
|
||||||
|
awk '/^AdminUser-Password/{sub(/^[^:]*: */,"");printf "%s",$0;exit}' ~/Desktop/'رمز سرور.txt' | tr -d '\r\n' > "$SCRATCH/.pwn" && chmod 600 "$SCRATCH/.pwn" # 216
|
||||||
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 '<command>'
|
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 '<command>'
|
||||||
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 'sudo -S -p "" <command>' < "$SCRATCH/.pw"
|
sshpass -f "$SCRATCH/.pw" ssh administrator@78.38.35.249 'sudo -S -p "" <command>' < "$SCRATCH/.pw"
|
||||||
rm -P "$SCRATCH/.pw" # as soon as the work is done (macOS has no shred); recreate it next time
|
rm -P "$SCRATCH/.pw" # as soon as the work is done (macOS has no shred); recreate it next time
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ GET /api/health — «آیا این سرور واقعاً سالم است؟»
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -160,6 +161,8 @@ def _offsite_backup() -> dict:
|
|||||||
"""
|
"""
|
||||||
if connection.vendor != 'postgresql':
|
if connection.vendor != 'postgresql':
|
||||||
return {'state': 'not_postgres'}
|
return {'state': 'not_postgres'}
|
||||||
|
if settings.OFFSITE_SYNC_STATUS_FILE:
|
||||||
|
return _pushed_backup()
|
||||||
slot = settings.OFFSITE_WAL_SLOT
|
slot = settings.OFFSITE_WAL_SLOT
|
||||||
if not slot:
|
if not slot:
|
||||||
return {'state': 'not_configured'}
|
return {'state': 'not_configured'}
|
||||||
@@ -193,6 +196,45 @@ def _slot_state(row) -> dict:
|
|||||||
return {'state': state, 'lagBytes': lag}
|
return {'state': state, 'lagBytes': lag}
|
||||||
|
|
||||||
|
|
||||||
|
def _pushed_backup() -> dict:
|
||||||
|
"""
|
||||||
|
بکاپِ رمزشدهی ۲۱۶: pgBackRest هر WAL را با archive_command آرشیو میکند (pg_stat_archiver) و
|
||||||
|
`tadrisino-offsite-sync` مخزن را هر دقیقه به ۲۴۷ میفرستد و زمانش را در OFFSITE_SYNC_STATUS_FILE مینویسد.
|
||||||
|
کهنگیِ آخرین آرشیو هشدار نیست: دیتابیسِ بیتغییر WALِ تازهای ندارد که آرشیو شود.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute('SELECT last_archived_time, last_failed_time FROM pg_stat_archiver')
|
||||||
|
last_archived, last_failed = cursor.fetchone()
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return {'state': 'error', 'error': type(exc).__name__}
|
||||||
|
try:
|
||||||
|
with open(settings.OFFSITE_SYNC_STATUS_FILE) as f:
|
||||||
|
synced_at = int(f.read().strip())
|
||||||
|
except FileNotFoundError:
|
||||||
|
synced_at = None
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
return {'state': 'error', 'error': type(exc).__name__}
|
||||||
|
return _pushed_state(last_archived, last_failed, synced_at, time.time())
|
||||||
|
|
||||||
|
|
||||||
|
def _pushed_state(last_archived, last_failed, synced_at, now) -> dict:
|
||||||
|
age = None if synced_at is None else int(now - synced_at)
|
||||||
|
if last_failed is not None and (last_archived is None or last_failed > last_archived):
|
||||||
|
state = 'archive_failing'
|
||||||
|
elif age is None:
|
||||||
|
state = 'never_synced'
|
||||||
|
elif age > settings.OFFSITE_SYNC_MAX_AGE_SECONDS:
|
||||||
|
state = 'sync_stale'
|
||||||
|
else:
|
||||||
|
state = 'ok'
|
||||||
|
return {
|
||||||
|
'state': state,
|
||||||
|
'syncAgeSeconds': age,
|
||||||
|
'lastArchivedAt': last_archived.isoformat() if last_archived else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# اتصالهایی بیرون از poolِ workerها: pg_receivewal و pg_basebackupِ سرورِ بکاپ (۲)، pg_dump و
|
# اتصالهایی بیرون از poolِ workerها: pg_receivewal و pg_basebackupِ سرورِ بکاپ (۲)، pg_dump و
|
||||||
# migrateِ deploy.sh (۲)، فرمانِ مدیریتی یا psqlِ دستی (۲)، و workerی که gunicorn با max_requests
|
# migrateِ deploy.sh (۲)، فرمانِ مدیریتی یا psqlِ دستی (۲)، و workerی که gunicorn با max_requests
|
||||||
# عوض میکند و تا بسته شدنِ قبلی همزمان بالاست (یک pool).
|
# عوض میکند و تا بسته شدنِ قبلی همزمان بالاست (یک pool).
|
||||||
@@ -267,6 +309,9 @@ _OFFSITE_MESSAGES = {
|
|||||||
'lagging': 'سرورِ بکاپ از دیتابیس عقب است.',
|
'lagging': 'سرورِ بکاپ از دیتابیس عقب است.',
|
||||||
'lost': 'سرورِ بکاپ آنقدر قطع بود که زنجیرهی WAL برید؛ روی آن سرور اسلات و base backupِ تازه لازم است.',
|
'lost': 'سرورِ بکاپ آنقدر قطع بود که زنجیرهی WAL برید؛ روی آن سرور اسلات و base backupِ تازه لازم است.',
|
||||||
'error': 'وضعیتِ کپیِ زندهی دیتابیس خوانده نشد.',
|
'error': 'وضعیتِ کپیِ زندهی دیتابیس خوانده نشد.',
|
||||||
|
'archive_failing': 'آرشیوِ WAL (pgBackRest) شکست میخورد و تغییراتِ تازه بکاپ نمیشوند: sudo -u postgres pgbackrest --stanza=zareian check',
|
||||||
|
'never_synced': 'بکاپِ رمزشده هنوز هیچوقت به سرورِ بیرونی (۲۴۷) نرسیده (فایلِ OFFSITE_SYNC_STATUS_FILE نیست).',
|
||||||
|
'sync_stale': 'ارسالِ بکاپِ رمزشده به ۲۴۷ عقب افتاده: journalctl -u tadrisino-offsite-sync',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1078,6 +1078,12 @@ LOG_DIR = os.environ.get('LOG_DIR', '')
|
|||||||
OFFSITE_WAL_SLOT = os.environ.get('OFFSITE_WAL_SLOT', '')
|
OFFSITE_WAL_SLOT = os.environ.get('OFFSITE_WAL_SLOT', '')
|
||||||
# بیش از این عقبماندن هشدار است. ۶۴ مگ ≈ چهار فایلِ WAL؛ در ترافیکِ عادی عقبماندگی نزدیکِ صفر است.
|
# بیش از این عقبماندن هشدار است. ۶۴ مگ ≈ چهار فایلِ WAL؛ در ترافیکِ عادی عقبماندگی نزدیکِ صفر است.
|
||||||
OFFSITE_WAL_MAX_LAG_BYTES = 64 * 2**20
|
OFFSITE_WAL_MAX_LAG_BYTES = 64 * 2**20
|
||||||
|
# سرورِ اختصاصی (۲۱۶): بکاپِ رمزشدهی pgBackRest؛ `tadrisino-offsite-sync` هر دقیقه آن را به ۲۴۷ میفرستد و زمانِ
|
||||||
|
# آخرین ارسالِ موفق (epoch) را در این فایل مینویسد (docs/operations.md «سرورِ اختصاصیِ جدید»). ست باشد، بر
|
||||||
|
# OFFSITE_WAL_SLOT مقدم است. OFFSITE_WAL_SLOT را فقط ۲۴۹ دارد و با سوئیچ به ۲۱۶ همراهِ _slot_state حذف میشود.
|
||||||
|
OFFSITE_SYNC_STATUS_FILE = os.environ.get('OFFSITE_SYNC_STATUS_FILE', '')
|
||||||
|
# ارسال هر دقیقه است؛ ۱۰ دقیقه بیارسال یعنی چند اجرای پشتِ سرِ هم شکست خورده.
|
||||||
|
OFFSITE_SYNC_MAX_AGE_SECONDS = 600
|
||||||
|
|
||||||
# چرخشِ فایلِ لاگ: 'size' (پیشفرض، Docker و محلی) = ۶ فایلِ ۱۰ مگی و قدیمیها پاک میشوند؛
|
# چرخشِ فایلِ لاگ: 'size' (پیشفرض، Docker و محلی) = ۶ فایلِ ۱۰ مگی و قدیمیها پاک میشوند؛
|
||||||
# 'external' (سرور) = برنامه هرگز پاک نمیکند، `ops/applogs/archive-app-log.sh` هر شب app.log را جابهجا و
|
# 'external' (سرور) = برنامه هرگز پاک نمیکند، `ops/applogs/archive-app-log.sh` هر شب app.log را جابهجا و
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
پرسشِ واقعی از pg_replication_slots اینجا اجرا نمیشود (تستها روی SQLiteاند)؛ روی سرور با اسلاتِ
|
پرسشِ واقعی از pg_replication_slots اینجا اجرا نمیشود (تستها روی SQLiteاند)؛ روی سرور با اسلاتِ
|
||||||
واقعی دیده شد (docs/operations.md «بکاپ»).
|
واقعی دیده شد (docs/operations.md «بکاپ»).
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
|
||||||
from config import health
|
from config import health
|
||||||
@@ -27,13 +29,45 @@ class SlotStateTests(SimpleTestCase):
|
|||||||
self.assertEqual(health._slot_state((True, 'reserved', 100))['state'], 'ok')
|
self.assertEqual(health._slot_state((True, 'reserved', 100))['state'], 'ok')
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(OFFSITE_SYNC_MAX_AGE_SECONDS=600)
|
||||||
|
class PushedStateTests(SimpleTestCase):
|
||||||
|
"""بکاپِ رمزشدهی ۲۱۶ (_pushed_state). پرسشِ pg_stat_archiver و خواندنِ فایل روی سرور دیده شد، نه اینجا."""
|
||||||
|
|
||||||
|
T0 = datetime(2026, 10, 17, 16, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
def test_recent_sync_is_ok(self):
|
||||||
|
r = health._pushed_state(self.T0, None, 1000, now=1060)
|
||||||
|
self.assertEqual((r['state'], r['syncAgeSeconds']), ('ok', 60))
|
||||||
|
self.assertEqual(r['lastArchivedAt'], self.T0.isoformat())
|
||||||
|
|
||||||
|
def test_old_sync_is_stale(self):
|
||||||
|
self.assertEqual(health._pushed_state(self.T0, None, 1000, now=1601)['state'], 'sync_stale')
|
||||||
|
self.assertEqual(health._pushed_state(self.T0, None, 1000, now=1600)['state'], 'ok')
|
||||||
|
|
||||||
|
def test_no_status_file_is_never_synced(self):
|
||||||
|
self.assertEqual(health._pushed_state(self.T0, None, None, now=1000)['state'], 'never_synced')
|
||||||
|
|
||||||
|
def test_failure_after_last_success_wins(self):
|
||||||
|
later = self.T0 + timedelta(minutes=1)
|
||||||
|
self.assertEqual(health._pushed_state(self.T0, later, 1000, now=1001)['state'], 'archive_failing')
|
||||||
|
self.assertEqual(health._pushed_state(None, later, 1000, now=1001)['state'], 'archive_failing')
|
||||||
|
|
||||||
|
def test_old_failure_then_success_is_ok(self):
|
||||||
|
earlier = self.T0 - timedelta(minutes=1)
|
||||||
|
self.assertEqual(health._pushed_state(self.T0, earlier, 1000, now=1001)['state'], 'ok')
|
||||||
|
|
||||||
|
def test_idle_database_with_no_archive_yet_is_ok(self):
|
||||||
|
self.assertEqual(health._pushed_state(None, None, 1000, now=1001)['state'], 'ok')
|
||||||
|
|
||||||
|
|
||||||
class OffsiteWarningTests(SimpleTestCase):
|
class OffsiteWarningTests(SimpleTestCase):
|
||||||
def test_ok_and_dev_are_silent(self):
|
def test_ok_and_dev_are_silent(self):
|
||||||
self.assertEqual(health._offsite_warnings({'state': 'ok', 'lagBytes': 0}), [])
|
self.assertEqual(health._offsite_warnings({'state': 'ok', 'lagBytes': 0}), [])
|
||||||
self.assertEqual(health._offsite_warnings({'state': 'not_postgres'}), [])
|
self.assertEqual(health._offsite_warnings({'state': 'not_postgres'}), [])
|
||||||
|
|
||||||
def test_every_bad_state_warns(self):
|
def test_every_bad_state_warns(self):
|
||||||
for state in ('not_configured', 'slot_missing', 'disconnected', 'lagging', 'lost', 'error'):
|
for state in ('not_configured', 'slot_missing', 'disconnected', 'lagging', 'lost', 'error',
|
||||||
|
'archive_failing', 'never_synced', 'sync_stale'):
|
||||||
self.assertEqual(len(health._offsite_warnings({'state': state})), 1, state)
|
self.assertEqual(len(health._offsite_warnings({'state': state})), 1, state)
|
||||||
|
|
||||||
def test_sqlite_reports_not_postgres(self):
|
def test_sqlite_reports_not_postgres(self):
|
||||||
|
|||||||
@@ -382,6 +382,104 @@ the servers»):
|
|||||||
- سنجیده (۲۰۲۶-۱۰-۱۳): اپ در Chrome، بیورود و با ورود، همهی تبها و آزمونِ یک جزیره، بی هیچ خطای CSP. **نسنجیده:** اپِ اندروید
|
- سنجیده (۲۰۲۶-۱۰-۱۳): اپ در Chrome، بیورود و با ورود، همهی تبها و آزمونِ یک جزیره، بی هیچ خطای CSP. **نسنجیده:** اپِ اندروید
|
||||||
(WebView همان CSP را میگیرد) و صفحهی `pay.html` تا درگاه.
|
(WebView همان CSP را میگیرد) و صفحهی `pay.html` تا درگاه.
|
||||||
|
|
||||||
|
## سرورِ اختصاصیِ جدید (`78.38.35.216`): آماده، هنوز سوئیچ نشده
|
||||||
|
|
||||||
|
اپ به یک ماشینِ مجازیِ اختصاصی منتقل میشود. کاربران هنوز از ۲۴۹ سرو میشوند؛ ArvanCloud هنوز به ۲۴۹ اشاره میکند.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| ماشین | VMware، Ubuntu 24.04، ۸ هسته، ۱۲ گیگ رم، دیسکِ ۹۷ گیگ؛ hostname `mathapp`، IPِ داخلی `192.168.200.163` (۲۴۹ = `.164`)، IPِ خروجی `78.38.35.228` |
|
||||||
|
| ورود | دو حساب با sudo: `zareian` (صاحبِ محصول) و `mathapp-admin` (مشترکِ سینیور و صاحبِ شرکت؛ رمزش در فایلِ رمز زیرِ `AdminUser`). `temp` حذف شد (۲۰۲۶-۱۰-۱۷؛ authorized_keysش در `/home/zareian/backups/`). ورودِ root بسته، `AllowUsers zareian mathapp-admin`، forwarding بسته (`10-tadrisino.conf`) |
|
||||||
|
| فایلها | نسخهی اصلیِ همهی اسکریپتها و تنظیمهای این سرور در `ops/server216/` (بیکلید). تغییر: فایلِ ریپو، بعد کپی روی سرور |
|
||||||
|
| ضدِ ربات | ۱) `tadrisino-blocklist.timer`: هر روز FireHOL level1 (Spamhaus، DShield، …) در ipsetِ `tadrisino-block`، همهی پورتها؛ شبکهی داخلی، `78.38.35.0/24` و رنجهای آروان هرگز در آن نیستند. ۲) `ufw limit 22`: بیش از ۶ اتصال در ۳۰ ثانیه رد. ۳) fail2ban پلهای: ۶ اشتباه در ۱۰ دقیقه → ۱۰ دقیقه، هر تکرار دو برابر تا یک هفته؛ `recidive`: ۵ بار در روز → یک هفته همهی پورتها؛ `192.168.200.0/24` هرگز. آزاد کردن: `sudo fail2ban-client set sshd unbanip <IP>` |
|
||||||
|
| بکاپ | **رمزنگاری روی همین سرور، ۲۴۷ فقط فایلِ رمزشده میبیند** (۲۴۷ مشترک است و هر کس `administrator` را دارد root است). pgBackRest با `repo1-cipher-type=aes-256-cbc`: آرشیوِ پیوستهی WAL (`archive_timeout=60`، یعنی حداکثر یک دقیقه تغییرِ آرشیونشده)، `tadrisino-pgbackrest-full` یکشنبهها، `-diff` هر روز، دو نسخهی کامل نگه داشته میشود. CSVهای پرداخت هر ۶ ساعت با age رمز میشوند (`tadrisino-export-payment-events`). `tadrisino-offsite-sync` هر دقیقه `/var/lib/pgbackrest` و `payment-log` را به `~/zareian-pgbackrest-216/` روی ۲۴۷ میفرستد؛ کلیدِ postgres در `authorized_keys`ِ ۲۴۷ با `from=192.168.200.163,restrict,command="rrsync -wo …"` فقط میتواند در همان پوشه بنویسد (نه بخواند، نه شِل). `/api/health` → `offsiteBackup` از `pg_stat_archiver` و `OFFSITE_SYNC_STATUS_FILE` |
|
||||||
|
| کلیدهای بازیابی | `/etc/tadrisino-backup/RECOVERY-KEYS` (root، 600): کلیدِ pgBackRest و کلیدِ age. **بیآنها بکاپِ ۲۴۷ خواندنی نیست**؛ یک نسخه بیرون از سرورها نزدِ صاحبِ محصول لازم است: `ssh -t zareian@78.38.35.216 sudo cat /etc/tadrisino-backup/RECOVERY-KEYS` در ترمینالِ خودش (نه در چت) |
|
||||||
|
| آزمونِ بازیابی | `sudo -u postgres tadrisino-restore-check` (ماهی یک بار): بازیابی در پوشهی موقت و مقایسهی شمارشها با دیتابیسِ زنده. بازیابیِ واقعی از ۲۴۷ (اگر ۲۱۶ از دست رفت): پوشهی `repo/` را از ۲۴۷ کپی، `pgbackrest.conf` با کلید از RECOVERY-KEYS، `pgbackrest --stanza=zareian restore` |
|
||||||
|
| اپ | کاربرِ بیsudoِ `mathapp`؛ `/home/mathapp/mathapp-v2` و `/home/mathapp/mathapp_frontend_new` |
|
||||||
|
| سرویسها | `zareian-mathapp` (= `gunicorn-mathapp`، با `RuntimeDirectory`)، `zareian-redis` (6390)، `postgresql@16-zareian` (5440)، `nginx`، `zareian-applog-archive.timer` |
|
||||||
|
| رمزها | رمزِ Postgres و Redis روی این سرور تازه ساخته شده (`.pg_password`، `.redis_password` کنارِ `.env`)، نه کپیِ ۲۴۹ |
|
||||||
|
| فایروال | ufw فقط 22/80/443. فایروالِ شبکهی شرکت 443ِ ورودی را هنوز میبندد (80 باز است) |
|
||||||
|
| بستهها | apt از `mirror.arvancloud.ir`، pip از `mirror-pypi.runflare.com` (`~mathapp/.config/pip/pip.conf`)، npm از `mirror-npm.runflare.com`؛ `files.pythonhosted.org` و `registry.npmjs.org` از این سرور بستهاند |
|
||||||
|
|
||||||
|
سنجیده: `/api/health` با همهی جزئیات ok، آزمونِ بازیابی از بکاپِ رمزشده با همان شمارشها، فایلهای ۲۴۷ رمزشده (`Salted__`،
|
||||||
|
`age-encryption.org/v1`)، کلیدِ ۲۱۶ روی ۲۴۷ نه میخواند نه شِل میگیرد، کپیِ ۲۴۷ با `rsync --checksum` عینِ مخزنِ محلی، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و
|
||||||
|
بالا آمدنِ خودکارِ همهی سرویسها، درخواستِ مستقیم به IP ۴۰۳ (nginx IPِ واقعی را میبیند)، بازیابیِ کامل از نسخهی رمزشدهی
|
||||||
|
خودِ ۲۴۷ (دیتابیس و CSV؛ عقبماندگیِ طراحیشده حداکثر ~۲ دقیقه). سفرِ کاملِ کاربر از nginx+TLS روی خودِ سرور (۲۹ مورد): فرانت و
|
||||||
|
هدرهای امنیتی، پیامکِ واقعیِ کاوهنگار و ورود، نقشه، آزمونِ جزیره و پایانش، پیِ زنده، سفارش و redirect به StartPayِ زرینپال (بیپرداخت)،
|
||||||
|
خروج و باطل شدنِ توکن. کاوهنگار و GapGPT از IPِ خروجیِ این سرور پذیرفتهاند. نسنجیده: هیچ درخواستی از مسیرِ ArvanCloud
|
||||||
|
(۴۴۳ بسته است)، پرداختِ کامل، خریدِ بازار.
|
||||||
|
|
||||||
|
### چه چیزی منتظرِ دیگران است
|
||||||
|
|
||||||
|
درخواست از سینیور فرستاده شده (۲۰۲۶-۱۰-۱۷): ۱) 443ِ ورودی روی فایروالِ شرکت؛ ۲) (انجام شد: `temp` حذف و `mathapp-admin` ساخته شد)؛ ۳) عوض کردنِ originِ
|
||||||
|
`math.tadrisino2.ir` و `mathapi.tadrisino2.ir` در پنلِ ArvanCloud به `78.38.35.216` (ما به پنل دسترسی نداریم؛ همزمان با قدمِ ۴ پایین)؛
|
||||||
|
۴) private کردنِ مخزن در Gitea؛ ۵) ۱۲ هسته و رمِ بیشتر (اختیاری؛ سرور ریبوت را تحمل میکند، تست شده). زرینپال و بازار به
|
||||||
|
دامنه بستهاند نه IP، پس در پنلِ آنها چیزی عوض نمیشود.
|
||||||
|
|
||||||
|
### سوئیچ، قدمبهقدم (وقتی سینیور گفت آماده است)
|
||||||
|
|
||||||
|
1. **پیششرط:** از لپتاپ `curl -sk -o /dev/null -w '%{http_code}' https://78.38.35.216/` باید ۴۰۳ بدهد (نه ۰۰۰: یعنی 443 باز است و
|
||||||
|
nginx فقط آروان را راه میدهد). کامیتها push شده باشند و `deploy.sh` روی هر دو سرور اجرا شده باشد (کدِ دو طرف یکی).
|
||||||
|
2. **۲۴۹ را بخوابان:** `sudo systemctl stop zareian-mathapp` (از این لحظه کاربران خطا میبینند تا قدمِ ۵؛ کلِ کار ~۱۵ دقیقه).
|
||||||
|
3. **دیتابیسِ نهایی:** `sudo -u postgres pg_dump -p 5440 -Fc zareian_mathapp` روی ۲۴۹، با pipe از لپتاپ به
|
||||||
|
`/home/zareian/stage/` روی ۲۱۶ (مسیرِ داخلیِ ۲۴۹→۲۱۶ کلید ندارد).
|
||||||
|
4. **روی ۲۱۶:** `systemctl stop zareian-mathapp`؛ `dropdb -p 5440 zareian_mathapp && createdb -p 5440 -O zareian_app zareian_mathapp` (با
|
||||||
|
`sudo -u postgres`)؛ `pg_restore -p 5440 -d zareian_mathapp --no-owner --role=zareian_app --exit-on-error`؛ شمارشِ `accounts_user`،
|
||||||
|
`content_question`، `payment_order`، `payment_paymentevent`، `content_useranswer` با همان پرسش روی ۲۴۹ یکی باشد؛ `systemctl start
|
||||||
|
zareian-mathapp`؛ health با `X-Internal-Key` بیهشدار. دادههای تستِ ۲۰۲۶-۱۰-۱۷ روی ۲۱۶ با همین قدم پاک میشوند.
|
||||||
|
5. **سینیور origin را در آروان عوض میکند.** بعد از بیرون: `https://mathapi.tadrisino2.ir/api/health` ok؛ در
|
||||||
|
`/var/log/nginx/access.log`ِ ۲۱۶ IPهای آروان دیده شوند؛ ورود در Chrome (روشِ OTPِ ادمین، بخشِ «ورود بیپیامک») و یک آزمون؛ کاربرِ
|
||||||
|
اندروید (WebView و `pay.html`) هم یک بار.
|
||||||
|
6. **بکاپِ تازه:** `sudo systemctl start tadrisino-pgbackrest-full` و بعد `tadrisino-offsite-sync`؛ `sudo -u postgres tadrisino-restore-check`.
|
||||||
|
7. **۲۴۹ را کنار بگذار (پاک نکن):** `sudo systemctl disable zareian-mathapp` روی ۲۴۹؛ اسلات را بردار تا WAL روی دیسکِ مشترک جمع نشود:
|
||||||
|
`sudo -u postgres psql -p 5440 -c "select pg_drop_replication_slot('zareian_247')"`؛ روی ۲۴۷ `systemctl disable --now zareian-pg-tunnel
|
||||||
|
zareian-pg-wal zareian-pg-basebackup.timer zareian-pg-export-events.timer zareian-applog-pull.timer`. دیتابیسِ ۲۴۹ یک هفته برای برگشت میماند.
|
||||||
|
8. **برگشت (اگر قدمِ ۵ خراب شد):** origin در آروان دوباره ۲۴۹، و روی ۲۴۹ `systemctl start zareian-mathapp`. فقط تا وقتی درست است که ۲۱۶
|
||||||
|
نوشتنِ واقعی (پرداخت، ثبتنام) نگرفته؛ بعد از آن اول dumpِ ۲۱۶ به ۲۴۹.
|
||||||
|
9. **اسناد:** CLAUDE.md (سرورِ زنده = ۲۱۶؛ رویهی دیپلو با `zareian@78.38.35.216` و `sudo -u mathapp ./deploy.sh`)، این بخش، و
|
||||||
|
`ops/nginx/` با مسیرهای `/home/mathapp` (۲۱۶ نسخهی sedشده را دارد).
|
||||||
|
|
||||||
|
### بعد از سوئیچ
|
||||||
|
|
||||||
|
- یک هفته بعد، با تأییدِ صاحبِ محصول: پاک کردنِ بکاپهای رمزنگارینشدهی ۲۴۹ روی ۲۴۷ (`~/zareian-pgbackup`، یونیتهای `zareian-pg-*`)
|
||||||
|
و حذفِ `_slot_state`/`OFFSITE_WAL_SLOT` از `config/health.py` و `settings.py`.
|
||||||
|
- لاگِ اپ: آرشیوِ شبانه روی ۲۱۶ هست (`zareian-applog-archive.timer`) ولی هنوز به ۲۴۷ نمیرود؛ راهِ پیشنهادی: افزودنِ
|
||||||
|
`logs/archive` به `tadrisino-offsite-sync` (همان rrsync، پوشهی `app-logs/`).
|
||||||
|
- ورود با کلیدِ SSH برای `zareian` (صاحبِ محصول خواسته فعلاً بماند؛ روی لپتاپ `ssh-keygen -t ed25519` و `ssh-copy-id`)، بعد
|
||||||
|
`PasswordAuthentication no` با `Match Address 192.168.200.0/24` برای ورودِ با رمز از شبکهی داخلی.
|
||||||
|
- توکنِ فقطخواندنیِ Gitea برای `deploy.sh` روی ۲۱۶ (بعد از private شدنِ مخزن، `git fetch` بیرمز دیگر کار نمیکند).
|
||||||
|
- `CORS_ALLOWED_ORIGINS` در `.env`ِ سرور هنوز `localhost:5174` دارد؛ حذفش.
|
||||||
|
- برنامهی صاحبِ محصول: مرورِ کاملِ یک فهرستِ امنیتیِ استاندارد (OWASP Top 10 / API Top 10 برای اپ، CIS Benchmark برای سرور، همراهِ
|
||||||
|
`docs/security-before-production.md`)، بعد لودتست روی ۲۱۶ (`tools/loadtest/README.md`؛ با هستهی بیشتر: ۸ worker و `DB_POOL_MAX_SIZE` 6).
|
||||||
|
|
||||||
|
### Qdrant
|
||||||
|
|
||||||
|
**وضعِ فعلی Qdrant:** بردارهای RAG روی `https://qdrant.tadrisino2.ir` هستند (پشت ArvanCloud؛ سرورِ اصلی از بیرون پیدا
|
||||||
|
نیست و در فهرستِ سرورهای ما نیست. `rag_catalog` آن را سرورِ تیمِ دیگر میداند). نسخهٔ 1.17.0، ۳۴ collection، حدود
|
||||||
|
۱۰۸۸۰ نقطه، همه با بُعد ۱۵۳۶ و Cosine.
|
||||||
|
|
||||||
|
**هدف:** همهٔ کتابهای ریاضیای که در Qdrant هست (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته؛ پایههای سوم تا دوازدهم) روی یک
|
||||||
|
Qdrant اختصاصی ببریم و اپ را به آن وصل کنیم، بدون ingest دوباره، چون ingest هزینهٔ GapGPT دارد.
|
||||||
|
|
||||||
|
**کدام collectionها (۱۵ تا، حدود ۵۶۸۰ نقطه):**
|
||||||
|
- ریاضی سوم، چهارم، پنجم، ششم، هفتم، هشتم، نهم، دهم.
|
||||||
|
- حسابان یازدهم، دوازدهم.
|
||||||
|
- هندسه دهم، یازدهم، دوازدهم.
|
||||||
|
- آمار و احتمال یازدهم.
|
||||||
|
- گسسته دوازدهم.
|
||||||
|
|
||||||
|
ریاضیِ سوم، ششم و هفتم را تیمِ دیگر ingest کرده، و صاحبِ محصول خواسته هر سه منتقل شوند. بقیهٔ collectionها (علوم، فارسی،
|
||||||
|
مطالعات اجتماعی، هدیهٔ آسمان، پیامهای آسمان) در این برنامه نیستند.
|
||||||
|
|
||||||
|
**اصولِ انتقال:**
|
||||||
|
- از هر collection یک snapshot بگیرید، فایلِ آن را منتقل و روی Qdrant جدید بارگذاری کنید (بدون فراخوانیِ OpenAI).
|
||||||
|
- بعد از انتقال: تعدادِ نقاط، بُعد و Cosine یکی باشند؛ یک بردارِ ذخیرهشده را بهعنوانِ query بدهید و همان نقطه بالا بیاید.
|
||||||
|
- `/api/rag/ingest` هنوز روی Qdrant فعلی مینویسد. در بازهٔ کپی ingest متوقف شود، یا بعد از سوئیچ نقاطِ تازه دوباره همگام شوند.
|
||||||
|
- `QDRANT_URL` و `QDRANT_API_KEY` را فقط **بعد** از تأییدِ کپی عوض کنید؛ زودتر عوض شود، تیوتور جوابِ ناقص میدهد.
|
||||||
|
|
||||||
|
**باز:** محلِ سرورِ اصلیِ فعلیِ Qdrant (ادعای «روی سرورِ تیمِ دیگر» هنوز از روی سرور تأیید نشده). سرورِ جدید فعلاً به همان
|
||||||
|
`qdrant.tadrisino2.ir` وصل است.
|
||||||
|
|
||||||
## لاگها
|
## لاگها
|
||||||
|
|
||||||
هر درخواستِ `/api/` بهصورتِ یک شیءِ JSON در هر خط لاگ میشود
|
هر درخواستِ `/api/` بهصورتِ یک شیءِ JSON در هر خط لاگ میشود
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# روی ۲۴۹، هر شب (zareian-applog-archive.timer). app.log را به logs/archive/ میبرد و فشرده میکند.
|
# روی سرورِ اپ، هر شب (zareian-applog-archive.timer). app.log را به logs/archive/ میبرد و فشرده میکند.
|
||||||
# برنامه با LOG_FILE_ROTATION=external از WatchedFileHandler استفاده میکند و پس از جابهجایی خودش app.log تازه
|
# برنامه با LOG_FILE_ROTATION=external از WatchedFileHandler استفاده میکند و پس از جابهجایی خودش app.log تازه
|
||||||
# میسازد؛ خطی که در همان لحظه نوشته شود در فایلِ جابهجاشده میماند، گم نمیشود.
|
# میسازد؛ خطی که در همان لحظه نوشته شود در فایلِ جابهجاشده میماند، گم نمیشود.
|
||||||
# اینجا فقط ۳۰ روز میماند (دیسکِ ۲۴۹ پر است) و فقط فایلی پاک میشود که ۲۴۷ گرفتنش را تأیید کرده
|
# اینجا فقط ۳۰ روز میماند (دیسکِ ۲۴۹ پر است) و فقط فایلی پاک میشود که ۲۴۷ گرفتنش را تأیید کرده
|
||||||
# (`.shipped`)؛ نسخهی همیشگی روی ۲۴۷ است (pull-app-logs.sh). فقط زیرِ ~/mathapp-v2/logs مینویسد.
|
# (`.shipped`)؛ نسخهی همیشگی روی ۲۴۷ است (pull-app-logs.sh). فقط زیرِ ~/mathapp-v2/logs مینویسد.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
LOGS=/home/administrator/mathapp-v2/logs
|
# پوشهی پروژه از جای خودِ اسکریپت (ops/applogs/ → دو پوشه بالاتر)، تا روی هر سروری با هر کاربری درست باشد.
|
||||||
|
LOGS="$(cd "$(dirname "$0")/../.." && pwd)/logs"
|
||||||
ARCH="$LOGS/archive"
|
ARCH="$LOGS/archive"
|
||||||
mkdir -p "$ARCH"
|
mkdir -p "$ARCH"
|
||||||
chmod 750 "$ARCH"
|
chmod 750 "$ARCH"
|
||||||
|
|||||||
13
ops/server216/10-tadrisino.conf
Normal file
13
ops/server216/10-tadrisino.conf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# تدریسینو: سختسازیِ SSH. فقط zareian (صاحبِ محصول) و mathapp-admin (حسابِ مشترکِ سینیور و صاحبِ شرکت) وارد میشوند.
|
||||||
|
# ورود با رمز هنوز باز است (تا رفتن به کلید SSH)؛ fail2ban جلوی حدس زدن را میگیرد.
|
||||||
|
PermitRootLogin no
|
||||||
|
AllowUsers zareian mathapp-admin
|
||||||
|
MaxAuthTries 4
|
||||||
|
MaxSessions 10
|
||||||
|
LoginGraceTime 2m
|
||||||
|
X11Forwarding no
|
||||||
|
AllowAgentForwarding no
|
||||||
|
AllowTcpForwarding no
|
||||||
|
PermitTunnel no
|
||||||
|
ClientAliveInterval 300
|
||||||
|
ClientAliveCountMax 2
|
||||||
18
ops/server216/80-tadrisino-hardening.conf
Normal file
18
ops/server216/80-tadrisino-hardening.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# تدریسینو: تنظیماتِ شبکهی استاندارد (CIS). سرور مسیریاب نیست.
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv6.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv4.conf.default.send_redirects = 0
|
||||||
|
net.ipv4.conf.all.accept_source_route = 0
|
||||||
|
net.ipv6.conf.all.accept_source_route = 0
|
||||||
|
net.ipv4.conf.all.log_martians = 1
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||||
|
kernel.kptr_restrict = 2
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
4
ops/server216/backup.conf
Normal file
4
ops/server216/backup.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# تدریسینو: آرشیوِ پیوستهی WAL با pgBackRest (رمزنگاریشده). archive_timeout یعنی حداکثر ۶۰ ثانیه تغییرِ آرشیونشده.
|
||||||
|
archive_mode = on
|
||||||
|
archive_command = 'pgbackrest --stanza=zareian archive-push %p'
|
||||||
|
archive_timeout = 60
|
||||||
20
ops/server216/gunicorn-mathapp.service
Normal file
20
ops/server216/gunicorn-mathapp.service
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Gunicorn MathApp (tadrisino API)
|
||||||
|
After=network.target postgresql@16-zareian.service zareian-redis.service
|
||||||
|
Wants=postgresql@16-zareian.service zareian-redis.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=mathapp
|
||||||
|
Group=www-data
|
||||||
|
WorkingDirectory=/home/mathapp/mathapp-v2
|
||||||
|
EnvironmentFile=/home/mathapp/mathapp-v2/.env
|
||||||
|
# /run/gunicorn بعد از هر ریبوت ساخته میشود (روی ۲۴۹ دستی ساخته شده بود و بعد از ریبوت نبود).
|
||||||
|
RuntimeDirectory=gunicorn
|
||||||
|
RuntimeDirectoryMode=0750
|
||||||
|
ExecStart=/home/mathapp/mathapp-v2/venv/bin/gunicorn --bind unix:/run/gunicorn/mathapp.sock config.wsgi:application
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
Alias=zareian-mathapp.service
|
||||||
17
ops/server216/pgbackrest.conf
Normal file
17
ops/server216/pgbackrest.conf
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# روی ۲۱۶: /etc/pgbackrest.conf (root:postgres 640). کلیدِ واقعی فقط روی سرور و در /etc/tadrisino-backup/RECOVERY-KEYS است.
|
||||||
|
# تدریسینو: بکاپِ رمزنگاریشده. مخزنِ محلی /var/lib/pgbackrest؛ نسخهی بیرونی با tadrisino-offsite-sync به ۲۴۷ (فقط فایلِ رمزشده).
|
||||||
|
[global]
|
||||||
|
repo1-path=/var/lib/pgbackrest
|
||||||
|
repo1-cipher-type=aes-256-cbc
|
||||||
|
repo1-cipher-pass=<RECOVERY-KEYS، خطِ کلیدِ ۱>
|
||||||
|
repo1-retention-full=2
|
||||||
|
compress-type=zst
|
||||||
|
process-max=2
|
||||||
|
start-fast=y
|
||||||
|
log-level-console=info
|
||||||
|
log-level-file=detail
|
||||||
|
|
||||||
|
[zareian]
|
||||||
|
pg1-path=/var/lib/postgresql/16/zareian
|
||||||
|
pg1-port=5440
|
||||||
|
pg1-socket-path=/var/run/postgresql
|
||||||
44
ops/server216/tadrisino-blocklist
Executable file
44
ops/server216/tadrisino-blocklist
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# تدریسینو: IPهای شناختهشدهی مخرب را پیش از اولین تلاش روی همهی پورتها میبندد.
|
||||||
|
# فهرست: FireHOL level1 (Spamhaus DROP/EDROP، DShield، Feodo، bogons)، روزی یک بار (tadrisino-blocklist.timer).
|
||||||
|
# شبکههای داخلی، رنجِ IPِ شرکت و رنجهای ArvanCloud (از snippetِ nginx) هرگز بسته نمیشوند.
|
||||||
|
# اگر دانلود نشد، آخرین فهرستِ سالم (/var/lib/tadrisino-blocklist/list) دوباره بار میشود.
|
||||||
|
set -euo pipefail
|
||||||
|
URL=https://iplists.firehol.org/files/firehol_level1.netset
|
||||||
|
DIR=/var/lib/tadrisino-blocklist; SET=tadrisino-block
|
||||||
|
mkdir -p "$DIR"
|
||||||
|
tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT
|
||||||
|
if curl -fsS -m 90 "$URL" -o "$tmp" && [ "$(grep -c '^[0-9]' "$tmp")" -gt 1000 ]; then
|
||||||
|
python3 - "$tmp" "$DIR/list" <<'PY'
|
||||||
|
import ipaddress, re, sys
|
||||||
|
src, dst = sys.argv[1], sys.argv[2]
|
||||||
|
keep = ['127.0.0.0/8','10.0.0.0/8','172.16.0.0/12','192.168.0.0/16','100.64.0.0/10','169.254.0.0/16','78.38.35.0/24']
|
||||||
|
try:
|
||||||
|
keep += re.findall(r'^allow\s+([0-9./]+);', open('/etc/nginx/snippets/zareian-arvan-only.conf').read(), re.M)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
keep = [ipaddress.ip_network(k) for k in keep if ':' not in k]
|
||||||
|
out, dropped = [], 0
|
||||||
|
for line in open(src):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line[0] == '#':
|
||||||
|
continue
|
||||||
|
n = ipaddress.ip_network(line, strict=False)
|
||||||
|
if any(n.overlaps(k) for k in keep):
|
||||||
|
dropped += 1
|
||||||
|
continue
|
||||||
|
out.append(str(n))
|
||||||
|
open(dst, 'w').write('\n'.join(out) + '\n')
|
||||||
|
print(f'{len(out)} networks, {dropped} skipped (internal/company/Arvan)')
|
||||||
|
PY
|
||||||
|
else
|
||||||
|
echo "download failed; reloading last good list" >&2
|
||||||
|
fi
|
||||||
|
[ -s "$DIR/list" ] || { echo "no list available" >&2; exit 1; }
|
||||||
|
ipset create "$SET" hash:net maxelem 262144 -exist
|
||||||
|
ipset create "$SET-new" hash:net maxelem 262144 -exist
|
||||||
|
ipset flush "$SET-new"
|
||||||
|
sed "s/^/add $SET-new /" "$DIR/list" | ipset restore -exist
|
||||||
|
ipset swap "$SET-new" "$SET"; ipset destroy "$SET-new"
|
||||||
|
iptables -C INPUT -m set --match-set "$SET" src -j DROP 2>/dev/null || iptables -I INPUT 1 -m set --match-set "$SET" src -j DROP
|
||||||
|
echo "loaded $(ipset list "$SET" -t | sed -n 's/^Number of entries: //p') entries into $SET"
|
||||||
11
ops/server216/tadrisino-blocklist.service
Normal file
11
ops/server216/tadrisino-blocklist.service
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: block known-malicious IPs (FireHOL level1)
|
||||||
|
After=network-online.target ufw.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/tadrisino-blocklist
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
10
ops/server216/tadrisino-blocklist.timer
Normal file
10
ops/server216/tadrisino-blocklist.timer
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: refresh malicious-IP blocklist daily
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 05:20:00
|
||||||
|
RandomizedDelaySec=30m
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
19
ops/server216/tadrisino-export-payment-events
Executable file
19
ops/server216/tadrisino-export-payment-events
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# تدریسینو: دفترِ خرید (payment_paymentevent)، سفارشها و تراکنشها به CSV، هر ۶ ساعت. هر فایل پیش از نوشتهشدن روی دیسک
|
||||||
|
# با age برای کلیدِ /etc/tadrisino-backup/age-recipient.txt رمز میشود؛ متنِ خوانا هیچجا ذخیره نمیشود.
|
||||||
|
# بازکردن: age -d -i age-identity.txt payment-events-….csv.age (کلید در RECOVERY-KEYS). ۱۴ فایلِ آخرِ هر نوع میماند.
|
||||||
|
set -euo pipefail
|
||||||
|
OUT=/var/lib/tadrisino-backup/payment-log
|
||||||
|
REC=/etc/tadrisino-backup/age-recipient.txt
|
||||||
|
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
q() {
|
||||||
|
psql -p 5440 -d zareian_mathapp -v ON_ERROR_STOP=1 -Atqc "\\copy ($1) to stdout csv header" | age -R "$REC" -o "$OUT/$2-$stamp.csv.age"
|
||||||
|
}
|
||||||
|
q "select id, created_at, order_id, user_id, provider, event, detail from payment_paymentevent order by id" payment-events
|
||||||
|
q "select id, created_at, user_id, plan_id, plan_title, amount, status, provider, gateway_authority, subscription_id from payment_order order by id" orders
|
||||||
|
q "select id, created_at, order_id, gateway, gateway_ref_id, status, paid_at from payment_transaction order by id" transactions
|
||||||
|
chmod 640 "$OUT"/*-"$stamp".csv.age
|
||||||
|
for kind in payment-events orders transactions; do
|
||||||
|
ls -1t "$OUT/$kind"-*.csv.age | tail -n +15 | xargs -r rm -f --
|
||||||
|
done
|
||||||
|
echo "wrote $OUT/*-$stamp.csv.age"
|
||||||
8
ops/server216/tadrisino-export-payment-events.service
Normal file
8
ops/server216/tadrisino-export-payment-events.service
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: encrypted payment CSVs every 6 hours
|
||||||
|
After=postgresql@16-zareian.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=postgres
|
||||||
|
ExecStart=/usr/local/sbin/tadrisino-export-payment-events
|
||||||
9
ops/server216/tadrisino-export-payment-events.timer
Normal file
9
ops/server216/tadrisino-export-payment-events.timer
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: encrypted payment CSVs every 6 hours
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 01,07,13,19:15:00
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
11
ops/server216/tadrisino-offsite-sync
Executable file
11
ops/server216/tadrisino-offsite-sync
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# تدریسینو: نسخهی بیرونیِ بکاپ. فقط فایلهای رمزشده (مخزنِ pgBackRest با AES-256 و CSVهای age) به ۲۴۷ میروند.
|
||||||
|
# کلیدِ postgres روی ۲۴۷ با rrsync -wo محدود است: فقط نوشتن در ~/zareian-pgbackrest-216، نه خواندن، نه شِل.
|
||||||
|
# پس از موفقیتِ هر دو، زمانش در offsite-synced-at ثبت میشود و /api/health از روی آن عقبماندگی را میسنجد.
|
||||||
|
set -euo pipefail
|
||||||
|
R=administrator@192.168.200.167
|
||||||
|
SSH="ssh -i /var/lib/postgresql/.ssh/id_offsite -o BatchMode=yes -o ConnectTimeout=20"
|
||||||
|
rsync -a --delete -e "$SSH" /var/lib/pgbackrest/ "$R:repo/"
|
||||||
|
rsync -a --delete -e "$SSH" /var/lib/tadrisino-backup/payment-log/ "$R:payment-log/"
|
||||||
|
date -u +%s > /var/lib/tadrisino-backup/offsite-synced-at.tmp
|
||||||
|
mv /var/lib/tadrisino-backup/offsite-synced-at.tmp /var/lib/tadrisino-backup/offsite-synced-at
|
||||||
8
ops/server216/tadrisino-offsite-sync.service
Normal file
8
ops/server216/tadrisino-offsite-sync.service
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: copy encrypted backups to 247
|
||||||
|
After=postgresql@16-zareian.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=postgres
|
||||||
|
ExecStart=/usr/local/sbin/tadrisino-offsite-sync
|
||||||
9
ops/server216/tadrisino-offsite-sync.timer
Normal file
9
ops/server216/tadrisino-offsite-sync.timer
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: copy encrypted backups to 247
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=minutely
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
8
ops/server216/tadrisino-pgbackrest-diff.service
Normal file
8
ops/server216/tadrisino-pgbackrest-diff.service
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: daily differential encrypted backup
|
||||||
|
After=postgresql@16-zareian.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=postgres
|
||||||
|
ExecStart=/usr/bin/pgbackrest --stanza=zareian --type=diff backup
|
||||||
9
ops/server216/tadrisino-pgbackrest-diff.timer
Normal file
9
ops/server216/tadrisino-pgbackrest-diff.timer
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: daily differential encrypted backup
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* 03:40:00
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
8
ops/server216/tadrisino-pgbackrest-full.service
Normal file
8
ops/server216/tadrisino-pgbackrest-full.service
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: weekly full encrypted backup
|
||||||
|
After=postgresql@16-zareian.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=postgres
|
||||||
|
ExecStart=/usr/bin/pgbackrest --stanza=zareian --type=full backup
|
||||||
9
ops/server216/tadrisino-pgbackrest-full.timer
Normal file
9
ops/server216/tadrisino-pgbackrest-full.timer
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Tadrisino: weekly full encrypted backup
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=Sun *-*-* 03:00:00
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
26
ops/server216/tadrisino-restore-check
Executable file
26
ops/server216/tadrisino-restore-check
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# تدریسینو: آزمونِ بازیابی از بکاپِ رمزشده (ماهی یک بار، دستی: sudo -u postgres tadrisino-restore-check).
|
||||||
|
# آخرین بکاپ + همهی WALِ آرشیوشده را در پوشهی موقت بازیابی میکند، Postgresِ موقت را روی سوکتِ همان پوشه (بیپورتِ شبکه،
|
||||||
|
# archive_mode=off تا چیزی به مخزن ننویسد) بالا میآورد، شمارشها را کنارِ دیتابیسِ اصلی چاپ میکند و همه را پاک میکند.
|
||||||
|
set -euo pipefail
|
||||||
|
BIN=/usr/lib/postgresql/16/bin
|
||||||
|
work=$(mktemp -d /var/lib/postgresql/restore-check.XXXXXX)
|
||||||
|
trap '"$BIN/pg_ctl" -D "$work/data" -m immediate stop >/dev/null 2>&1 || true; rm -rf "$work"' EXIT
|
||||||
|
pgbackrest --stanza=zareian --pg1-path="$work/data" --type=default --archive-mode=off restore --log-level-console=warn
|
||||||
|
cat > "$work/data/postgresql.conf" <<CONF
|
||||||
|
listen_addresses = ''
|
||||||
|
port = 5499
|
||||||
|
unix_socket_directories = '$work'
|
||||||
|
archive_mode = off
|
||||||
|
shared_buffers = 128MB
|
||||||
|
CONF
|
||||||
|
echo 'local all all peer' > "$work/data/pg_hba.conf"
|
||||||
|
"$BIN/pg_ctl" -D "$work/data" -l "$work/log" -w -t 600 start >/dev/null
|
||||||
|
for _ in $(seq 120); do
|
||||||
|
[ "$(psql -h "$work" -p 5499 -d zareian_mathapp -Atc 'select pg_is_in_recovery()')" = f ] && break; sleep 1
|
||||||
|
done
|
||||||
|
Q="select 'users', count(*) from accounts_user union all select 'questions', count(*) from content_question
|
||||||
|
union all select 'orders', count(*) from payment_order union all select 'payment_events', count(*) from payment_paymentevent
|
||||||
|
union all select 'login_events', count(*) from accounts_loginevent"
|
||||||
|
echo "restored | live"
|
||||||
|
paste -d'|' <(psql -h "$work" -p 5499 -d zareian_mathapp -Atc "$Q") <(psql -p 5440 -d zareian_mathapp -Atc "$Q" | cut -d'|' -f2)
|
||||||
25
ops/server216/tadrisino.local
Normal file
25
ops/server216/tadrisino.local
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# تدریسینو: SSH در برابرِ رباتهای حدسِ رمز، پلهای تا آدمِ واقعی (اشتباهِ تایپی، کیبوردِ فارسی) گیر نکند.
|
||||||
|
# ۶ اشتباه در ۱۰ دقیقه → ۱۰ دقیقه بسته؛ هر بارِ تکرار دو برابر (۲۰، ۴۰، ...، حداکثر یک هفته).
|
||||||
|
# شبکهی داخلیِ شرکت (ادمینها، ۲۴۷، ۲۴۹) هرگز بسته نمیشود.
|
||||||
|
[DEFAULT]
|
||||||
|
ignoreip = 127.0.0.1/8 ::1 192.168.200.0/24
|
||||||
|
bantime.increment = true
|
||||||
|
bantime.factor = 2
|
||||||
|
bantime.maxtime = 1w
|
||||||
|
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
backend = systemd
|
||||||
|
mode = aggressive
|
||||||
|
maxretry = 6
|
||||||
|
findtime = 10m
|
||||||
|
bantime = 10m
|
||||||
|
|
||||||
|
# فقط رباتِ سمج: پنج بار بستهشدن در یک روز → یک هفته روی همهی پورتها.
|
||||||
|
[recidive]
|
||||||
|
enabled = true
|
||||||
|
logpath = /var/log/fail2ban.log
|
||||||
|
banaction = iptables-allports
|
||||||
|
findtime = 1d
|
||||||
|
maxretry = 5
|
||||||
|
bantime = 1w
|
||||||
13
ops/server216/zareian-redis.service
Normal file
13
ops/server216/zareian-redis.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Redis for zareian-mathapp only
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=redis
|
||||||
|
Group=redis
|
||||||
|
ExecStart=/usr/bin/redis-server /etc/zareian-redis.conf
|
||||||
|
Restart=on-failure
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
5
ops/server216/zareian.conf
Normal file
5
ops/server216/zareian.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# تدریسینو، سرور اختصاصی: فقط localhost. max_connections با workers × DB_POOL_MAX_SIZE + 6 در /api/health سنجیده میشود.
|
||||||
|
listen_addresses = '127.0.0.1'
|
||||||
|
max_connections = 60
|
||||||
|
shared_buffers = 1GB
|
||||||
|
effective_cache_size = 4GB
|
||||||
Reference in New Issue
Block a user