diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index bca1dd8..6476845 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -1,4 +1,5 @@ # Admin Task Board +_Updated: 2026-06-12 — 🔴→🟢 **MSSQL backup incident + estate backup audit.** VDS daily backup падал с 12.06 (line 108, exit 1): mssql-блок (added 11.06) использовал `WITH ... INIT` → упирался в компрессованный media-header майских `.bak` (Express не пишет COMPRESSION) → молча падал первым cron-запуском. **5 боевых CMS-баз 3 недели без offsite-копии.** Fix: `INIT`→`FORMAT`, прогон verified (MoreThenCms 910M/StayerCalculator 528M/StayerPrice 39M/TireService 4.5M/stostayer 990M на kreknin). Репо-копия `run.sh` синхронизирована (mssql-блока не было). Concept: `.wiki/concepts/mssql-on-vds.md` gotcha#2. **Estate backup-аудит** → `.wiki/concepts/backup-inventory-2026-06.md`: ✅ openwrt UCI backup настроен (cron 03:30 → kreknin, restricted forced-command key); ⚪ заведена `kreknin-self-backup` (#1 SPOF, на потом); nl-vds x-ui.db — user: не нужен; windows-host stale script — user: забыть._ _Updated: 2026-06-08 — 🟢 ad-hoc fix `maljarka.tandemmebel.ru` (iis-migration follow-up): домен переехал на RUVDS корректно (DNS/TLS/IIS ок), но отдавал 502 **только на HTTPS**. Root cause не в миграции: у тенанта maljarka `dbo.Sites.SettingsData=NULL` (нет блока `httpSecure`) → MoreThenCms `SnollaMiddleware` бросает KeyNotFound на HTTPS-ветке → 502; по HTTP 200. Fix: `UPDATE dbo.Sites SET SettingsData='{httpSecure:enableHttps=true,…}' WHERE SiteId=a2476738… AND SettingsData IS NULL` + `Restart-WebAppPool snolla`. Verified maljarka:443→200 (server-local + external по 80.64.31.36), kupimknigi не задет. Audit: maljarka — единственный NULL-сайт с :443-биндингом из 25. rimiz degraded по др. причине. Concept: `.wiki/concepts/morethencms-null-settingsdata-https-502.md`._ _Updated: 2026-06-05 (вечер) — 🟡 `fix-nl-vds-reality-pq-dest`: по команде user применён server-side fix + ротация кредов. Reality dest intel→microsoft (x-ui.db + restart), сквозной тоннель через реальный :443 = HTTP 204; 32030 жив. Креды ротированы (root SSH / panel user+pass / panel secret-JWT — светились в плейнтексте; новые в `pass nl-vds-3xui/full-env`, старые отклоняются, БД-бэкап на сервере). Остался client-side: user меняет SNI в v2rayN (`pqmkayaxo2`→microsoft) + подтверждает → close._ _Updated: 2026-06-05 — заведена `fix-nl-vds-reality-pq-dest`. Новый NL VDS (213.176.64.253, 3x-UI/Xray 26.6.1): диагностирован отказ Reality-инбаунда 443 — **ML-DSA-65 (PQ) × dest `www.intel.com` (Akamai шлёт HRR)**; plain VLESS 32030 работает. Узел+root-cause в вики (`nl-vds-3xui`, `reality-pq-mldsa65-dest-incompatibility`); креды в `pass nl-vds-3xui/full-env`._ @@ -37,6 +38,15 @@ _Updated: 2026-05-25 (`iis-migration-to-ruvds` 🟢 closed Phase 1 per user deci --- +## ⚪ [kreknin-self-backup] — второй таргет для приёмника бэкапов (на потом) +**Status:** ⚪ backlog — заведена 2026-06-12 по итогам backup-gap аудита, user: «на потом». +**Where I stopped:** kreknin (`195.19.90.188`, `/volume1` 7 ТБ) — единственный приёмник всех 4 пайплайнов, сам не бэкапится → SPOF всей estate. Направление: второй облачный таргет (Backblaze B2 / Synology Hyper Backup, client-side encryption) для critical-subset (`*/latest` ~30 ГБ + `.hbk` vault). +**Next action:** при подъёме — выбрать B2 vs Glacier, настроить DSM Hyper Backup на subset. См. [kreknin-self-backup.md](kreknin-self-backup.md) + `.wiki/concepts/backup-inventory-2026-06.md`. +**Branch:** n/a (admin ops) + + +--- + ## 🟢 [vehicles-loader-progress-deploy] — closed 2026-05-31 — 0.4.0 задеплоен + live-verify прошёл. Прогон Sun 31.05 06:21:48→07:53:14 MSK на changed-выгрузке: журнал показал движение по всем фазам (▶ branches/vehicles/units/delete-sweep, батч-прогресс `manufacturers 18/183…`, `units 3/26…`, `✓ done: N rows`, per-model delete-sweep), НЕ тишина. exit 0, `importRun id=4 ok`, `reportJson errors:[]`. **Email-баг найден+починен:** `STOSTAYER_MAIL_TO` слался сам себе (`site@stostayer.ru`) → исправлен на `vitya.kuznetsov@gmail.com` в host env (бэкап `.bak.20260531`), доставка на gmail подтверждена тест-письмом. Все 4 acceptance ✅. См. [vehicles-loader-progress-deploy.md](vehicles-loader-progress-deploy.md). **Follow-ups (не блокеры, в stostayer.new):** (1) units-фаза 1h31m на 100529 rows — узкое место, батч-инсёрт/индексы; (2) generation `unmatched:51` — проверить не теряем ли данные; (3) `config/default.json` дефолт `to: site@stostayer.ru` выровнять (прод перекрыт env). **Branch:** n/a diff --git a/.tasks/kreknin-self-backup.md b/.tasks/kreknin-self-backup.md new file mode 100644 index 0000000..3106fea --- /dev/null +++ b/.tasks/kreknin-self-backup.md @@ -0,0 +1,26 @@ +# kreknin-self-backup — второй таргет для приёмника бэкапов + +**Status:** ⚪ backlog (на потом, по решению user 2026-06-12) +**Priority:** #1 среди backup-дыр (см. [[../.wiki/concepts/backup-inventory-2026-06]]) + +## Проблема + +[[../.wiki/entities/kreknin-synology]] (`195.19.90.188`, `/volume1` 7.0 ТБ) — **единственный приёмник всех 4 бэкап-пайплайнов** (vds-kzntsv 05:00, books-vds 06:00, ruvds-iis 04:30, openwrt 03:30) и при этом **сам никуда не бэкапится**. Если его том откажет — одновременно теряются offsite-копии ВСЕХ машин. Худший single point of failure в estate. + +Тип RAID не подтверждён («тип неизвестен» в entity). Hyper Backup vault (`diskstation_1.hbk`, 430 ГБ) на нём же — тоже не реплицирован вовне. + +## Направление (не финал, обсудить при подъёме) + +Второй таргет для критичного subset — облако: +- **Backblaze B2** (дёшево, S3-совместимо) или **S3 Glacier** (холодное, ещё дешевле, но retrieval-latency). +- Synology **Cloud Sync** (B2) или **Hyper Backup** (→ B2/S3) — нативные пакеты DSM. +- Минимальный subset: `/volume1/NetBackup/*/latest` каждого пайплайна + `*.hbk` vault. Полные 7 ТБ лить не нужно — только последние снапшоты. +- Шифрование на стороне DSM (Hyper Backup client-side encryption) — облако untrusted. + +## Оценка + +Объём critical-subset: vds-kzntsv ~2.4 ГБ MSSQL + ~12 ГБ stacks, books-vds ~5 ГБ, ruvds ~9 ГБ, openwrt 15 КБ → ~30 ГБ latest. B2 storage $6/ТБ/мес → копейки. Egress при restore — разовый. + +## Decisions log + +- **2026-06-12** — заведена по итогам backup-gap аудита (триггер: MSSQL 3-нед gap). User: «задача, но на потом». Не горит, но это #1 по риску среди оставшихся дыр. diff --git a/.wiki/concepts/backup-inventory-2026-06.md b/.wiki/concepts/backup-inventory-2026-06.md new file mode 100644 index 0000000..c34b1eb --- /dev/null +++ b/.wiki/concepts/backup-inventory-2026-06.md @@ -0,0 +1,61 @@ +--- +title: Backup inventory & gap audit — вся estate (2026-06-12) +type: concept +tags: [backup, audit, inventory, infra, ops, disaster-recovery, gap] +related: [[../entities/vds-kzntsv]], [[../entities/books-vds]], [[../entities/ruvds-iis-host]], [[../entities/kreknin-synology]], [[../entities/nl-vds-3xui]], [[../entities/openwrt-router]], [[mssql-on-vds]], [[vds-backup-rsync-kreknin]] +updated: 2026-06-12 +--- + +# Backup inventory & gap audit (2026-06-12) + +Полная карта: что за данные на каждой машине и **что реально бэкапится** (с доказательством +последнего успеха, не «настроено по таске»). Триггер аудита — MSSQL-инцидент 12.06: 5 боевых +CMS-баз на [[../entities/vds-kzntsv]] **3 недели не имели offsite-копии** (dump-ветка добавлена +11.06, молча падала первым cron-запуском — см. [[mssql-on-vds]] gotcha #2). Урок: **бэкап-шаг не +готов, пока не предъявлен лог хотя бы одного реального успешного прогона.** + +## Матрица + +| Машина | Данные / сервисы | Метод | Куда | Расписание | Последний УСПЕХ (доказательство) | Статус | +|---|---|---|---|---|---|---| +| **vds-kzntsv** `89.253.255.94` | gitea, verdaccio (8.6G), registry, traefik, portainer, ntfy, owncloud; DB: postgres, mariadb, mongo, redis, **MSSQL ×5 (MoreThenCms/StayerCalculator/StayerPrice/TireService/stostayer)** | logical dumps + rsync `--link-dest` | kreknin `/volume1/NetBackup/vds-kzntsv/` ret.7 | daily 05:00 | snapshots до 2026-06-12 ✓. **MSSQL — впервые в копии 12.06** (910+528+39+4.5+990 МБ) | ✅ (MSSQL fixed 12.06) | +| **books-vds** `89.253.255.133` | books-db (maria), mongo 4.2, scheduler-mongo, ES 7.10 (928k docs), minio, imgproxy | dumps + ES REST snapshot + rsync | kreknin `/volume1/NetBackup/books-vds/` ret.7 | daily 06:00 | snapshots до 2026-06-12 ✓; ES restore проверен 46 сек (28.05) | ✅ / ⚠️ bookva-* не покрыт | +| **ruvds-iis-host** `80.64.31.36` | `C:\sites\snolla` (8.66G), applicationHost.config, IIS WebConfig, LE PFX, ssh-config. БД нет (→ mssql.kzntsv.site) | rclone sync (SFTP) | kreknin `/volume1/NetBackup/ruvds-iis/` ret.7 | daily 04:30 | snapshots до 2026-06-12 ✓ | ✅ | +| **kreknin-synology** `195.19.90.188` | **приёмник всех 4 пайплайнов** + Hyper Backup vault (430G) + live-сервисы | — | — | — | — | ❌ **SPOF: сам не бэкапится** | +| **nl-vds-3xui** `213.176.64.253` | 3x-UI SQLite `/etc/x-ui/x-ui.db` (все inbound, Reality-ключи, client UUID, SS-ключи) | только локальные `.bak` на том же хосте | (локально) | manual | нет offsite | ❌ **no offsite** | +| **openwrt-router** `192.168.1.1` | UCI `/etc/config/*`, port-forwards, DHCP-резервации, dropbear keys | — | — | — | — | ❌ **none** | +| **windows-recovery-host** DESKTOP-NSEF0UK | DECOMM 08.06; осталось: stostayer IIS ×2, lightrag+postgres, markitdown MCP | скрипт-сирота (таргетит удалённое) | (был) kreknin | (был) 03:00 | таска удалена 11.06 | ⚠️ stale script; lightrag/postgres без копии | +| **dead-synology** `192.168.1.10` | МЁРТВ — данные мигрированы | n/a (живёт в kreknin hbk) | — | — | hbk 2026-05-09 | n/a | +| **snolla-recovery-vm** | УДАЛЕНА 08.06 | n/a | — | — | — | n/a | + +## Дыры — статус после триажа 2026-06-12 + +| # | Дыра | Решение user (2026-06-12) | Статус | +|---|---|---|---| +| 1 | **kreknin сам не бэкапится** — SPOF всей estate (1 том 7 ТБ, RAID не подтверждён; смерть `/volume1` = одновременная потеря offsite-копий ВСЕХ машин) | «задача, но на потом» | ⚪ заведена `[[../../.tasks/kreknin-self-backup]]` (#1 по риску) | +| 2 | **openwrt UCI** — единственный публичный ingress всех CMS, без копии | «можешь сделать» | ✅ **СДЕЛАНО** — см. ниже | +| 3 | **bookva-* на books-vds** (`bookva-db/mongo/es/minio`, 4.7 ГБ tenant-данных) не дампятся/не rsync'ятся | не обсуждалось | ⚠️ открыто (follow-up при активации bookva) | +| 4 | **nl-vds-3xui `x-ui.db`** — профили + Reality-ключи только на хосте | «не нужен» | ⏸ accepted (won't-do) | +| 5 | **windows-host stale script** — таргетит удалённые 08.06 ресурсы | «забыть, не актуально» | ⏸ accepted (ignore) | + +### openwrt UCI backup — реализовано 2026-06-12 + +Daily push с роутера на kreknin, **без раздачи роутеру широких прав в хранилище**: +- На роутере: `dropbearkey` ed25519 `/root/.ssh/id_kreknin`; `/root/uci-backup.sh` = `sysupgrade -b` → `dbclient` pipe; cron `30 3 * * *`. +- На kreknin: pubkey роутера в `~/.ssh/authorized_keys` с **forced-command** `command="cat > /volume1/NetBackup/openwrt/openwrt-latest.tar.gz"` + `no-port-forwarding,no-X11,no-agent,no-pty`. Ключ умеет ТОЛЬКО писать в один файл — компрометация публично-доступного роутера не даёт доступа к остальному vault. +- Latest-only (без истории) — конфиг роутера меняется редко и осознанно; приемлемо. Tarball ~15 КБ. +- Verified 2026-06-12: `openwrt-latest.tar.gz` 14554 байт на kreknin (== source). + +## Что сделано правильно (не трогать) + +- Все 3 живых VDS-пайплайна используют **logical dumps поверх raw datadir** — корректно + (running-container locks → raw `.mdf/.ldf` копия битая). `/opt/stacks/databases/*/data` намеренно + вне rsync. +- `--link-dest` hardlink-incremental + retention 7 + ntfy/email нотификация на каждый прогон. +- MinIO blobs на books-vds — raw-копия `minio/data` (immutable objects, допустимо). + +## Связанные + +- [[mssql-on-vds]] — INIT vs FORMAT gotcha, корень инцидента 12.06 +- [[vds-backup-rsync-kreknin]] — таска VDS-пайплайна +- [[../entities/kreknin-synology]] — приёмник (SPOF #1) diff --git a/.wiki/concepts/mssql-on-vds.md b/.wiki/concepts/mssql-on-vds.md index 4e7bc1e..8ef335b 100644 --- a/.wiki/concepts/mssql-on-vds.md +++ b/.wiki/concepts/mssql-on-vds.md @@ -3,7 +3,7 @@ title: MSSQL on vds-kzntsv (Express 2022 Linux) type: concept tags: [vds, mssql, migration, ops] related: [[../entities/vds-kzntsv]], [[recovery-architecture-snapshot]], [[traefik-tcp-passthrough-vs-starttls]], [[db-tls-self-signed-via-traefik-raw-tcp]] -updated: 2026-05-22 +updated: 2026-06-12 --- # MSSQL on vds-kzntsv @@ -93,13 +93,17 @@ source /opt/stacks/databases/mssql/.env # injects MSSQL_SA_PASSWORD for DB in MoreThenCms StayerCalculator StayerPrice TireService stostayer; do docker exec mssql /opt/mssql-tools18/bin/sqlcmd \ -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -b \ - -Q "BACKUP DATABASE [$DB] TO DISK='/var/opt/mssql/backups/${DB}.bak' WITH COPY_ONLY, INIT" + -Q "BACKUP DATABASE [$DB] TO DISK='/var/opt/mssql/backups/${DB}.bak' WITH COPY_ONLY, FORMAT" mv /opt/stacks/databases/mssql/backups/${DB}.bak "$DUMP_DIR/mssql-${DB}.bak" done unset MSSQL_SA_PASSWORD ``` -**Gotcha:** Express Edition не поддерживает `WITH COMPRESSION` — только `COPY_ONLY, INIT`. .bak файлы создаются в bind-mount `/var/opt/mssql/backups/` (uid 10001:10001); root mv их в `$DUMP_DIR` для rsync на kreknin. 5 DBs ~ 486 MB uncompressed. sqlcmd path: `/opt/mssql-tools18/bin/sqlcmd`. +**Gotcha #1 — Express не умеет COMPRESSION:** Express Edition не поддерживает `WITH COMPRESSION`. .bak файлы создаются в bind-mount `/var/opt/mssql/backups/` (uid 10001:10001); root mv их в `$DUMP_DIR` для rsync на kreknin. sqlcmd path: `/opt/mssql-tools18/bin/sqlcmd`. + +**Gotcha #2 — `FORMAT`, не `INIT` (incident 2026-06-12):** первая версия блока (added 2026-06-11) использовала `WITH COPY_ONLY, INIT` и **молча падала первым же cron-запуском**. Причина: в `/var/opt/mssql/backups/` лежали миграционные `.bak` от 22.05, созданные `WITH COMPRESSION` на Developer-источнике. `INIT` переиспользует существующий media-set header (компрессованный) → Express не может в него писать → `Msg 1844 "BACKUP ... WITH COMPRESSION is not supported on Express"` (хотя в команде COMPRESSION нет!). `NO_COMPRESSION` тоже не спасает → `Msg 3098 "media formatted with an incompatible structure"`. **Fix: `WITH COPY_ONLY, FORMAT`** — всегда создаёт новый media set, иммунно к остаткам. Урок: при бэкапе на Express в каталог с возможными чужими/старыми `.bak` — только `FORMAT`, не `INIT`. + +**Backup gap:** между 22.05 (миграция) и 12.06 у 5 боевых баз **не было ни одной offsite-копии** — raw datadir намеренно не rsync'ится (lock), а dump-ветка падала с первого дня. Закрыто 2026-06-12. ## Rollback recipe diff --git a/.wiki/entities/openwrt-router.md b/.wiki/entities/openwrt-router.md index 5742a45..05516a0 100644 --- a/.wiki/entities/openwrt-router.md +++ b/.wiki/entities/openwrt-router.md @@ -55,7 +55,15 @@ updated: 2026-05-19 Дополнительные input rules для wan (стандартные OpenWRT): Allow-DHCP-Renew, Allow-Ping, Allow-IGMP, Allow-DHCPv6, Allow-MLD, Allow-ICMPv6-*, Allow-IPSec-ESP. -## Что bgужно сделать (на потом) +## Config backup (с 2026-06-12) + +Daily UCI/config backup → [[kreknin-synology]]: +- `/root/uci-backup.sh` — `sysupgrade -b /tmp/uci-backup.tar.gz` → `dbclient -i /root/.ssh/id_kreknin -y vitya@195.19.90.188` (pipe stdin). +- Cron `/etc/crontabs/root`: `30 3 * * * /root/uci-backup.sh`. +- Ключ роутера `/root/.ssh/id_kreknin` (dropbear ed25519) авторизован на kreknin с **forced-command** `cat > /volume1/NetBackup/openwrt/openwrt-latest.tar.gz` + `no-pty,no-*-forwarding` — ключ умеет только записать один файл (роутер публично-доступен → ограничение обязательно). +- Latest-only (без истории снапшотов); tarball ~15 КБ. См. [[../concepts/backup-inventory-2026-06]]. + +## Что нужно сделать (на потом) - Отключить устаревшие redirects на 192.168.1.10 (DSM/FTP/SQL/Cloud Station) — снижает attack surface. - Запланировать DDNS для случая смены публичного IP (REGRU domains всё ещё указывают на 94.19.247.14). diff --git a/.wiki/index.md b/.wiki/index.md index a17eeed..1c82516 100644 --- a/.wiki/index.md +++ b/.wiki/index.md @@ -22,6 +22,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever - [admin-infra-project](concepts/admin-infra-project.md) — design + migration plan для OpeItcLoc03/admin (canonical) +- [backup-inventory-2026-06](concepts/backup-inventory-2026-06.md) — карта всех машин × что реально бэкапится (с доказательством); дыры по приоритету (kreknin SPOF, nl-vds x-ui.db, bookva-*, openwrt) — триггер: MSSQL 3-нед gap - [vds-kzntsv-ssh-access](concepts/vds-kzntsv-ssh-access.md) — SSH access audit на vds-kzntsv (infra VDS), retained keys + add/revoke processes (was misnamed `books-ssh-access` до 2026-05-25 — content всегда был про vds-kzntsv) - [compose-bcrypt-escape-trap](concepts/compose-bcrypt-escape-trap.md) — Docker Compose ест `$` в bcrypt hashes - [db-tls-self-signed-via-traefik-raw-tcp](concepts/db-tls-self-signed-via-traefik-raw-tcp.md) — DB TLS через traefik raw TCP с self-signed certs diff --git a/scripts/vds-backup-rsync-kreknin/run.sh b/scripts/vds-backup-rsync-kreknin/run.sh index 71f4e20..c28352d 100644 --- a/scripts/vds-backup-rsync-kreknin/run.sh +++ b/scripts/vds-backup-rsync-kreknin/run.sh @@ -100,6 +100,21 @@ docker cp redis:/data/dump.rdb "$DUMP_DIR/redis.rdb" gzip -f "$DUMP_DIR/redis.rdb" echo "redis: $(stat -c %s "$DUMP_DIR/redis.rdb.gz") bytes" +# MSSQL: backup 5 prod DBs via sqlcmd into bind-mounted /var/opt/mssql/backups/ +# WITH FORMAT (not INIT): Express can't write to a media set previously formatted +# WITH COMPRESSION (e.g. migration-era .bak); FORMAT recreates the media set fresh. +source /opt/stacks/databases/mssql/.env # injects MSSQL_SA_PASSWORD +MSSQL_BACKUP_DIR=/opt/stacks/databases/mssql/backups +for DB in MoreThenCms StayerCalculator StayerPrice TireService stostayer; do + docker exec mssql /opt/mssql-tools18/bin/sqlcmd \ + -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -b \ + -Q "BACKUP DATABASE [$DB] TO DISK='/var/opt/mssql/backups/${DB}.bak' WITH COPY_ONLY, FORMAT" \ + > /dev/null + mv "$MSSQL_BACKUP_DIR/${DB}.bak" "$DUMP_DIR/mssql-${DB}.bak" + echo "mssql $DB: $(stat -c %s "$DUMP_DIR/mssql-${DB}.bak") bytes" +done +unset MSSQL_SA_PASSWORD + # === Step 2: rsync to kreknin === echo "--- rsync to $DEST_USER@$DEST_HOST:$DEST_PATH ---" ssh $SSH_OPTS "$DEST_USER@$DEST_HOST" "mkdir -p '$DEST_PATH'"