incident(books-vds-es): true RCA — ransom-бот через открытый :9200, не оператор
Рецидив вчерашнего ES-инцидента вскрыл истинную причину. Вчерашняя гипотеза «оператор в cutover попал на canonical» опровергнута. Root cause: ES (stack 33) публиковал 0.0.0.0:9200 мимо traefik. Free-ES 7.10 без auth → порт открыт всему интернету. Ransom-бот сносил индексы by-name (мимо Control #1 destructive_requires_name), оставлял read_me с BTC-выкупом. accessLog (Control #2) пуст — бот шёл прямо в порт, не через traefik. firewalld бесполезен (docker-publish обходит INPUT-зоны). Fix (Control #3): убрана публикация host-порта из stack 33 (Portainer PUT), дыра закрыта; re-restore epz/products/artmone из daily-2026-05-25. Отдельный баг: epz-поиск падал у ОБОИХ тенантов — getTenantIdSeller( config.get("tenant")) через node-config, а tenant не задан ни в default.json, ни в env-маппинге (TENANT env = мёртвый груз). Добавлен tenant в overlay default.json (slovo/bookva). products работал — отдельный код-путь. accessLog откатан (сторожил не ту дверь). - wiki concept: correction-блок + секция «Рецидив 2026-05-29» + exposure-audit - tasks: restore-es reopened+reclosed; new ⚪ harden-books-vds-exposed-ports - NEXT_SESSION handoff Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,56 +1,51 @@
|
||||
---
|
||||
_last_updated_: 2026-05-28T20:30:00+03:00
|
||||
session_id: 2026-05-28-restore-es-indices-books-vds-closure
|
||||
_last_updated_: 2026-05-29T09:40:00+03:00
|
||||
session_id: 2026-05-29-es-ransom-recurrence-true-rca
|
||||
---
|
||||
|
||||
# Next session handoff
|
||||
|
||||
_Сессия: восстановили 3 ES индекса на canonical (books VDS stack 33) через
|
||||
snapshot restore + добавили 2 preventive control'а против повтора. Таска
|
||||
🟢 closed. Wiki concept + closure note записаны._
|
||||
_Сессия: рецидив ES-инцидента → нашли ИСТИННУЮ причину (ransom-бот через открытый
|
||||
`:9200`, вчерашний диагноз «оператор» опровергнут). Закрыли порт (Control #3) + re-restore.
|
||||
Отдельно починили epz-поиск у ОБОИХ тенантов (tenant-config баг). accessLog откатан._
|
||||
|
||||
## Recent commits
|
||||
|
||||
- (этой сессии — см. ниже, коммитятся при закрытии)
|
||||
- `63708fe6` tasks(NEXT_SESSION): session-close — ES restore + 2 preventive controls live
|
||||
- `48a5cf39` tasks(restore-es-indices-books-vds): closed — snapshot restore + 2 preventive controls
|
||||
- `e9b0a9ed` docs(tasks): restore-elasticsearch-indices-books-vds — заведение таски на prod outage
|
||||
- `068f7b26` docs(iis-migration): owner DNS-flip instructions (labtools, tandemmebel)
|
||||
- `11554d45` wiki(ingest): vds-kzntsv network-stack mismatch RCA + ifupdown anti-pattern
|
||||
- `dacc39a1` tasks(modulair-rag-vds-redeploy): follow-ups #1-3 done
|
||||
- `e9b0a9ed` docs(tasks): restore-elasticsearch-indices-books-vds — books VDS ES stack 33 пуст
|
||||
|
||||
## Что закрыли в эту сессию
|
||||
## Что сделали в эту сессию
|
||||
|
||||
- **Restore (46 сек):** `POST /_snapshot/kreknin/daily-2026-05-25/_restore` с `indices=epz,products,artmone`. Counts == source (820604/105922/2621), green.
|
||||
- **RCA:** 5 индексов удалены через ES API `DELETE _all` 2026-05-26 10:21 UTC, **1ч 11мин после создания bookva-es**. Best guess — оператор в cutover-prep попал на canonical вместо internal-only bookva-es:9200. Caller identity unrecoverable (audit/accessLog отсутствовали).
|
||||
- **Control #1:** `action.destructive_requires_name=true` env var на stack 33 — wildcard/`_all` DELETE теперь 400. By-name работает. Verified.
|
||||
- **Control #2:** Traefik JSON accessLog в `/letsencrypt/access.log` — будущие DELETE с IP/user/method/path в логе. Restart traefik сделан, log файл генерится.
|
||||
- **Wiki:** `concepts/es-destructive-delete-incident-2026-05-26.md` (recovery runbook + preventive controls + cross-refs). Index + log обновлены.
|
||||
- **True RCA:** ES (stack 33) публиковал `0.0.0.0:9200` мимо traefik. Free-ES 7.10 без auth → `curl :9200` без кредов = cluster info. Ransom-бот сносил индексы by-name (мимо Control #1 `destructive_requires_name`), оставлял `read_me` с BTC-выкупом (0.0041 BTC, кошелёк получил ~0.018 BTC с 5 жертв). Снос 28.05 20:13 UTC — через ~3ч после вчерашнего restore.
|
||||
- **Control #3 (fix дыры):** убрана публикация host-порта из stack 33 (Portainer PUT `/api/stacks/33?endpointId=1`). `docker ps` → `9200/tcp` без `0.0.0.0:`, внешний `curl :9200` refused, traefik route жив. Затем `DELETE /read_me` + restore `epz,products,artmone` из `daily-2026-05-25` (counts 820604/105922/2621 green) + restart books consumers.
|
||||
- **Отдельный баг — epz-поиск у ОБОИХ тенантов:** `getTenantIdSeller(config.get("tenant"))` через node-config; `tenant` не задан нигде, откуда node-config читает (ни `default.json`, ни `custom-environment-variables.json` — тот мапит только ELASTICSEARCH). `TENANT=bookva` env на bookva-web — мёртвый груз. Fix: добавил `"tenant":"slovo"`/`"bookva"` в overlay `default.json` (volumes `books-web-config`/`bookva-web-config`, бэкапы `*.bak-pre-tenant-2026-05-29`), рестарт. `config.tenant` резолвится, tenant-ошибки прекратились. **products работал и так** (другой код-путь).
|
||||
- **accessLog откатан:** Control #2 сторожил не ту дверь (бот шёл мимо traefik). `traefik.yml` восстановлен из `*.bak-pre-accesslog-2026-05-28`, лог-файл удалён, traefik рестартнут. Версия с accessLog сохранена как `*.bak-with-accesslog-2026-05-29`.
|
||||
- **Wiki:** concept `es-destructive-delete-incident-2026-05-26` обновлён (correction-блок + опровергнутая гипотеза + секция «Рецидив 2026-05-29» + exposure-audit). index + log обновлены.
|
||||
|
||||
## Open треки
|
||||
|
||||
| Трек | Готовность | Entry-point |
|
||||
|---|---|---|
|
||||
| `books-bookva-user-whitelist-gathering` | ⚪ ready — gathering от учредителя Bookva | `.tasks/STATUS.md` line ~221 |
|
||||
| `stateful-split-volume-copy` | ⚪ ready — maintenance window 5–10 мин | `.tasks/STATUS.md` line ~97 |
|
||||
| `infra-inventory` | ⚪ ready — two-tier (public + admin) | `.tasks/STATUS.md` line ~150 |
|
||||
| `books-stateful-split-execution` | 🔵 blocked — deps в `victor/books` | `.tasks/STATUS.md` line ~250 |
|
||||
| `books-vds-bookva-bootstrap` | 🔵 blocked — Bookva billing + provision | `.tasks/STATUS.md` line ~161 |
|
||||
| `books-dns-cutover-bookva` | 🔵 blocked — после `books-vds-bookva-bootstrap` | `.tasks/STATUS.md` line ~191 |
|
||||
|
||||
Активных 🔴 / 🟡 нет.
|
||||
| `harden-books-vds-exposed-ports` | ⚪ ready — exposure-audit сделан | `.tasks/harden-books-vds-exposed-ports.md` |
|
||||
| `books-bookva-user-whitelist-gathering` | ⚪ ready — gathering от учредителя Bookva | `.tasks/STATUS.md` |
|
||||
| `stateful-split-volume-copy` | ⚪ ready — maintenance window 5–10 мин | `.tasks/STATUS.md` |
|
||||
| `infra-inventory` | ⚪ ready — two-tier (public + admin) | `.tasks/STATUS.md` |
|
||||
| `books-stateful-split-execution` / `books-vds-bookva-bootstrap` / `books-dns-cutover-bookva` | 🔵 blocked | `.tasks/STATUS.md` |
|
||||
|
||||
## Спроси user'а
|
||||
|
||||
- **Logrotate для `access.log`** (~30–50 MB/day, disk free 63G): отдельная micro-task сейчас, или отложить до первого ощутимого роста? Записан как follow-up в task closure + wiki concept, но не оформлен.
|
||||
- **`pass books-vds/full-env` comment outdated** — в нём ещё написано «NOT Portainer-managed: elasticsearch, mongo, minio, books-db…». На самом деле migration 25.05 (`books-vds-stacks-to-portainer`) их мигрировала. Поправить при следующем pass-edit. Сейчас?
|
||||
- **`stack33-put-2026-05-28.json` в `.scratch/`** — он gitignore'нут (transient drop). Если хочется keep как audit artefact — нужно отдельно архивировать.
|
||||
- **epz-поиск end-to-end не подтверждён руками** — я починил место падения (`tenant undefined`), но реальный аутентифицированный запрос из UI не делал (auth-токен). Попросить юзера проверить epz в обоих UI (bookseller.kzntsv.site = bookva, bookva.kzntsv.site = slovo). Если всплывёт downstream (seller-строка в БД / Ozon API) — смотреть логи `books-web`/`bookva-web`.
|
||||
- **harden-ports**: user сказал «пока нет» на немедленный mongo-allowlist. Не начинать без явного запроса. Когда захочет — DOCKER-USER IP-allowlist (firewalld docker не контролит), + mongo 4.2 EOL upgrade + развод reused-пароля `FNud4Um47y`.
|
||||
|
||||
## Не делать (preemptive guards)
|
||||
|
||||
- **Не снимать `action.destructive_requires_name=true`** на canonical ES. Если ОЧЕНЬ нужен wildcard DELETE — сначала остановиться и подумать на каком endpoint.
|
||||
- **Не использовать `ssh + docker compose up -d`** на VDS для управления Portainer stacks — anti-pattern из CLAUDE.md. Только Portainer API / UI (как делали PUT stack 33 в эту сессию).
|
||||
- **Никогда не делиться `elasticsearch.kzntsv.site`** между slovo и bookva. Bookva-es сейчас internal-only через docker network — это правильно. Если когда-нибудь нужен external — отдельный hostname.
|
||||
- **branch ahead by 1 commit от origin/master** — push без explicit user grant (project-discipline Rule 4). Сейчас в working tree 2 unpushed commit'а: `e9b0a9ed` + `48a5cf39`.
|
||||
- **Не возвращать публикацию `:9200`** (и любых stateful-портов) на `0.0.0.0` без IP-allowlist. Auth должна быть НА сервисе, не только в traefik-мидлваре — free-ES auth не имеет.
|
||||
- **Не трогать mongo/mariadb/minio порты** без подтверждения user'а — он сказал mongo «нужен» снаружи. Сначала выяснить кто коннектится (`ss -tnp | grep 27017`).
|
||||
- **Не использовать `ssh + docker compose up -d`** на VDS для Portainer-stacks — только Portainer API (как делали PUT stack 33). Исключение: traefik/portainer (management-plane, ad-hoc compose — как правка `traefik.yml`).
|
||||
- **branch ahead от origin** — push без явного grant'а (project-discipline Rule 4). Этой сессии grant НЕ давали.
|
||||
|
||||
## Memory updates за сессию
|
||||
|
||||
(нет на этом раунде — ничего не сохраняли в `~/.claude/projects/.../memory/`)
|
||||
- Стоит сохранить: **books VDS — docker port-publish обходит firewalld; stateful-сервисы наружу = vector; ransom-бот уже отметился через :9200**. (не сохранял в memory/ — кандидат на следующий заход, см. `[[vds-kzntsv-traefik-and-minio-hostnames]]` соседний контекст books VDS.)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Admin Task Board
|
||||
_Updated: 2026-05-29 — **РЕЦИДИВ того же инцидента, диагноз сменён.** Вчерашняя гипотеза «оператор в cutover» **опровергнута**. Истинная причина: ES публиковал `0.0.0.0:9200` мимо traefik (free-ES без auth) → **ransom-бот** удалял индексы by-name (мимо Control #1), оставлял `read_me` с BTC-выкупом. accessLog (Control #2) пуст — бот шёл прямо в порт. **Control #3 (fix):** убрана публикация host-порта (Portainer PUT stack 33) → дыра закрыта; `epz/products/artmone` restore из `daily-2026-05-25`. Второй, отдельный баг: epz-поиск падал у ОБОИХ тенантов — `config.get("tenant")` (node-config) не задан ни в `default.json`, ни в env-маппинге, `TENANT` env был мёртвым грузом → добавил `tenant` в overlay `default.json` (slovo/bookva), резолвится, ошибки прекратились (products работал — другой код-путь). accessLog откатан (сторожил не ту дверь). Exposure-audit → ⚪ `harden-books-vds-exposed-ports`. См. `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md` § «Рецидив 2026-05-29»._
|
||||
_Updated: 2026-05-28 (вечер) — 🟢 `restore-elasticsearch-indices-books-vds` **closed** в ту же сессию через snapshot restore (46 сек) из `daily-2026-05-25` (полные данные, ~2ч после оригинального reindex'а). RCA: 5 индексов (включая system `.tasks`) удалены через ES API `DELETE _all` за 1 сек на 2026-05-26 10:21 UTC, **1ч 11мин после создания bookva-es** — оператор в cutover-prep попал на canonical вместо internal-only bookva-es. Caller identity не восстановим (audit log = X-Pack платный, traefik accessLog был выключен, Portainer audit = enterprise). 2 preventive фикса applied + verified: (1) ES env `action.destructive_requires_name=true` — `DELETE _all` / wildcard теперь 400; (2) traefik JSON accessLog в `/letsencrypt/access.log` — будущие DELETE оставят forensic след. См. таску + `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md`._
|
||||
_Updated: 2026-05-28 — **prod incident**: books-app slovo поиск товаров + EPZ сломаны, ES `elasticsearch.kzntsv.site` (books VDS stack 33) пуст (0 индексов из 3 ожидаемых; source `elasticold.kzntsv.site` rollback — все 928k docs на месте). Заведена ⚪ `restore-elasticsearch-indices-books-vds`. Окно поломки: 27.05 10:24 → 28.05 13:59, в этом окне шла работа bookva-cutover-prep (bookva-es stack 37 создавался) — возможный конфликт. См. таску для playbook'а._
|
||||
_Updated: 2026-05-28 — **vds-kzntsv network-stack mismatch RESOLVED** в 13:52 MSK. Сначала ~2.5ч активного outage (05:45-08:30) + ~5.5ч на эфемерной статике до окончательного fix хостером. Revised RCA: наш netplan+networkd поверх provider's expected ifupdown stack ломал их auto-recovery когда DHCP-binding разорвался на их стороне. Fix: `systemctl mask netplan systemd-networkd` (на running system, без stop — IP и SSH сохранились), Rusonyx ребутнули + положили чистый `/etc/network/interfaces.d/ifcfg-eth0` с /18 netmask через свой `start/ipadd` procedure. Все 24 docker контейнера up. Disk after GC: 79% (132G→118G/158G). Anti-pattern закреплён: НЕ использовать netplan на Rusonyx VDS. Wiki updated с revised RCA + permanent-fix runbook + 2 quirks (#9 /18 layout, #10 ifupdown vs netplan)._
|
||||
@@ -24,6 +25,16 @@ _Updated: 2026-05-25 (`iis-migration-to-ruvds` 🟢 closed Phase 1 per user deci
|
||||
**Branch:** n/a
|
||||
<!-- created-by: vitya@books-session / 2026-05-28 / trigger: prod 404 index_not_found_exception на /api/epz/search + /api/products/search -->
|
||||
<!-- closed-by: vitya@.admin-exec / 2026-05-28 / snapshot restore + 2 preventive fixes verified -->
|
||||
<!-- REOPENED+RECLOSED: vitya@.admin-exec / 2026-05-29 / рецидив — true RCA = exposed :9200 ransom-бот, Control #3 (порт закрыт) + re-restore + epz tenant-config fix -->
|
||||
|
||||
---
|
||||
|
||||
## ⚪ [harden-books-vds-exposed-ports] — закрыть публично торчащие host-порты на books VDS (89.253.255.133)
|
||||
**Status:** ready
|
||||
**Where I stopped:** exposure-audit сделан (ES :9200 уже закрыт в рамках инцидента); mongo/books-db/bookva-db/minio/bookva-minio + rsync:873 торчат на `0.0.0.0`, все credentialed (auth включён, не дыра-нараспашку как free-ES).
|
||||
**Next action:** по каждому сервису определить — приложение коннектится через docker-сеть или host-порт; нужен ли внешний admin (тогда SSH-туннель / DOCKER-USER IP-allowlist вместо публичного порта). Начать с mongo (EOL 4.2 + reuse пароля).
|
||||
**Branch:** n/a (admin ops)
|
||||
<!-- created-by: vitya@.admin-exec / 2026-05-29 / trigger: exposure-audit во время ES ransom-incident — 5+ stateful-сервисов с публичными host-портами -->
|
||||
|
||||
---
|
||||
|
||||
|
||||
62
.tasks/harden-books-vds-exposed-ports.md
Normal file
62
.tasks/harden-books-vds-exposed-ports.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# harden-books-vds-exposed-ports
|
||||
|
||||
## Goal
|
||||
|
||||
Закрыть/ограничить публично торчащие host-порты на books VDS (89.253.255.133). Выявлены
|
||||
во время инцидента `restore-elasticsearch-indices-books-vds` (2026-05-29): ES был снесён
|
||||
ransom-ботом через открытый `0.0.0.0:9200` в обход traefik+firewalld. Тот же класс дыры
|
||||
(docker-publish обходит firewalld INPUT-зоны) зияет ещё на ряде stateful-сервисов. Цель —
|
||||
ни один stateful-сервис не должен быть доступен с публичного IP напрямую; доступ либо через
|
||||
traefik (под auth), либо по внутренней docker-сети, либо по SSH-туннелю для admin.
|
||||
|
||||
## Exposure audit (2026-05-29, `docker ps` + `ss -tlnp`)
|
||||
|
||||
| Сервис | Порт наружу | Auth на сервисе | Действие |
|
||||
|---|---|---|---|
|
||||
| `elasticsearch` | ~~`0.0.0.0:9200`~~ | ❌ free-ES без auth | ✅ ЗАКРЫТ (Fix #3, инцидент) |
|
||||
| `mongo` | `0.0.0.0:27017` | ✅ enabled | убрать публикацию (app ходит по docker-сети) |
|
||||
| `books-db` (mariadb) | `0.0.0.0:3306` | ✅ root+pass | убрать публикацию / SSH-туннель для admin |
|
||||
| `bookva-db` (mariadb) | `0.0.0.0:33306` | ✅ | убрать публикацию |
|
||||
| `minio` | `0.0.0.0:9000` | ✅ | оставить только через traefik (minio.kzntsv.site) |
|
||||
| `bookva-minio` | `0.0.0.0:9001` | ✅ | то же |
|
||||
| rsync | `:873` | ? проверить | проверить нужен ли наружу |
|
||||
| imgproxy / imgproxy-nginx / proxy-chain | `:8787/8788/8778` | ? | проверить, internal? |
|
||||
|
||||
## Key files / endpoints
|
||||
|
||||
- Portainer API `https://portainer.kzntsv.site` (key в `pass books-vds/full-env`), PUT `/api/stacks/<id>?endpointId=1` — паттерн как в Fix #3 (убрать `ports:` блок из compose).
|
||||
- Stack ids: см. `.wiki/entities/books-vds.md` § Стек (22–33). mongo=31, books-db=32, minio=30, ES=33. bookva-* стек(и) — id уточнить через Portainer API list.
|
||||
- firewalld активен но docker его обходит — закрытие через docker port-publish, НЕ через firewalld rules.
|
||||
|
||||
## Decisions log
|
||||
|
||||
- 2026-05-29: Не закрывать в emergency-режиме инцидента — БД credentialed (не дыра-нараспашку как free-ES), и публикация может использоваться для внешнего admin (DBeaver/Compass/mc). Нужно сперва проверить по каждому: ходит ли приложение через host-порт или по docker-сети, и нужен ли внешний admin-доступ (тогда SSH-туннель вместо публикации).
|
||||
|
||||
## Open questions
|
||||
|
||||
- [ ] Каждый сервис: приложение коннектится через `service-name:port` (docker DNS) или через host-порт? Если через docker-сеть → публикацию убрать безопасно.
|
||||
- [ ] Нужен ли кому-то внешний admin к mongo/mariadb? Если да → SSH local-forward, не публичный порт.
|
||||
- [ ] rsync:873 — это backup-pipeline или забытый демон? Проверить.
|
||||
- [ ] imgproxy/proxy-chain порты — реально нужны наружу или artefact?
|
||||
|
||||
## Completed steps
|
||||
|
||||
- [x] 2026-05-29: exposure audit (таблица выше)
|
||||
- [x] 2026-05-29: ES :9200 закрыт (в рамках инцидент-фикса)
|
||||
- [x] 2026-05-29: проверена auth — mongo/mariadb/minio требуют креды (не auth-less)
|
||||
|
||||
## Notes
|
||||
|
||||
Главный learning из инцидента: auth должна быть **на самом сервисе**, не только в
|
||||
traefik-мидлваре. firewalld не защищает от docker-publish. См.
|
||||
`.wiki/concepts/es-destructive-delete-incident-2026-05-26.md` § «Рецидив 2026-05-29».
|
||||
|
||||
## Branch
|
||||
|
||||
n/a (admin ops)
|
||||
|
||||
## Status
|
||||
|
||||
ready 2026-05-29
|
||||
|
||||
<!-- created-by: vitya@.admin-exec / 2026-05-29 / trigger: exposure-audit во время ES ransom-incident — 5 stateful-сервисов с публичными host-портами -->
|
||||
@@ -93,13 +93,39 @@ products yellow 105922 26.6mb
|
||||
|
||||
**Target (books VDS, Portainer stack 33, canonical):** `https://elasticsearch.kzntsv.site/` — restored 2026-05-28, counts == source, green.
|
||||
|
||||
## ⛔ RECURRENCE 2026-05-29 — RCA был НЕВЕРНЫМ, дыра не закрыта
|
||||
|
||||
Через ~3ч после вчерашнего restore индексы снова исчезли (`artmone`/`epz`/`products`
|
||||
удалены 28.05 20:13 UTC, остался ransom-`read_me`). Вчерашний best-guess «оператор в
|
||||
cutover» **опровергнут**.
|
||||
|
||||
**True root cause:** stack 33 публиковал `ports: - 9200:9200` → docker прокинул
|
||||
`0.0.0.0:9200` на публичный IP в обход traefik+firewalld. ES 7.10 free **без auth** →
|
||||
`curl http://89.253.255.133:9200/` без кредов возвращал cluster info. **Ransom-бот**
|
||||
сканил порт, удалял индексы by-name (мимо вчерашнего Fix #1, который ловит только
|
||||
`_all`/`*`), оставлял `read_me` с требованием 0.0041 BTC. accessLog (Fix #2) пуст по
|
||||
DELETE — бот шёл прямо в `:9200`, не через traefik. bookva не задет (bookva-es порт не
|
||||
публикует).
|
||||
|
||||
**Fix #3 (applied 2026-05-29):** убран `ports:` блок из stack 33 (Portainer PUT
|
||||
`/api/stacks/33?endpointId=1`). Порт больше не публикуется (`docker ps` → `9200/tcp` без
|
||||
`0.0.0.0:`, внешний `curl :9200` refused), traefik route жив. Затем `DELETE /read_me` +
|
||||
restore `epz,products,artmone` из `daily-2026-05-25` (counts 820604/105922/2621, green) +
|
||||
restart `books-api books-task-runner books-job-scheduler`. Поиск slovo через traefik
|
||||
отдаёт хиты. Полный разбор: `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md`
|
||||
§ «Рецидив 2026-05-29».
|
||||
|
||||
**Spawned follow-up:** `harden-books-vds-exposed-ports` — exposure-audit нашёл ещё 5
|
||||
сервисов с публичными host-портами (mongo/books-db/bookva-db/minio/bookva-minio, все
|
||||
credentialed) + rsync:873. Не emergency, но закрыть.
|
||||
|
||||
## Branch
|
||||
|
||||
n/a (admin ops)
|
||||
|
||||
## Status
|
||||
|
||||
closed 2026-05-28
|
||||
closed 2026-05-28 → **reopened+reclosed 2026-05-29** (true RCA: exposed port, Fix #3 applied)
|
||||
|
||||
<!-- created-by: vitya@books-session / 2026-05-28 / trigger: prod slovo поиск товаров+EPZ сломан, ES indices пусты на canonical endpoint -->
|
||||
<!-- closed-by: vitya@.admin-exec / 2026-05-28 / snapshot restore + 2 preventive fixes + wiki concept ingest -->
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
---
|
||||
title: ES destructive delete incident — canonical endpoint vs internal-only bookva-es
|
||||
type: concept
|
||||
tags: [elasticsearch, incident, books-vds, tenant-cutover, operator-error]
|
||||
tags: [elasticsearch, incident, books-vds, ransom-bot, exposed-port, docker-firewalld-bypass]
|
||||
related: [[../entities/books-vds]], [[../sources/books-vds-backup-daily-kreknin-2026-05-25]]
|
||||
updated: 2026-05-28
|
||||
updated: 2026-05-29
|
||||
---
|
||||
|
||||
# ES destructive delete incident — canonical endpoint vs internal-only bookva-es
|
||||
|
||||
> **CORRECTION 2026-05-29 — диагноз сменился.** Первичная гипотеза (оператор в cutover-prep
|
||||
> ошибся endpoint'ом) **опровергнута**. Реальная причина — **ransom-бот через публично
|
||||
> открытый порт `0.0.0.0:9200`** в обход traefik+basicAuth. ES 7.10 free вообще без auth →
|
||||
> голый ES на host-порту отвечал кому угодно без кредов. Доказано: рецидив в ночь
|
||||
> 28→29.05, `read_me` = записка о выкупе (BTC), снос by-name (мимо Control #1), в traefik
|
||||
> accessLog DELETE'ов нет (бот шёл прямо в `:9200`). Полный разбор — секция **«Рецидив
|
||||
> 2026-05-29 — true root cause»** ниже. Секция «Hypothesis» сохранена как опровергнутая.
|
||||
|
||||
## Summary
|
||||
|
||||
В период `bookva-tenant-cutover-prep` (2026-05-26) **дважды** были удалены все user-индексы (`epz`, `products`, `artmone`, плюс system `.tasks` и placeholder `read_me`) на canonical ES `elasticsearch.kzntsv.site` (books VDS stack 33). Удаление выполнено через ES REST API одной командой типа `DELETE _all` / `DELETE *` (5 индексов за <1 секунды). Контейнер не рестартовал, bind-volume не задет — данные стёрты на уровне cluster state.
|
||||
|
||||
**Caller identity не восстановим** — ES audit log = X-Pack платная фича (отсутствует на free 7.10), traefik accessLog был выключен, Portainer audit log = enterprise.
|
||||
|
||||
## Hypothesis (timeline-based, не доказан)
|
||||
## Hypothesis (timeline-based, не доказан) — ⛔ ОПРОВЕРГНУТА 2026-05-29
|
||||
|
||||
> **Эта гипотеза неверна.** Рецидив 29.05 (см. секцию ниже) доказал автоматизированную
|
||||
> атаку, не человеческую ошибку. Оставлено для истории рассуждений.
|
||||
|
||||
Оператор в момент cutover-prep хотел очистить **новый bookva-es** перед заполнением, но команда ушла на canonical `elasticsearch.kzntsv.site` вместо `bookva-es:9200`.
|
||||
|
||||
@@ -67,6 +78,77 @@ EOF
|
||||
|
||||
Если все snapshot'ы посткоммитные (содержат только placeholder), fallback на reindex-from-remote из `elasticold.kzntsv.site` (см. [[../../tasks/migrate-elasticsearch-to-books-vds]] § Playbook). Source ES не выключен per migration policy.
|
||||
|
||||
## Рецидив 2026-05-29 — true root cause (ransom-бот через открытый :9200)
|
||||
|
||||
Через ~3 часа после вчерашнего restore индексы снова исчезли. Разбор дал **настоящую**
|
||||
причину, опровергнув гипотезу оператора.
|
||||
|
||||
### Таймлайн (canonical ES container log)
|
||||
|
||||
```
|
||||
2026-05-28T17:06:25Z epz shard started → GREEN ← restore прошлой сессии
|
||||
2026-05-28T20:13:01Z [artmone] deleting index ← снос by-name
|
||||
2026-05-28T20:13:02Z [epz] deleting index
|
||||
2026-05-28T20:13:09Z [products] deleting index
|
||||
2026-05-28T20:13:13Z [read_me] creating index (bulk api) ← записка о выкупе
|
||||
2026-05-29T04:30:19Z [read_me] deleted + recreated ← повтор (автоматика)
|
||||
```
|
||||
|
||||
### Доказательства
|
||||
|
||||
1. **`read_me` = ransom note.** Содержимое: *«Your database has been deleted... send
|
||||
0.0041 BTC to `bc1q38rjul6gdamfflf6p4ukz0ymtvfgfv2j9saf6r`... email
|
||||
`wendy.etabw@gmx.com` code `0SH7HH1Q72JL`... 48 hours»*. Шаблон ES-ransom-бота.
|
||||
2. **Порт 9200 был открыт в интернет.** Stack 33 compose содержал `ports: - 9200:9200`
|
||||
→ docker публиковал `0.0.0.0:9200` на публичный IP. `curl http://89.253.255.133:9200/`
|
||||
**без `-u`/кредов** вернул cluster info. ES 7.10 free **не имеет встроенной auth**
|
||||
(X-Pack Security платный, выключен) → голый ES отвечал кому угодно.
|
||||
3. **Мимо traefik/basicAuth.** basicAuth — это traefik-мидлвара на `elasticsearch.kzntsv.site:443`,
|
||||
а НЕ в самом ES. Бот шёл прямо в `:9200`, минуя traefik → в accessLog (Control #2)
|
||||
DELETE'ов **нет**. Control #2 ретроспективно бесполезен против этого вектора.
|
||||
4. **Снос by-name** (каждый индекс отдельной командой) → Control #1
|
||||
(`destructive_requires_name=true`) **не ловит** — он блокирует только `_all`/`*`.
|
||||
5. **firewalld активен, но бесполезен** — docker вставляет свои iptables-правила (chain
|
||||
DOCKER) в обход firewalld-зон. `docker-proxy` слушал `*:9200`. Классический
|
||||
docker-publish-bypasses-firewalld.
|
||||
6. **bookva-es НЕ пострадал** — его compose порт не публикует (`PortBindings: {}`),
|
||||
доступ только по внутренней docker-сети `proxy`. Данные целы (epz 820604 + products
|
||||
105922). Подтверждает: вектор — именно публикация порта, не сам ES.
|
||||
|
||||
### Реальный fix (applied 2026-05-29) — Control #3
|
||||
|
||||
**Убрать публикацию host-порта** из stack 33 (Portainer PUT `/api/stacks/33?endpointId=1`,
|
||||
убран блок `ports: - 9200:9200`). После recreate: `docker ps` показывает `9200/tcp` без
|
||||
`0.0.0.0:` префикса, host-listener на 9200 исчез, внешний `curl :9200` → refused. Traefik
|
||||
route (`elasticsearch.kzntsv.site`, под basicAuth) и внутренний docker-доступ работают —
|
||||
приложение ходит в ES только через traefik, host:9200 ему не нужен. **Это закрывает вектор**
|
||||
(Control #1/#2 лечили симптом/forensics, не дыру).
|
||||
|
||||
Затем: `DELETE /read_me`, restore `epz,products,artmone` из `daily-2026-05-25` (single good
|
||||
snapshot — 26–29 содержат только `read_me`, бот успевал снести до 03:00 UTC backup'а),
|
||||
restart `books-api books-task-runner books-job-scheduler`. Counts восстановлены (green).
|
||||
|
||||
### Exposure audit (2026-05-29) — та же дыра на других сервисах
|
||||
|
||||
`docker ps` + `ss -tlnp` на books VDS: публично опубликованы host-портами также:
|
||||
|
||||
| Сервис | Порт наружу | Auth | Статус |
|
||||
|---|---|---|---|
|
||||
| `mongo` | `0.0.0.0:27017` | ✅ enabled (`Unauthorized` без кредов) | exposed, credentialed |
|
||||
| `books-db` (mariadb) | `0.0.0.0:3306` | ✅ root требует пароль | exposed, credentialed |
|
||||
| `bookva-db` (mariadb) | `0.0.0.0:33306` | ✅ | exposed, credentialed |
|
||||
| `minio` | `0.0.0.0:9000` | ✅ `AccessDenied` анонимно | exposed, credentialed |
|
||||
| `bookva-minio` | `0.0.0.0:9001` | ✅ | exposed, credentialed |
|
||||
| rsync | `:873` | ? | exposed |
|
||||
|
||||
Все БД **имеют auth** (в отличие от ES) → не дыра-нараспашку, но публикация портов БД
|
||||
наружу — плохая практика (brute-force / leaked-creds). Не emergency, рискованнее закрывать
|
||||
(возможен внешний admin-доступ). Follow-up hardening task, не часть инцидент-фикса.
|
||||
|
||||
**Главный learning:** любой stateful-сервис с публикуемым host-портом на VDS = публичный
|
||||
вектор в обход traefik+firewalld. Auth должна быть **на самом сервисе**, не только в
|
||||
traefik-мидлваре. ES free auth не имеет → его порт публиковать нельзя НИКОГДА.
|
||||
|
||||
## Preventive controls (applied 2026-05-28)
|
||||
|
||||
### Control #1 — ES `action.destructive_requires_name=true`
|
||||
|
||||
@@ -24,7 +24,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
|
||||
- [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
|
||||
- [docker-host-loopback-detect](concepts/docker-host-loopback-detect.md) — Docker host-loopback detection — как доказать что host.docker.internal не петля
|
||||
- [es-destructive-delete-incident-2026-05-26](concepts/es-destructive-delete-incident-2026-05-26.md) — ES destructive delete на canonical endpoint в момент bookva cutover-prep — RCA + recovery runbook + preventive controls (`action.destructive_requires_name=true` + traefik accessLog)
|
||||
- [es-destructive-delete-incident-2026-05-26](concepts/es-destructive-delete-incident-2026-05-26.md) — ES indices wiped on canonical — **true root cause (2026-05-29): ransom-бот через открытый `0.0.0.0:9200` мимо traefik, free-ES без auth**; гипотеза operator-error опровергнута. Fix = убрать публикацию host-порта (Control #3) + restore. Exposure-audit: mongo/mariadb/minio тоже exposed (credentialed)
|
||||
- [future-resilient-architecture-goals](concepts/future-resilient-architecture-goals.md) — fault-tolerance roadmap placeholder (расширяется через `[resilience-roadmap-design]`)
|
||||
- [hyper-backup-structure-and-recovery](concepts/hyper-backup-structure-and-recovery.md) — Hyper Backup — структура репо и стратегия восстановления
|
||||
- [iis-migration-2026-05-19-postmortem](concepts/iis-migration-2026-05-19-postmortem.md) — post-mortem миграции CMS на нативный IIS, 2026-05-19
|
||||
|
||||
@@ -29,3 +29,5 @@ Append-only log of wiki operations (ingests, promotions, lints, migrations).
|
||||
## [2026-05-28] ingest | vds-kzntsv DHCP outage postmortem — concepts/vds-kzntsv-dhcp-outage-2026-05-28 (NEW, симптомокартина + диагностический алгоритм + recovery runbook + RCA) + sources/vds-kzntsv-incident-2026-05-28 (NEW, timeline 05:25 backup OK → 08:15 detect → 08:43 statics fix → 08:50 disk GC; ticket text) + UPDATE entities/vds-kzntsv (Доступ §+subnet/gw/hypervisor/DNS resolvers; pass-store вместо .common/secrets; Known issues §NEW with 2026-05-28 incident; Open issues bump GC priority + iputils-ping note) + UPDATE concepts/rusonyx-vps-onboarding-quirks (quirk #9 NEW — gw в /18 надсети + recovery commands). Live session, no raw source ingested.
|
||||
|
||||
## [2026-05-28] update | vds-kzntsv DHCP outage post-resolution — после reset хостером в 13:52 MSK выяснилось что root cause — конфликт двух сетевых стеков (netplan+networkd поверх ожидаемого provider's ifupdown). Их `start/ipadd` ожидает чистый ifupdown, не мог auto-recover при разрыве DHCP binding. Resolution: mask netplan+systemd-networkd, reboot, provider положил `/etc/network/interfaces.d/ifcfg-eth0` с /18 netmask. UPDATE concepts/vds-kzntsv-dhcp-outage-2026-05-28 (revised RCA + permanent-fix § + anti-pattern + revised lessons-learned) + UPDATE sources/vds-kzntsv-incident-2026-05-28 (timeline до 14:00 + final config) + UPDATE entities/vds-kzntsv (mask /18, ifupdown stack, kernel cmdline net.ifnames=0) + UPDATE concepts/rusonyx-vps-onboarding-quirks (quirk #9 переписан про /18, quirk #10 NEW про ifupdown vs netplan stack). Memory `vds-kzntsv-rusonyx-network-recovery` переписан с новыми фактами.
|
||||
|
||||
## [2026-05-29] update | ES destructive-delete RECURRENCE — true root cause найден, диагноз сменился. UPDATE concepts/es-destructive-delete-incident-2026-05-26 (correction-блок + Hypothesis помечена ОПРОВЕРГНУТА + новая секция «Рецидив 2026-05-29 — true root cause»: ransom-бот через открытый `0.0.0.0:9200` мимо traefik+basicAuth, ES 7.10 free без auth; `read_me`=BTC-выкуп; снос by-name мимо Control #1; accessLog пуст т.к. бот шёл прямо в :9200; firewalld bypass docker-publish; Control #3 = убрать публикацию host-порта via Portainer PUT stack 33; restore из daily-2026-05-25; exposure-audit таблица — mongo/books-db/bookva-db/minio/bookva-minio тоже exposed но credentialed). bookva не пострадал (bookva-es порт не публикует). Live incident response session.
|
||||
|
||||
Reference in New Issue
Block a user