216 backups encrypted on 216: pgBackRest (AES-256) + age-encrypted payment CSVs, pushed write-only to 247 with rrsync; health watches pg_stat_archiver and OFFSITE_SYNC_STATUS_FILE; 216 scripts/configs in ops/server216; plaintext pg216 pipeline removed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 19:43:00 +03:30
parent dd98f16d12
commit 4cd859b5fa
37 changed files with 417 additions and 96 deletions

View File

@@ -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=
# ═══════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════
# ۲) دامنه‌ها — دو آدرس، بقیه از آن‌ها ساخته می‌شوند # ۲) دامنه‌ها — دو آدرس، بقیه از آن‌ها ساخته می‌شوند

View File

@@ -63,9 +63,9 @@ 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
`~/zareian-pgbackup-216/`, `~/.ssh/zareian_pgbackup*`, and the `zareian-pg-*` / `zareian-pg216-*` / live copy), `~/zareian-pgbackrest-216/` (216's backups, encrypted on 216; 247 never holds the key), our lines in
`zareian-applog-pull` units. It also runs other teams' services and its `~/.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` (`tadrisino`, new dedicated VM, not yet serving users)**, account `zareian` (sudo): the whole **`78.38.35.216` (`tadrisino`, new dedicated VM, not yet serving users)**, account `zareian` (sudo): the whole

View File

@@ -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',
} }

View File

@@ -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 را جابه‌جا و

View File

@@ -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):

View File

@@ -389,23 +389,32 @@ the servers»):
| | | | | |
|---|---| |---|---|
| ماشین | VMware، Ubuntu 24.04، ۸ هسته، ۱۲ گیگ رم، دیسکِ ۹۷ گیگ؛ hostname `tadrisino`، IPِ داخلی `192.168.200.163` (۲۴۹ = `.164`)، IPِ خروجی `78.38.35.228` | | ماشین | VMware، Ubuntu 24.04، ۸ هسته، ۱۲ گیگ رم، دیسکِ ۹۷ گیگ؛ hostname `tadrisino`، IPِ داخلی `192.168.200.163` (۲۴۹ = `.164`)، IPِ خروجی `78.38.35.228` |
| ورود | `ssh zareian@78.38.35.216` (sudo). `temp` حسابِ موقتِ ادمینِ شبکه است و حذف می‌شود. ورودِ root بسته، `AllowUsers zareian temp pgbackup@192.168.200.167`، forwarding بسته (`/etc/ssh/sshd_config.d/10-tadrisino.conf`) | | ورود | `ssh zareian@78.38.35.216` (sudo). `temp` حسابِ موقتِ ادمینِ شبکه است و حذف می‌شود. ورودِ root بسته، `AllowUsers zareian temp`، 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>` | | ضدِ ربات | ۱) `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>` |
| بکاپ | زنده روی ۲۴۷ (`~/zareian-pgbackup-216/`، یونیت‌های `zareian-pg216-*`، ابزارِ Postgres 16 بازشده در همان پوشه). ۲۴۷ با کاربرِ `pgbackup` (فقط کلید، فقط از `.167`، فقط `permitopen=127.0.0.1:5440`) تونل می‌زند؛ رمزِ `zareian_replicator` فقط در `~/.pgpass`ِ ۲۴۷، روی ۲۱۶ فقط هشِ SCRAM. اسلات `zareian_247`، `max_slot_wal_keep_size=5GB`. آزمونِ بازیابی: `PGB_ROOT=… PGB_BIN=… LD_LIBRARY_PATH=… restore-check.sh` (مقادیر در یونیتِ `zareian-pg216-basebackup`) | | بکاپ | **رمزنگاری روی همین سرور، ۲۴۷ فقط فایلِ رمزشده می‌بیند** (۲۴۷ مشترک است و هر کس `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` | | اپ | کاربرِ بی‌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` | | سرویس‌ها | `zareian-mathapp` (= `gunicorn-mathapp`، با `RuntimeDirectory`)، `zareian-redis` (6390)، `postgresql@16-zareian` (5440)، `nginx`، `zareian-applog-archive.timer` |
| رمزها | رمزِ Postgres و Redis روی این سرور تازه ساخته شده (`.pg_password`، `.redis_password` کنارِ `.env`)، نه کپیِ ۲۴۹ | | رمزها | رمزِ Postgres و Redis روی این سرور تازه ساخته شده (`.pg_password`، `.redis_password` کنارِ `.env`)، نه کپیِ ۲۴۹ |
| فایروال | ufw فقط 22/80/443. فایروالِ شبکه‌ی شرکت 443ِ ورودی را هنوز می‌بندد (80 باز است) | | فایروال | 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` از این سرور بسته‌اند | | بسته‌ها | 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 و بی‌هشدار (`offsiteBackup` ok، عقب‌ماندگی ۰)، آزمونِ بازیابی از بکاپِ ۲۴۷ با همان شمارش‌ها، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و سنجیده: `/api/health` با همه‌ی جزئیات ok، آزمونِ بازیابی از بکاپِ رمزشده با همان شمارش‌ها، فایل‌های ۲۴۷ رمزشده (`Salted__`،
`age-encryption.org/v1`)، کلیدِ ۲۱۶ روی ۲۴۷ نه می‌خواند نه شِل می‌گیرد، کپیِ ۲۴۷ با `rsync --checksum` عینِ مخزنِ محلی، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و
بالا آمدنِ خودکارِ همه‌ی سرویس‌ها، درخواستِ مستقیم به IP ۴۰۳ (nginx IPِ واقعی را می‌بیند). نسنجیده: هیچ درخواستی از مسیرِ ArvanCloud، بالا آمدنِ خودکارِ همه‌ی سرویس‌ها، درخواستِ مستقیم به IP ۴۰۳ (nginx IPِ واقعی را می‌بیند). نسنجیده: هیچ درخواستی از مسیرِ ArvanCloud،
پیامک، پرداخت و تیوتورِ زنده. پیامک، پرداخت و تیوتورِ زنده.
**سوئیچ (ترتیب):** ۱) 443ِ ورودی باز شود. ۲) روی ۲۴۹ `zareian-mathapp` متوقف (پیامِ نگه‌داری در ArvanCloud)، `pg_dump -Fc`، **سوئیچ (ترتیب):** ۱) 443ِ ورودی باز شود. ۲) روی ۲۴۹ `zareian-mathapp` متوقف (پیامِ نگه‌داری در ArvanCloud)، `pg_dump -Fc`،
بازیابی روی سرورِ جدید در دیتابیسِ خالی‌شده، مقایسه‌ی تعدادِ ردیف‌ها. ۳) originِ هر دو دامنه در ArvanCloud به `78.38.35.216`. بازیابی روی سرورِ جدید در دیتابیسِ خالی‌شده، مقایسه‌ی تعدادِ ردیف‌ها. ۳) originِ هر دو دامنه در ArvanCloud به `78.38.35.216`.
۴) health و یک ورود و یک آزمون از بیرون. ۵) روی ۲۴۷ یونیت‌های بکاپِ ۲۴۹ (`zareian-pg-*`) متوقف، `zareian-pg216-basebackup` ۴) health و یک ورود و یک آزمون از بیرون. ۵) `tadrisino-pgbackrest-full` یک بار دستی (بعد از بازیابیِ دیتابیسِ نهایی)؛ روی ۲۴۷
یک بار دستی (بعد از بازیابیِ دیتابیسِ نهایی)، و `pull-app-logs.sh` به آدرسِ جدید. ۶) CLAUDE.md و این سند. یونیت‌های بکاپِ ۲۴۹ (`zareian-pg-*`) متوقف و، با تأییدِ صاحبِ محصول، بکاپ‌های رمزنگاری‌نشده‌ی ۲۴۹ (`~/zareian-pgbackup`) پاک؛
`pull-app-logs.sh` به آدرسِ جدید؛ در کدِ health، `_slot_state` و `OFFSITE_WAL_SLOT` حذف. ۶) CLAUDE.md و این سند.
**باز، بعد از سوئیچ:** ورود با کلیدِ SSH برای `zareian` (صاحبِ محصول روی لپ‌تاپ `ssh-keygen -t ed25519` و `ssh-copy-id`)، بعد
`PasswordAuthentication no` با `Match Address 192.168.200.0/24` برای ورودِ با رمز از شبکه‌ی داخلی؛ private کردنِ مخزن در Gitea
(الان بی‌رمز از اینترنت خواندنی است) و توکنِ فقط‌خواندنی برای `deploy.sh`.
تا سوئیچ، `deploy.sh` روی هر دو سرور اجرا شود تا کدِ دو طرف یکی بماند. تا سوئیچ، `deploy.sh` روی هر دو سرور اجرا شود تا کدِ دو طرف یکی بماند.
### Qdrant ### Qdrant

View File

@@ -4,10 +4,8 @@
# --checkpoint=spread و --max-rate بار روی ۲۴۹ را کم نگه می‌دارند (آن‌جا سرورِ اصلی است). # --checkpoint=spread و --max-rate بار روی ۲۴۹ را کم نگه می‌دارند (آن‌جا سرورِ اصلی است).
# نسخه تا pg_verifybackup قبولش نکند جای قبلی‌ها را نمی‌گیرد و چیزی پاک نمی‌شود. # نسخه تا pg_verifybackup قبولش نکند جای قبلی‌ها را نمی‌گیرد و چیزی پاک نمی‌شود.
set -euo pipefail set -euo pipefail
# ROOT/BIN/PORT از یونیت می‌آیند؛ پیش‌فرض‌ها همان بکاپِ ۲۴۹ است (یونیت‌های zareian-pg216-* برای سرورِ جدید). ROOT=/home/administrator/zareian-pgbackup
ROOT="${PGB_ROOT:-/home/administrator/zareian-pgbackup}" BIN=/usr/lib/postgresql/14/bin
BIN="${PGB_BIN:-/usr/lib/postgresql/14/bin}"
PORT="${PGB_PORT:-15440}"
KEEP=7 KEEP=7
cd "$ROOT" cd "$ROOT"
@@ -16,7 +14,7 @@ ts=$(date +%Y%m%d-%H%M%S)
rm -rf base/.inprogress-* rm -rf base/.inprogress-*
tmp="base/.inprogress-$ts" tmp="base/.inprogress-$ts"
"$BIN/pg_basebackup" -h 127.0.0.1 -p "$PORT" -U zareian_replicator --no-password \ "$BIN/pg_basebackup" -h 127.0.0.1 -p 15440 -U zareian_replicator --no-password \
-D "$tmp" -Fp -X stream --checkpoint=spread --max-rate=20M -D "$tmp" -Fp -X stream --checkpoint=spread --max-rate=20M
"$BIN/pg_verifybackup" "$tmp" "$BIN/pg_verifybackup" "$tmp"
mv "$tmp" "base/$ts" mv "$tmp" "base/$ts"

View File

@@ -6,9 +6,8 @@
# خروجی: payment-log/payment-events-<UTC>.csv، orders-…csv، transactions-…csv؛ ۱۴ فایلِ آخرِ هر نوع می‌ماند. # خروجی: payment-log/payment-events-<UTC>.csv، orders-…csv، transactions-…csv؛ ۱۴ فایلِ آخرِ هر نوع می‌ماند.
# توجه: نسخه‌ی کامل تا چند دقیقه‌ی WAL عقب‌تر از ۲۴۹ می‌شود، ولی WALِ رسیده همه اعمال می‌شود. # توجه: نسخه‌ی کامل تا چند دقیقه‌ی WAL عقب‌تر از ۲۴۹ می‌شود، ولی WALِ رسیده همه اعمال می‌شود.
set -euo pipefail set -euo pipefail
# ROOT/BIN از یونیت می‌آیند؛ پیش‌فرض‌ها همان بکاپِ ۲۴۹ است (یونیت‌های zareian-pg216-* برای سرورِ جدید). ROOT=/home/administrator/zareian-pgbackup
ROOT="${PGB_ROOT:-/home/administrator/zareian-pgbackup}" BIN=/usr/lib/postgresql/14/bin
BIN="${PGB_BIN:-/usr/lib/postgresql/14/bin}"
OUT="$ROOT/payment-log" OUT="$ROOT/payment-log"
mkdir -p "$OUT" mkdir -p "$OUT"

View File

@@ -6,9 +6,8 @@
# restore-check.sh '2026-10-10 16:30:00+03:30' # restore-check.sh '2026-10-10 16:30:00+03:30'
# با KEEP=1 پوشه و Postgresِ موقت می‌مانند (برای بازیابیِ واقعی: از آن pg_dump بگیرید، بعد pg_ctl stop و rm). # با KEEP=1 پوشه و Postgresِ موقت می‌مانند (برای بازیابیِ واقعی: از آن pg_dump بگیرید، بعد pg_ctl stop و rm).
set -euo pipefail set -euo pipefail
# ROOT/BIN از یونیت می‌آیند؛ پیش‌فرض‌ها همان بکاپِ ۲۴۹ است (یونیت‌های zareian-pg216-* برای سرورِ جدید). ROOT=/home/administrator/zareian-pgbackup
ROOT="${PGB_ROOT:-/home/administrator/zareian-pgbackup}" BIN=/usr/lib/postgresql/14/bin
BIN="${PGB_BIN:-/usr/lib/postgresql/14/bin}"
TARGET="${1:-}" TARGET="${1:-}"
latest=$(ls -1d "$ROOT"/base/2* | tail -1) latest=$(ls -1d "$ROOT"/base/2* | tail -1)

View File

@@ -1,11 +0,0 @@
# روی سرورِ بکاپ (۲۴۷). نسخه‌ی کاملِ روزانه از ۲۱۶؛ همان basebackup.sh با مسیر و پورتِ ۲۱۶.
[Unit]
Description=Zareian MathApp: daily base backup from 216
Requires=zareian-pg216-tunnel.service
After=zareian-pg216-tunnel.service
[Service]
Type=oneshot
User=administrator
Environment=PGB_ROOT=/home/administrator/zareian-pgbackup-216 PGB_BIN=/home/administrator/zareian-pgbackup-216/pg16/root/usr/lib/postgresql/16/bin PGB_PORT=15441 LD_LIBRARY_PATH=/home/administrator/zareian-pgbackup-216/pg16/root/usr/lib/x86_64-linux-gnu
ExecStart=/home/administrator/zareian-pgbackup-216/bin/basebackup.sh

View File

@@ -1,9 +0,0 @@
[Unit]
Description=Zareian MathApp: daily base backup from 216
[Timer]
OnCalendar=*-*-* 04:50:00
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -1,8 +0,0 @@
[Unit]
Description=Zareian MathApp: export payment events from the 216 backup copy to CSV
[Service]
Type=oneshot
User=administrator
Environment=PGB_ROOT=/home/administrator/zareian-pgbackup-216 PGB_BIN=/home/administrator/zareian-pgbackup-216/pg16/root/usr/lib/postgresql/16/bin LD_LIBRARY_PATH=/home/administrator/zareian-pgbackup-216/pg16/root/usr/lib/x86_64-linux-gnu
ExecStart=/home/administrator/zareian-pgbackup-216/bin/export-payment-events.sh

View File

@@ -1,9 +0,0 @@
[Unit]
Description=Zareian MathApp: payment events CSV from 216 every 6 hours
[Timer]
OnCalendar=*-*-* 01,07,13,19:35:00
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -1,20 +0,0 @@
# روی سرورِ بکاپ (۲۴۷). تونلِ SSH از 127.0.0.1:15441ِ این‌جا به Postgresِ سرورِ اختصاصی (۲۱۶، 127.0.0.1:5440).
# طرفِ ۲۱۶ کاربرِ سیستمیِ pgbackup است: فقط همین کلید، فقط از 192.168.200.167، بی‌شِل، و در authorized_keys
# permitopen="127.0.0.1:5440" (هیچ پورتِ دیگری). رمزِ zareian_replicator فقط در ~/.pgpassِ ۲۴۷ است؛ ۲۱۶ فقط هشِ SCRAM آن را دارد.
# (forwardingِ سوکت ممکن نیست: AllowTcpForwarding no در sshd آن را هم می‌بندد.)
[Unit]
Description=Zareian MathApp: SSH tunnel to the app Postgres on 216
After=network-online.target
Wants=network-online.target
[Service]
User=administrator
ExecStart=/usr/bin/ssh -N -i /home/administrator/.ssh/zareian_pgbackup_216 \
-o BatchMode=yes -o ExitOnForwardFailure=yes \
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
-L 127.0.0.1:15441:127.0.0.1:5440 pgbackup@192.168.200.163
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -1,19 +0,0 @@
# روی سرورِ بکاپ (۲۴۷). هر تغییرِ دیتابیسِ ۲۱۶ را لحظه‌ای در ~/zareian-pgbackup-216/wal می‌نویسد.
# ابزارهای Postgres 16 از مخزنِ رسمی در ~/zareian-pgbackup-216/pg16 باز شده‌اند (نصبِ سیستمی نیست؛ ۲۴۷ فقط 14 دارد).
# اسلاتِ zareian_247 روی ۲۱۶، سقف max_slot_wal_keep_size=5GB. اپ منتظرِ این‌جا نمی‌ماند.
[Unit]
Description=Zareian MathApp: live WAL stream from 216
After=zareian-pg216-tunnel.service
BindsTo=zareian-pg216-tunnel.service
[Service]
User=administrator
Environment=LD_LIBRARY_PATH=/home/administrator/zareian-pgbackup-216/pg16/root/usr/lib/x86_64-linux-gnu
ExecStart=/home/administrator/zareian-pgbackup-216/pg16/root/usr/lib/postgresql/16/bin/pg_receivewal -h 127.0.0.1 -p 15441 -U zareian_replicator \
--no-password --slot=zareian_247 --synchronous --no-loop \
-D /home/administrator/zareian-pgbackup-216/wal
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,13 @@
# تدریسینو: سخت‌سازیِ SSH. فقط کاربرهای نام‌برده وارد می‌شوند؛ temp تا حذفش به دستِ ادمینِ شبکه.
# ورود با رمز هنوز باز است (تا رفتن به کلید SSH)؛ fail2ban جلوی حدس زدن را می‌گیرد.
PermitRootLogin no
AllowUsers zareian temp
MaxAuthTries 4
MaxSessions 10
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
ClientAliveInterval 300
ClientAliveCountMax 2

View 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

View File

@@ -0,0 +1,4 @@
# تدریسینو: آرشیوِ پیوسته‌ی WAL با pgBackRest (رمزنگاری‌شده). archive_timeout یعنی حداکثر ۶۰ ثانیه تغییرِ آرشیونشده.
archive_mode = on
archive_command = 'pgbackrest --stanza=zareian archive-push %p'
archive_timeout = 60

View 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

View 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

View 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"

View 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

View 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

View 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"

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Tadrisino: copy encrypted backups to 247
[Timer]
OnCalendar=minutely
Persistent=true
[Install]
WantedBy=timers.target

View 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

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Tadrisino: daily differential encrypted backup
[Timer]
OnCalendar=*-*-* 03:40:00
Persistent=true
[Install]
WantedBy=timers.target

View 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

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Tadrisino: weekly full encrypted backup
[Timer]
OnCalendar=Sun *-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target

View 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)

View 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

View 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

View 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