Рецидив вчерашнего 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>
132 lines
9.5 KiB
Markdown
132 lines
9.5 KiB
Markdown
# restore-elasticsearch-indices-books-vds
|
||
|
||
## Goal
|
||
|
||
Восстановить 3 ES индекса (`epz`, `products`, `artmone`) на canonical endpoint `elasticsearch.kzntsv.site` (books VDS, Portainer stack 33). Сейчас target ES пустой → весь поиск в books-app (slovo) сломан (404 `index_not_found_exception` для `epz` и `products`).
|
||
|
||
## Симптомы (зафиксировано 2026-05-28 ~15:50 MSK)
|
||
|
||
- `bookva.kzntsv.site/api/products/search?q=...` → 500 `no such index [products]`.
|
||
- `bookva.kzntsv.site/api/epz/search?q=...` → 500 `no such index [epz]`.
|
||
- `books-api` + `books-web` логи (stderr) — десятки `ResponseError: index_not_found_exception` за час.
|
||
- User-facing impact: страница `/epz/search` и поиск товаров в slovo UI не работают.
|
||
|
||
## Closure note (2026-05-28 ~20:10 MSK)
|
||
|
||
🟢 **Восстановлено за 46 сек через snapshot restore** + два preventive фикса в один заход.
|
||
|
||
### Restore
|
||
|
||
```
|
||
POST /_snapshot/kreknin/daily-2026-05-25/_restore?wait_for_completion=true
|
||
{"indices":"epz,products,artmone","include_global_state":false,"include_aliases":true,"index_settings":{"number_of_replicas":0}}
|
||
```
|
||
|
||
- artmone: 2621 docs ✓ (source: 2621)
|
||
- epz: 820604 docs ✓ (source: 820604)
|
||
- products: 105922 docs ✓ (source: 105922)
|
||
- cluster: green
|
||
- 3 consumers (books-api/task-runner/job-scheduler) рестартованы — 0 `index_not_found_exception` в логах за 5 мин
|
||
- public smoke `bookva.kzntsv.site/api/{epz,products}/search` → 401 от auth middleware (поиск-pipeline жив, до wipe было 500)
|
||
|
||
**Ключевая удача:** в исходной задаче было записано «`pre-migration-2026-05-25` содержит только `read_me` placeholder — не годится для восстановления» — это правда, но это был **другой** snapshot (он был _до_ reindex'а). А `daily-2026-05-25` (сделан кроном `books-vds-backup-daily-kreknin` в 09:51 UTC через 2ч _после_ reindex'а) содержал full corpus — `[read_me, products, epz, artmone, .tasks]`, 25 sec duration, 5 шардов SUCCESS. Snapshot pipeline (стояший с 25.05) автоматически создал safety net.
|
||
|
||
### Root cause (forensics из ES container log)
|
||
|
||
Container `elasticsearch` `restart_count=0, created=2026-05-25T07:51:38` — **никогда не рестартовал**, volume bind не задет, индексы существовали и были **удалены через ES API**:
|
||
|
||
```
|
||
2026-05-26 10:21:43.526 UTC [read_me/IfM2V1pTQk-...] deleting index
|
||
2026-05-26 10:21:44.075 UTC [artmone/xQdFWkafSKy-...] deleting index
|
||
2026-05-26 10:21:44.257 UTC [epz/AV8inT4YS-e-...] deleting index
|
||
2026-05-26 10:21:44.450 UTC [.tasks/EbutxIzdTPSq-...] deleting index
|
||
2026-05-26 10:21:44.576 UTC [products/6VRXsuP5QV-...] deleting index
|
||
```
|
||
|
||
5 индексов (включая system `.tasks`) удалены за 1 секунду = `DELETE _all` / `DELETE *` / Kibana DevTools «delete index».
|
||
|
||
Timeline:
|
||
- **2026-05-25 09:51 UTC** — daily snapshot SUCCESS [read_me, products, epz, artmone, .tasks] (full data)
|
||
- **между ~10:00 и 26.05 03:02 UTC** — Wipe #1 (snapshot 26.05 содержит только [read_me])
|
||
- **2026-05-26 05:49–06:44 UTC** — re-reindex (artmone+epz+products, full counts восстановлены)
|
||
- **2026-05-26 09:10:18 UTC** — `bookva-es` container created (cutover-prep Step 2)
|
||
- **2026-05-26 10:21:43 UTC** — Wipe #2 (1ч 11мин после создания bookva-es)
|
||
|
||
`bookva-es` использует named volume `bookva-es-data`, БЕЗ traefik labels (internal-only). Не задел canonical через mount или routing. **Best guess:** оператор в момент cutover-prep хотел очистить новый `bookva-es:9200`, но команда ушла на `elasticsearch.kzntsv.site` (canonical, slovo). Bookva-es internal-only → DELETE с воркстейшна туда не дойти; canonical через traefik basicAuth → доходит.
|
||
|
||
**Caller identity unrecoverable:** ES audit log = X-Pack платная фича (нет на free 7.10). Traefik accessLog был отключён (нет секции `accessLog:` в `traefik.yml`). Portainer audit = CE без enterprise. Только timestamp + DELETE fact в ES log.
|
||
|
||
### Preventive fixes (applied 2026-05-28 ~20:10 MSK)
|
||
|
||
**Fix #1: `action.destructive_requires_name=true`** на stack 33 (env var добавлен через Portainer PUT API). Verified:
|
||
- `DELETE /_all` → 400 «Wildcard expressions or all indices are not allowed»
|
||
- `DELETE /ep*` → 400 same
|
||
- `DELETE /probe-canary` (by exact name) → 200 (легитимные операции работают)
|
||
|
||
Pattern удаления который случился (5 индексов в 1 сек) теперь **физически невозможен**.
|
||
|
||
**Fix #2: traefik accessLog** в JSON формат, `/letsencrypt/access.log` (bind-mounted, persistent). Backup `traefik.yml.bak-pre-accesslog-2026-05-28` рядом. Verified entries содержат `RequestMethod`, `RequestHost`, `RequestPath`, `ClientHost`, `ClientUsername`, `DownstreamStatus`, `TLSCipher`. Будущие `DELETE` запросы оставят след — даже если кто-то снова попадёт не на тот endpoint, retrospective forensics возможен.
|
||
|
||
### Artifacts
|
||
|
||
- `.scratch/stack33-put-2026-05-28.json` — Portainer PUT payload (новый env + сохранён `reindex.remote.whitelist`).
|
||
- `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md` — incident concept (написан в эту сессию).
|
||
- ES container env post-fix: `reindex.remote.whitelist=elasticold.kzntsv.site:443` + `action.destructive_requires_name=true`.
|
||
- Traefik static config: `accessLog: { filePath: /letsencrypt/access.log, format: json, bufferingSize: 100 }`.
|
||
|
||
### Follow-ups (не блокеры)
|
||
|
||
- **Logrotate на `access.log`** — при ~1 req/sec будет ~30–50 MB/day. Disk free 63G, ressedimption через 1–2 года максимум — но добавить logrotate стоит. Отдельная micro-task.
|
||
- **Переезд access.log из `/letsencrypt/`** в отдельный bind (`/usr/docker/traefik/logs/`) — косметика, текущее место persistent и работает.
|
||
- **Pass entry update** — комментарий в `books-vds/full-env` устарел: «NOT Portainer-managed (legacy SSH-compose): **elasticsearch**, mongo, minio, books-db…». Migration 25.05 (`books-vds-stacks-to-portainer`) уже мигрировала их. Поправить при следующем pass-edit.
|
||
|
||
## Текущее состояние ES endpoint'ов
|
||
|
||
**Source (Windows host, rollback — не выключен):** `https://elasticold.kzntsv.site/`
|
||
|
||
```
|
||
index health docs.count store.size
|
||
artmone yellow 2621 1.4mb
|
||
epz yellow 820604 699.7mb
|
||
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 → **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 -->
|