From 4cd859b5fa9e0aab2d856e8e43d535f37183a2b5 Mon Sep 17 00:00:00 2001 From: Zareian Date: Sat, 17 Oct 2026 19:43:00 +0330 Subject: [PATCH] 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 --- .env.example | 3 ++ CLAUDE.md | 6 +-- config/health.py | 45 +++++++++++++++++++ config/settings.py | 6 +++ config/tests_offsite_backup.py | 36 ++++++++++++++- docs/operations.md | 19 +++++--- ops/pgbackup/basebackup.sh | 8 ++-- ops/pgbackup/export-payment-events.sh | 5 +-- ops/pgbackup/restore-check.sh | 5 +-- ops/pgbackup/zareian-pg216-basebackup.service | 11 ----- ops/pgbackup/zareian-pg216-basebackup.timer | 9 ---- .../zareian-pg216-export-events.service | 8 ---- .../zareian-pg216-export-events.timer | 9 ---- ops/pgbackup/zareian-pg216-tunnel.service | 20 --------- ops/pgbackup/zareian-pg216-wal.service | 19 -------- ops/server216/10-tadrisino.conf | 13 ++++++ ops/server216/80-tadrisino-hardening.conf | 18 ++++++++ ops/server216/backup.conf | 4 ++ ops/server216/gunicorn-mathapp.service | 20 +++++++++ ops/server216/pgbackrest.conf | 17 +++++++ ops/server216/tadrisino-blocklist | 44 ++++++++++++++++++ ops/server216/tadrisino-blocklist.service | 11 +++++ ops/server216/tadrisino-blocklist.timer | 10 +++++ ops/server216/tadrisino-export-payment-events | 19 ++++++++ .../tadrisino-export-payment-events.service | 8 ++++ .../tadrisino-export-payment-events.timer | 9 ++++ ops/server216/tadrisino-offsite-sync | 11 +++++ ops/server216/tadrisino-offsite-sync.service | 8 ++++ ops/server216/tadrisino-offsite-sync.timer | 9 ++++ .../tadrisino-pgbackrest-diff.service | 8 ++++ ops/server216/tadrisino-pgbackrest-diff.timer | 9 ++++ .../tadrisino-pgbackrest-full.service | 8 ++++ ops/server216/tadrisino-pgbackrest-full.timer | 9 ++++ ops/server216/tadrisino-restore-check | 26 +++++++++++ ops/server216/tadrisino.local | 25 +++++++++++ ops/server216/zareian-redis.service | 13 ++++++ ops/server216/zareian.conf | 5 +++ 37 files changed, 417 insertions(+), 96 deletions(-) delete mode 100644 ops/pgbackup/zareian-pg216-basebackup.service delete mode 100644 ops/pgbackup/zareian-pg216-basebackup.timer delete mode 100644 ops/pgbackup/zareian-pg216-export-events.service delete mode 100644 ops/pgbackup/zareian-pg216-export-events.timer delete mode 100644 ops/pgbackup/zareian-pg216-tunnel.service delete mode 100644 ops/pgbackup/zareian-pg216-wal.service create mode 100644 ops/server216/10-tadrisino.conf create mode 100644 ops/server216/80-tadrisino-hardening.conf create mode 100644 ops/server216/backup.conf create mode 100644 ops/server216/gunicorn-mathapp.service create mode 100644 ops/server216/pgbackrest.conf create mode 100755 ops/server216/tadrisino-blocklist create mode 100644 ops/server216/tadrisino-blocklist.service create mode 100644 ops/server216/tadrisino-blocklist.timer create mode 100755 ops/server216/tadrisino-export-payment-events create mode 100644 ops/server216/tadrisino-export-payment-events.service create mode 100644 ops/server216/tadrisino-export-payment-events.timer create mode 100755 ops/server216/tadrisino-offsite-sync create mode 100644 ops/server216/tadrisino-offsite-sync.service create mode 100644 ops/server216/tadrisino-offsite-sync.timer create mode 100644 ops/server216/tadrisino-pgbackrest-diff.service create mode 100644 ops/server216/tadrisino-pgbackrest-diff.timer create mode 100644 ops/server216/tadrisino-pgbackrest-full.service create mode 100644 ops/server216/tadrisino-pgbackrest-full.timer create mode 100755 ops/server216/tadrisino-restore-check create mode 100644 ops/server216/tadrisino.local create mode 100644 ops/server216/zareian-redis.service create mode 100644 ops/server216/zareian.conf diff --git a/.env.example b/.env.example index 64b50a3..9e40831 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,9 @@ DB_PORT=5432 # اسلاتِ تکثیری که سرورِ بکاپ از آن کپیِ زنده می‌گیرد (ops/pgbackup/). روی سرور: zareian_247. # خالی روی Postgres = کپیِ بیرونی نداریم و /api/health هشدار می‌دهد. روی لپ‌تاپ خالی. OFFSITE_WAL_SLOT= +# سرورِ اختصاصی (۲۱۶): فایلی که tadrisino-offsite-sync زمانِ آخرین ارسالِ بکاپِ رمزشده به ۲۴۷ را در آن می‌نویسد. +# روی ۲۱۶: /var/lib/tadrisino-backup/offsite-synced-at (بر OFFSITE_WAL_SLOT مقدم است). روی لپ‌تاپ خالی. +OFFSITE_SYNC_STATUS_FILE= # ═══════════════════════════════════════════════════════════════════════ # ۲) دامنه‌ها — دو آدرس، بقیه از آن‌ها ساخته می‌شوند diff --git a/CLAUDE.md b/CLAUDE.md index 033709d..67ac96c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 request to the server admin, not something to fix. -**`78.38.35.247` (backup server)**, same account and password: ours is only `~/zareian-pgbackup/`, -`~/zareian-pgbackup-216/`, `~/.ssh/zareian_pgbackup*`, and the `zareian-pg-*` / `zareian-pg216-*` / -`zareian-applog-pull` units. It also runs other teams' services and its +**`78.38.35.247` (backup server)**, same account and password: ours is only `~/zareian-pgbackup/` (249's +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. **`78.38.35.216` (`tadrisino`, new dedicated VM, not yet serving users)**, account `zareian` (sudo): the whole diff --git a/config/health.py b/config/health.py index 6aa9fb0..3fcebb5 100644 --- a/config/health.py +++ b/config/health.py @@ -14,6 +14,7 @@ GET /api/health — «آیا این سرور واقعاً سالم است؟» """ import logging import os +import time from datetime import timedelta from django.conf import settings @@ -160,6 +161,8 @@ def _offsite_backup() -> dict: """ if connection.vendor != 'postgresql': return {'state': 'not_postgres'} + if settings.OFFSITE_SYNC_STATUS_FILE: + return _pushed_backup() slot = settings.OFFSITE_WAL_SLOT if not slot: return {'state': 'not_configured'} @@ -193,6 +196,45 @@ def _slot_state(row) -> dict: 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 و # migrateِ deploy.sh (۲)، فرمانِ مدیریتی یا psqlِ دستی (۲)، و workerی که gunicorn با max_requests # عوض می‌کند و تا بسته شدنِ قبلی هم‌زمان بالاست (یک pool). @@ -267,6 +309,9 @@ _OFFSITE_MESSAGES = { 'lagging': 'سرورِ بکاپ از دیتابیس عقب است.', 'lost': 'سرورِ بکاپ آن‌قدر قطع بود که زنجیره‌ی WAL برید؛ روی آن سرور اسلات و base backupِ تازه لازم است.', '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', } diff --git a/config/settings.py b/config/settings.py index ca2cc21..91e2c10 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1078,6 +1078,12 @@ LOG_DIR = os.environ.get('LOG_DIR', '') OFFSITE_WAL_SLOT = os.environ.get('OFFSITE_WAL_SLOT', '') # بیش از این عقب‌ماندن هشدار است. ۶۴ مگ ≈ چهار فایلِ WAL؛ در ترافیکِ عادی عقب‌ماندگی نزدیکِ صفر است. 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 و محلی) = ۶ فایلِ ۱۰ مگی و قدیمی‌ها پاک می‌شوند؛ # 'external' (سرور) = برنامه هرگز پاک نمی‌کند، `ops/applogs/archive-app-log.sh` هر شب app.log را جابه‌جا و diff --git a/config/tests_offsite_backup.py b/config/tests_offsite_backup.py index 5c10e40..bf12520 100644 --- a/config/tests_offsite_backup.py +++ b/config/tests_offsite_backup.py @@ -3,6 +3,8 @@ پرسشِ واقعی از pg_replication_slots این‌جا اجرا نمی‌شود (تست‌ها روی SQLite‌اند)؛ روی سرور با اسلاتِ واقعی دیده شد (docs/operations.md «بکاپ»). """ +from datetime import datetime, timedelta, timezone + from django.test import SimpleTestCase, override_settings from config import health @@ -27,13 +29,45 @@ class SlotStateTests(SimpleTestCase): 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): def test_ok_and_dev_are_silent(self): self.assertEqual(health._offsite_warnings({'state': 'ok', 'lagBytes': 0}), []) self.assertEqual(health._offsite_warnings({'state': 'not_postgres'}), []) 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) def test_sqlite_reports_not_postgres(self): diff --git a/docs/operations.md b/docs/operations.md index deffca7..f9530e2 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -389,23 +389,32 @@ the servers»): | | | |---|---| | ماشین | 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 ` | -| بکاپ | زنده روی ۲۴۷ (`~/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` | | سرویس‌ها | `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 و بی‌هشدار (`offsiteBackup` ok، عقب‌ماندگی ۰)، آزمونِ بازیابی از بکاپِ ۲۴۷ با همان شمارش‌ها، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و +سنجیده: `/api/health` با همه‌ی جزئیات ok، آزمونِ بازیابی از بکاپِ رمزشده با همان شمارش‌ها، فایل‌های ۲۴۷ رمزشده (`Salted__`، +`age-encryption.org/v1`)، کلیدِ ۲۱۶ روی ۲۴۷ نه می‌خواند نه شِل می‌گیرد، کپیِ ۲۴۷ با `rsync --checksum` عینِ مخزنِ محلی، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و بالا آمدنِ خودکارِ همه‌ی سرویس‌ها، درخواستِ مستقیم به IP ۴۰۳ (nginx IPِ واقعی را می‌بیند). نسنجیده: هیچ درخواستی از مسیرِ ArvanCloud، پیامک، پرداخت و تیوتورِ زنده. **سوئیچ (ترتیب):** ۱) 443ِ ورودی باز شود. ۲) روی ۲۴۹ `zareian-mathapp` متوقف (پیامِ نگه‌داری در ArvanCloud)، `pg_dump -Fc`، بازیابی روی سرورِ جدید در دیتابیسِ خالی‌شده، مقایسه‌ی تعدادِ ردیف‌ها. ۳) originِ هر دو دامنه در ArvanCloud به `78.38.35.216`. -۴) health و یک ورود و یک آزمون از بیرون. ۵) روی ۲۴۷ یونیت‌های بکاپِ ۲۴۹ (`zareian-pg-*`) متوقف، `zareian-pg216-basebackup` -یک بار دستی (بعد از بازیابیِ دیتابیسِ نهایی)، و `pull-app-logs.sh` به آدرسِ جدید. ۶) CLAUDE.md و این سند. +۴) health و یک ورود و یک آزمون از بیرون. ۵) `tadrisino-pgbackrest-full` یک بار دستی (بعد از بازیابیِ دیتابیسِ نهایی)؛ روی ۲۴۷ +یونیت‌های بکاپِ ۲۴۹ (`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` روی هر دو سرور اجرا شود تا کدِ دو طرف یکی بماند. ### Qdrant diff --git a/ops/pgbackup/basebackup.sh b/ops/pgbackup/basebackup.sh index 4d7207d..58b460f 100755 --- a/ops/pgbackup/basebackup.sh +++ b/ops/pgbackup/basebackup.sh @@ -4,10 +4,8 @@ # --checkpoint=spread و --max-rate بار روی ۲۴۹ را کم نگه می‌دارند (آن‌جا سرورِ اصلی است). # نسخه تا pg_verifybackup قبولش نکند جای قبلی‌ها را نمی‌گیرد و چیزی پاک نمی‌شود. set -euo pipefail -# ROOT/BIN/PORT از یونیت می‌آیند؛ پیش‌فرض‌ها همان بکاپِ ۲۴۹ است (یونیت‌های zareian-pg216-* برای سرورِ جدید). -ROOT="${PGB_ROOT:-/home/administrator/zareian-pgbackup}" -BIN="${PGB_BIN:-/usr/lib/postgresql/14/bin}" -PORT="${PGB_PORT:-15440}" +ROOT=/home/administrator/zareian-pgbackup +BIN=/usr/lib/postgresql/14/bin KEEP=7 cd "$ROOT" @@ -16,7 +14,7 @@ ts=$(date +%Y%m%d-%H%M%S) rm -rf base/.inprogress-* 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 "$BIN/pg_verifybackup" "$tmp" mv "$tmp" "base/$ts" diff --git a/ops/pgbackup/export-payment-events.sh b/ops/pgbackup/export-payment-events.sh index 545bfab..a6162fa 100755 --- a/ops/pgbackup/export-payment-events.sh +++ b/ops/pgbackup/export-payment-events.sh @@ -6,9 +6,8 @@ # خروجی: payment-log/payment-events-.csv، orders-…csv، transactions-…csv؛ ۱۴ فایلِ آخرِ هر نوع می‌ماند. # توجه: نسخه‌ی کامل تا چند دقیقه‌ی WAL عقب‌تر از ۲۴۹ می‌شود، ولی WALِ رسیده همه اعمال می‌شود. set -euo pipefail -# ROOT/BIN از یونیت می‌آیند؛ پیش‌فرض‌ها همان بکاپِ ۲۴۹ است (یونیت‌های zareian-pg216-* برای سرورِ جدید). -ROOT="${PGB_ROOT:-/home/administrator/zareian-pgbackup}" -BIN="${PGB_BIN:-/usr/lib/postgresql/14/bin}" +ROOT=/home/administrator/zareian-pgbackup +BIN=/usr/lib/postgresql/14/bin OUT="$ROOT/payment-log" mkdir -p "$OUT" diff --git a/ops/pgbackup/restore-check.sh b/ops/pgbackup/restore-check.sh index 44756ff..2db122d 100755 --- a/ops/pgbackup/restore-check.sh +++ b/ops/pgbackup/restore-check.sh @@ -6,9 +6,8 @@ # restore-check.sh '2026-10-10 16:30:00+03:30' # با KEEP=1 پوشه و Postgresِ موقت می‌مانند (برای بازیابیِ واقعی: از آن pg_dump بگیرید، بعد pg_ctl stop و rm). set -euo pipefail -# ROOT/BIN از یونیت می‌آیند؛ پیش‌فرض‌ها همان بکاپِ ۲۴۹ است (یونیت‌های zareian-pg216-* برای سرورِ جدید). -ROOT="${PGB_ROOT:-/home/administrator/zareian-pgbackup}" -BIN="${PGB_BIN:-/usr/lib/postgresql/14/bin}" +ROOT=/home/administrator/zareian-pgbackup +BIN=/usr/lib/postgresql/14/bin TARGET="${1:-}" latest=$(ls -1d "$ROOT"/base/2* | tail -1) diff --git a/ops/pgbackup/zareian-pg216-basebackup.service b/ops/pgbackup/zareian-pg216-basebackup.service deleted file mode 100644 index 3b1bff3..0000000 --- a/ops/pgbackup/zareian-pg216-basebackup.service +++ /dev/null @@ -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 diff --git a/ops/pgbackup/zareian-pg216-basebackup.timer b/ops/pgbackup/zareian-pg216-basebackup.timer deleted file mode 100644 index cb4ffe6..0000000 --- a/ops/pgbackup/zareian-pg216-basebackup.timer +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Zareian MathApp: daily base backup from 216 - -[Timer] -OnCalendar=*-*-* 04:50:00 -Persistent=true - -[Install] -WantedBy=timers.target diff --git a/ops/pgbackup/zareian-pg216-export-events.service b/ops/pgbackup/zareian-pg216-export-events.service deleted file mode 100644 index a6de586..0000000 --- a/ops/pgbackup/zareian-pg216-export-events.service +++ /dev/null @@ -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 diff --git a/ops/pgbackup/zareian-pg216-export-events.timer b/ops/pgbackup/zareian-pg216-export-events.timer deleted file mode 100644 index 019f12c..0000000 --- a/ops/pgbackup/zareian-pg216-export-events.timer +++ /dev/null @@ -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 diff --git a/ops/pgbackup/zareian-pg216-tunnel.service b/ops/pgbackup/zareian-pg216-tunnel.service deleted file mode 100644 index f260179..0000000 --- a/ops/pgbackup/zareian-pg216-tunnel.service +++ /dev/null @@ -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 diff --git a/ops/pgbackup/zareian-pg216-wal.service b/ops/pgbackup/zareian-pg216-wal.service deleted file mode 100644 index a7ae8ca..0000000 --- a/ops/pgbackup/zareian-pg216-wal.service +++ /dev/null @@ -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 diff --git a/ops/server216/10-tadrisino.conf b/ops/server216/10-tadrisino.conf new file mode 100644 index 0000000..919d7a9 --- /dev/null +++ b/ops/server216/10-tadrisino.conf @@ -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 diff --git a/ops/server216/80-tadrisino-hardening.conf b/ops/server216/80-tadrisino-hardening.conf new file mode 100644 index 0000000..0c30973 --- /dev/null +++ b/ops/server216/80-tadrisino-hardening.conf @@ -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 diff --git a/ops/server216/backup.conf b/ops/server216/backup.conf new file mode 100644 index 0000000..b373344 --- /dev/null +++ b/ops/server216/backup.conf @@ -0,0 +1,4 @@ +# تدریسینو: آرشیوِ پیوسته‌ی WAL با pgBackRest (رمزنگاری‌شده). archive_timeout یعنی حداکثر ۶۰ ثانیه تغییرِ آرشیونشده. +archive_mode = on +archive_command = 'pgbackrest --stanza=zareian archive-push %p' +archive_timeout = 60 diff --git a/ops/server216/gunicorn-mathapp.service b/ops/server216/gunicorn-mathapp.service new file mode 100644 index 0000000..cc47b7d --- /dev/null +++ b/ops/server216/gunicorn-mathapp.service @@ -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 diff --git a/ops/server216/pgbackrest.conf b/ops/server216/pgbackrest.conf new file mode 100644 index 0000000..2189408 --- /dev/null +++ b/ops/server216/pgbackrest.conf @@ -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= +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 diff --git a/ops/server216/tadrisino-blocklist b/ops/server216/tadrisino-blocklist new file mode 100755 index 0000000..55f76dc --- /dev/null +++ b/ops/server216/tadrisino-blocklist @@ -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" diff --git a/ops/server216/tadrisino-blocklist.service b/ops/server216/tadrisino-blocklist.service new file mode 100644 index 0000000..7893137 --- /dev/null +++ b/ops/server216/tadrisino-blocklist.service @@ -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 diff --git a/ops/server216/tadrisino-blocklist.timer b/ops/server216/tadrisino-blocklist.timer new file mode 100644 index 0000000..e231f86 --- /dev/null +++ b/ops/server216/tadrisino-blocklist.timer @@ -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 diff --git a/ops/server216/tadrisino-export-payment-events b/ops/server216/tadrisino-export-payment-events new file mode 100755 index 0000000..518345d --- /dev/null +++ b/ops/server216/tadrisino-export-payment-events @@ -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" diff --git a/ops/server216/tadrisino-export-payment-events.service b/ops/server216/tadrisino-export-payment-events.service new file mode 100644 index 0000000..e0423a9 --- /dev/null +++ b/ops/server216/tadrisino-export-payment-events.service @@ -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 diff --git a/ops/server216/tadrisino-export-payment-events.timer b/ops/server216/tadrisino-export-payment-events.timer new file mode 100644 index 0000000..bb9645d --- /dev/null +++ b/ops/server216/tadrisino-export-payment-events.timer @@ -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 diff --git a/ops/server216/tadrisino-offsite-sync b/ops/server216/tadrisino-offsite-sync new file mode 100755 index 0000000..b97eb01 --- /dev/null +++ b/ops/server216/tadrisino-offsite-sync @@ -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 diff --git a/ops/server216/tadrisino-offsite-sync.service b/ops/server216/tadrisino-offsite-sync.service new file mode 100644 index 0000000..05b849c --- /dev/null +++ b/ops/server216/tadrisino-offsite-sync.service @@ -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 diff --git a/ops/server216/tadrisino-offsite-sync.timer b/ops/server216/tadrisino-offsite-sync.timer new file mode 100644 index 0000000..29ea2fb --- /dev/null +++ b/ops/server216/tadrisino-offsite-sync.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Tadrisino: copy encrypted backups to 247 + +[Timer] +OnCalendar=minutely +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/ops/server216/tadrisino-pgbackrest-diff.service b/ops/server216/tadrisino-pgbackrest-diff.service new file mode 100644 index 0000000..7eb64c3 --- /dev/null +++ b/ops/server216/tadrisino-pgbackrest-diff.service @@ -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 diff --git a/ops/server216/tadrisino-pgbackrest-diff.timer b/ops/server216/tadrisino-pgbackrest-diff.timer new file mode 100644 index 0000000..17e6a00 --- /dev/null +++ b/ops/server216/tadrisino-pgbackrest-diff.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Tadrisino: daily differential encrypted backup + +[Timer] +OnCalendar=*-*-* 03:40:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/ops/server216/tadrisino-pgbackrest-full.service b/ops/server216/tadrisino-pgbackrest-full.service new file mode 100644 index 0000000..cf3eddd --- /dev/null +++ b/ops/server216/tadrisino-pgbackrest-full.service @@ -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 diff --git a/ops/server216/tadrisino-pgbackrest-full.timer b/ops/server216/tadrisino-pgbackrest-full.timer new file mode 100644 index 0000000..ac1fb8f --- /dev/null +++ b/ops/server216/tadrisino-pgbackrest-full.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Tadrisino: weekly full encrypted backup + +[Timer] +OnCalendar=Sun *-*-* 03:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/ops/server216/tadrisino-restore-check b/ops/server216/tadrisino-restore-check new file mode 100755 index 0000000..7099cbd --- /dev/null +++ b/ops/server216/tadrisino-restore-check @@ -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" < "$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) diff --git a/ops/server216/tadrisino.local b/ops/server216/tadrisino.local new file mode 100644 index 0000000..8f1ad79 --- /dev/null +++ b/ops/server216/tadrisino.local @@ -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 diff --git a/ops/server216/zareian-redis.service b/ops/server216/zareian-redis.service new file mode 100644 index 0000000..3208f94 --- /dev/null +++ b/ops/server216/zareian-redis.service @@ -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 diff --git a/ops/server216/zareian.conf b/ops/server216/zareian.conf new file mode 100644 index 0000000..8dbba8a --- /dev/null +++ b/ops/server216/zareian.conf @@ -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