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:
2026-05-29 09:31:25 +03:00
parent 63708fe659
commit 063910e265
7 changed files with 214 additions and 36 deletions

View File

@@ -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 — 2629 содержат только `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`

View File

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

View File

@@ -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.