Compare commits

..

3 Commits

Author SHA1 Message Date
63708fe659 tasks(NEXT_SESSION): session-close — ES restore + 2 preventive controls live
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:12:11 +03:00
48a5cf397e tasks(restore-es-indices-books-vds): closed — snapshot restore + 2 preventive controls
snapshot restore из kreknin:daily-2026-05-25 за 46 сек (epz=820604,
products=105922, artmone=2621, counts == source).

RCA: 5 индексов (включая system .tasks) удалены через ES API DELETE _all
за 1 сек на 2026-05-26 10:21 UTC — 1ч 11мин после создания bookva-es в
cutover-prep. Каноничный endpoint elasticsearch.kzntsv.site попал под
команду которая предназначалась bookva-es:9200 (internal-only, без
traefik route). Caller identity unrecoverable: ES audit = X-Pack платный,
traefik accessLog был выключен, Portainer CE без audit.

Preventive controls applied + verified:
1. ES env action.destructive_requires_name=true (stack 33) — DELETE _all
   и wildcard теперь 400 BadRequest; by-name DELETE работает (нужно для
   reindex). Pattern удаления что случился физически невозможен.
2. Traefik JSON accessLog в /letsencrypt/access.log — будущие DELETE
   оставят forensic след с IP/user/method/path.

Wiki concept: .wiki/concepts/es-destructive-delete-incident-2026-05-26.md
с recovery runbook + preventive controls + cross-refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:11:07 +03:00
e9b0a9edbc docs(tasks): restore-elasticsearch-indices-books-vds — books VDS ES stack 33 пуст
Prod incident в репо books: slovo поиск товаров+EPZ лежит. `elasticsearch.kzntsv.site`
отвечает 200 на _cluster/health (green, 0 active shards, 0 docs); source
`elasticold.kzntsv.site` (Windows rollback) — полный набор: epz 820604 /
products 105922 / artmone 2621. Окно поломки 27.05 10:24 → 28.05 13:59, в нём
шла bookva-tenant-cutover-prep (bookva-es stack 37 создавался) — возможный
конфликт.

Playbook в task-файле: forensics (Portainer stack 33 audit + docker volume
inspect) → snapshot restore из kreknin repo если post-25.05 snapshot с
непустыми индексами → иначе reindex повторно по migrate-elasticsearch-to-books-vds
procedure (730MB ~5-10 мин). После — restart 4 consumer'ов.

NEXT_SESSION.md перезаписан, restore поставлен как приоритет 1, carried
items (registry GC, netplan artifacts, board-viewer-build) сохранены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:50:13 +03:00
6 changed files with 289 additions and 64 deletions

View File

@@ -1,72 +1,56 @@
---
_last_updated_: 2026-05-28T14:15:00+03:00
session_id: 2026-05-28-vds-kzntsv-network-stack-mismatch
_last_updated_: 2026-05-28T20:30:00+03:00
session_id: 2026-05-28-restore-es-indices-books-vds-closure
---
# Next session handoff
## Контекст сессии
vds-kzntsv (89.253.255.94) был недоступен по сети ~05:4508:30 MSK 28 мая 2026 (~2.5ч активного outage). С 08:43 жил на эфемерной статике (через VNC) до окончательного fix хостером в **13:52 MSK** — они переписали конфиг через свой `start/ipadd` procedure, мы предварительно masked netplan + systemd-networkd через SSH.
**Revised RCA** (после resolution): наш сетевой стек был mixed (netplan+networkd поверх provider's expected ifupdown). 8 дней работало потому что networkd сам получал DHCP. Когда что-то на стороне Rusonyx разорвало DHCP-binding (28.05 утром) — их auto-recovery `start/ipadd` не смогла применить static config, потому что networkd «держал» eth0. Fix = mask netplan/networkd, оставить только ifupdown.
## LIVE сейчас
- **vds-kzntsv** работает на чистом ifupdown stack: `/etc/network/interfaces.d/ifcfg-eth0` (provider-managed), netmask **/18** (`255.255.192.0`), gw `89.253.192.1`. `netplan` + `systemd-networkd*` masked.
- **24 docker контейнера up** включая весь стек (gitea, registry, verdaccio, postgres, mariadb, mongo, redis, owncloud/oCIS, modulair-rag×4, mssql, board-viewer, traefik, portainer, ntfy, vds-ops-mcp, vds-docker-proxy-ro). **1 unhealthy**`board-viewer-build` (предсуществующий, не связан с инцидентом).
- **Диск 79%** (118G/158G) — после quick GC сегодня утром (build cache 10.6G + dangling 0.3G + container logs ~3G truncated). Registry GC ~20G storage **не сделан** — pending.
- **Wiki, memory, STATUS** — обновлены с финальным RCA. Commit за сессию pending (см. ниже).
## Что делать на следующей сессии (по приоритету)
### 1. Cleanup netplan-artifacts (опционально, низкий приоритет)
После resolution на сервере остались netplan конфиги (не активны, netplan masked, но лежат):
- `/etc/netplan/01-eth0.yaml` — мы создали 28.05 утром при попытке fix через netplan
- `/etc/netplan/50-cloud-init.yaml` — от cloud-init
- `/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg` — мы создали (с опечаткой `disbaled` в одной из попыток, потом исправили)
Можно удалить или оставить — они не влияют (netplan masked). **Если** соберёмся когда-нибудь снять mask с netplan — следует тогда clean up. Сейчас не трогаем.
### 2. Registry GC (~20G storage reclaim — pending)
Из task #2 в TaskList. Окно read-only=true на 5-15 мин. **Делать только в low-traffic окно** — registry push/pull временно недоступен.
```bash
# на VDS
sudo docker exec registry registry garbage-collect --delete-untagged=true /etc/docker/registry/config.yml
# проверка размера до/после
sudo du -sh /opt/stacks/registry
```
### 3. board-viewer-build unhealthy — разбор
Предсуществующий, не связан с инцидентом. Если беспокоит — `sudo docker logs board-viewer-build | tail -50` + проверить healthcheck в compose. Иначе можно `--no-healthcheck` если он не нужен в running container.
### 4. SLA / RCA от Rusonyx — follow up (опционально)
В первом нашем тикете запрашивали технический RCA + SLA-компенсацию. User просил это не муссировать. На усмотрение — можно через несколько дней спросить «приходил ли формальный RCA», но force'ить не нужно.
## Спроси user'а
- **Registry GC** — окно ~5-15 мин в read-only=true, делать ночью или сейчас?
- **Cleanup netplan artifacts** — оставить как есть или зачистить `/etc/netplan/*`? (Low-priority)
- **Push wiki + memory + tasks** — push'нуть commit'нутые изменения в origin? (Per project-discipline нужен per-session grant, пока не давал)
## Не делать (preemptive guards)
- **НЕ unmask netplan/systemd-networkd** на vds-kzntsv. Anti-pattern для Rusonyx (см. concept).
- **НЕ редактировать `/etc/network/interfaces` или `/etc/network/interfaces.d/ifcfg-eth0`** — provider's start/ipadd procedure перезапишет при любых их manipulations с VM (resize, network reset).
- **НЕ reboot VDS без необходимости** — конфиг теперь правильный, но любой их network-action может что-то поменять, и проверить новое состояние придётся через VNC.
- **НЕ делать registry GC без `read-only=true` mode** — rogue push может corrupt'ить blobs.
## Memory updates за сессию
- **UPDATE** `vds-kzntsv-rusonyx-network-recovery.md` — переписан с revised RCA: provider stack = ifupdown, netmask /18, canonical bootstrap-step `mask netplan/networkd`. Anti-pattern: netplan на Rusonyx.
- **Уточнение паттерна:** при сетевых проблемах на Rusonyx VDS первым делом проверять что стек = ifupdown (не netplan). `systemctl is-enabled netplan systemd-networkd networking` — должно быть `masked masked enabled`.
_Сессия: восстановили 3 ES индекса на canonical (books VDS stack 33) через
snapshot restore + добавили 2 preventive control'а против повтора. Таска
🟢 closed. Wiki concept + closure note записаны._
## Recent commits
- (pending — этот сеанс ещё не commit'ил; wiki + memory + STATUS + NEXT_SESSION готовы к одному commit'у).
- `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
## Что закрыли в эту сессию
- **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 обновлены.
## Open треки
| Трек | Готовность | Entry-point |
|---|---|---|
| `books-bookva-user-whitelist-gathering` | ⚪ ready — gathering от учредителя Bookva | `.tasks/STATUS.md` line ~221 |
| `stateful-split-volume-copy` | ⚪ ready — maintenance window 510 мин | `.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 |
Активных 🔴 / 🟡 нет.
## Спроси user'а
- **Logrotate для `access.log`** (~3050 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 — нужно отдельно архивировать.
## Не делать (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`.
## Memory updates за сессию
(нет на этом раунде — ничего не сохраняли в `~/.claude/projects/.../memory/`)

View File

@@ -1,4 +1,6 @@
# Admin Task Board
_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)._
_Updated: 2026-05-27 — `modulair-rag-vds-redeploy` 🟢 **closed** в ту же сессию: 4-контейнерный стек развёрнут на VDS (Portainer stack 15), acceptance 6/6. Образы пересобраны на самом VDS (push 3.36GB через traefik с дома падал 499); env/entrypoint/minio-host скорректированы под VDS-реальность. 3 follow-up'а переданы в modulair-rag handoff._
_Updated: 2026-05-27 — заведена `modulair-rag-vds-redeploy` ⚪ (handoff из modulair-rag session). NAS-loss redeploy 4 контейнеров на VDS. Блокер MinIO снят — verified up на VDS 2026-05-27. postgres `proxy`-network reachability подтверждён (`postgres:5432` резолвится из pipeline/mcp). Спека: modulair-rag concept `nas-loss-vds-redeploy-context` + compose.yml as-is._
@@ -18,6 +20,13 @@ _Updated: 2026-05-26 — заведена `bookva-tenant-cutover-prep` ⚪ (7-st
_Updated: 2026-05-25 (`iis-migration-to-ruvds` 🟢 closed Phase 1 per user decision — 9/24 hostnames live на RUVDS, source IIS оставлен running. `migrate-elasticsearch-to-books-vds` 🟢 closed ранее сегодня. 16 IIS hostnames + LE renewal pipeline + decommission — descoped в Closure note, не отдельные tracker tasks.)_
## 🟢 [restore-elasticsearch-indices-books-vds] — closed 2026-05-28 — snapshot restore из `kreknin:daily-2026-05-25` за 46 сек (epz=820604, products=105922, artmone=2621, counts == source). Forensics: 5 индексов удалены через ES API `DELETE _all` 26.05 10:21 UTC, через 1ч 11мин после создания `bookva-es` — оператор в cutover-prep попал на canonical вместо internal-only bookva. Caller identity unrecoverable (audit log = X-Pack платный, traefik accessLog был выключен). 2 preventive фикса applied: ES env `action.destructive_requires_name=true` + traefik JSON accessLog в `/letsencrypt/access.log`. См. [restore-elasticsearch-indices-books-vds.md](restore-elasticsearch-indices-books-vds.md) § Closure + `.wiki/concepts/es-destructive-delete-incident-2026-05-26.md`.
**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 -->
---
## 🟢 [modulair-rag-vds-redeploy] — closed 2026-05-27 — 4-контейнерный стек развёрнут на VDS, acceptance 6/6
NAS помер → 4 контейнера пропали → fresh redeploy на VDS (89.253.255.94). Portainer stack id=15. **Acceptance 6/6 green:** 4 контейнера up без restart-loop; lightrag Uvicorn :9621; `https://modulair-mcp.kzntsv.site`→200; pipeline+tier1 0% CPU; pipeline-лог без postgres/minio ошибок.

View File

@@ -0,0 +1,105 @@
# 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:4906: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 будет ~3050 MB/day. Disk free 63G, ressedimption через 12 года максимум — но добавить 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.
## Branch
n/a (admin ops)
## Status
closed 2026-05-28
<!-- 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 -->

View File

@@ -0,0 +1,124 @@
---
title: ES destructive delete incident — canonical endpoint vs internal-only bookva-es
type: concept
tags: [elasticsearch, incident, books-vds, tenant-cutover, operator-error]
related: [[../entities/books-vds]], [[../sources/books-vds-backup-daily-kreknin-2026-05-25]]
updated: 2026-05-28
---
# ES destructive delete incident — canonical endpoint vs internal-only bookva-es
## 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, не доказан)
Оператор в момент cutover-prep хотел очистить **новый bookva-es** перед заполнением, но команда ушла на canonical `elasticsearch.kzntsv.site` вместо `bookva-es:9200`.
```
2026-05-25 09:51 UTC daily snapshot SUCCESS [read_me, products, epz, artmone, .tasks] ← данные ЕСТЬ
...
2026-05-26 03:02 UTC daily snapshot success [read_me] only ← Wipe #1 случился в окне
2026-05-26 05:4906:44 UTC re-reindex artmone+epz+products восстанавливают данные
2026-05-26 09:10:18 UTC bookva-es container created (cutover-prep Step 2)
2026-05-26 10:21:4344 UTC Wipe #2: 5 индексов удалены за 1 сек ← 1ч 11мин после bookva-es
```
`bookva-es` использует named volume `bookva-es-data`, БЕЗ traefik labels (internal-only через docker network `proxy`). Не задел canonical через volume mount или routing. `DELETE` с воркстейшна на `bookva-es:9200` физически невозможен (порт не expose'нут наружу); на `elasticsearch.kzntsv.site` — возможен через traefik basicAuth.
## Forensics — что есть и чего нет
| Источник | Доступность | Содержание |
|---|---|---|
| ES container log `docker logs elasticsearch` | ✅ есть | `o.e.c.m.MetadataDeleteIndexService` пишет факт + index UUID + timestamp. Caller отсутствует |
| ES audit log | ❌ нет | X-Pack Security платная фича. Free 7.10 audit отключён |
| Traefik accessLog | ❌ был выключен | Файл `traefik.yml` не содержал секции `accessLog:`. Запросы (включая DELETE) не пишутся |
| Portainer audit | ❌ нет | Portainer CE без enterprise = audit log не включён |
| Snapshot diff `_snapshot/kreknin/_all` | ✅ есть | Daily snapshots показывают какой день имеет какие индексы → окно wipe'а |
| Shell history `~/.bash_history` root | ⚠️ ограниченно | Только если delete был сделан с самого VDS. Если с воркстейшна / Kibana DevTools — пусто |
## Recovery procedure
Если индексы исчезли — **snapshot restore** из `kreknin` repo. Daily snapshot pipeline (`books-vds-backup-daily-kreknin`, cron 06:00 MSK) автоматически снапшотит непустые индексы. До wipe'а snapshot контейнерит full data → restore = ~46 сек для ~800MB (I/O bound, файлы уже на диске).
```bash
ssh -i ~/.ssh/id_ed25519_books_ops root@89.253.255.133 'bash -s' <<'EOF'
PW=$(python -c "import json; print(json.load(open('/opt/books/api/config/default.json'))['elasticsearch']['auth']['password'])")
# 1. Найти последний snapshot С полными данными
curl -sk -u "books:$PW" 'https://elasticsearch.kzntsv.site/_snapshot/kreknin/_all' \
| python -c "import json,sys; [print(s['snapshot'], s['indices']) for s in json.load(sys.stdin)['snapshots']]"
# 2. Restore (synchronous, не overwrites open indices — current state должен быть пустой)
curl -sk -u "books:$PW" -H 'Content-Type: application/json' -X POST \
'https://elasticsearch.kzntsv.site/_snapshot/kreknin/daily-<YYYY-MM-DD>/_restore?wait_for_completion=true' \
-d '{"indices":"epz,products,artmone","include_global_state":false,"include_aliases":true,"index_settings":{"number_of_replicas":0}}'
# 3. Verify counts == source
curl -sk -u "books:$PW" 'https://elasticsearch.kzntsv.site/_cat/indices?v'
# 4. Restart consumers (reset connection pool)
docker restart books-api books-task-runner books-job-scheduler
EOF
```
Если все snapshot'ы посткоммитные (содержат только placeholder), fallback на reindex-from-remote из `elasticold.kzntsv.site` (см. [[../../tasks/migrate-elasticsearch-to-books-vds]] § Playbook). Source ES не выключен per migration policy.
## Preventive controls (applied 2026-05-28)
### Control #1 — ES `action.destructive_requires_name=true`
Env var на stack 33 (Portainer-managed). После применения wildcard/_all delete возвращают **400 Bad Request**:
```
$ curl -sk -u books:<pw> -X DELETE 'https://elasticsearch.kzntsv.site/_all'
{"error":{"type":"illegal_argument_exception","reason":"Wildcard expressions or all indices are not allowed"},"status":400}
$ curl -sk -u books:<pw> -X DELETE 'https://elasticsearch.kzntsv.site/ep*'
{"error":{"type":"illegal_argument_exception","reason":"Wildcard expressions or all indices are not allowed"},"status":400}
$ curl -sk -u books:<pw> -X DELETE 'https://elasticsearch.kzntsv.site/epz'
{"acknowledged":true} # legit by-name delete still works
```
Класс ошибки «DELETE wildcard на неправильный endpoint» теперь блокируется ES до того, как достанется до данных. Reindex по одному индексу остаётся легитимным (нужно для миграции). Полностью **физически невозможно** удалить >1 индекса одной командой.
Apply: добавить `action.destructive_requires_name: "true"` в `environment:` блок stack 33 compose, PUT через Portainer API endpoint `/api/stacks/33?endpointId=1`. ES container recreate ~10 сек, consumers переподключатся через retry.
### Control #2 — Traefik accessLog (JSON)
Без этого никакая ретроспективная диагностика невозможна. Static config в `/usr/docker/traefik/data/traefik.yml`:
```yaml
accessLog:
filePath: /letsencrypt/access.log
format: json
bufferingSize: 100
```
`/letsencrypt/` уже bind-mounted (`./letsencrypt:/letsencrypt` в `/usr/docker/traefik/docker-compose.yml`), файл persistent. Restart container — изменения подхватились (accessLog = static config, не dynamic).
Sample entry:
```json
{"ClientHost":"...", "ClientUsername":"books", "RequestMethod":"DELETE", "RequestHost":"elasticsearch.kzntsv.site",
"RequestPath":"/_all", "RouterName":"elasticsearch@docker", "DownstreamStatus":400, ...}
```
Будущие подозрительные запросы видны с IP, username basicAuth, methodом, путём, response status. Логирование = ~3050 MB/day при типичной нагрузке (~1 req/sec).
## Follow-ups
- **Logrotate** на `/usr/docker/traefik/letsencrypt/access.log` — пока без него файл будет расти; не критично (disk free 63G, ressedimption ~12 года), но micro-task на logrotate cron имеет смысл.
- **bookva-es traefik route** — если когда-нибудь bookva-es потребует внешнего доступа, обязательно отдельный hostname (`bookva-es.kzntsv.site`) с отдельной basicAuth. **Никогда** не делиться `elasticsearch.kzntsv.site` между slovo/bookva.
- **Snapshot retention review** — сейчас 7 daily snapshots. Если бы инцидент проявился через 8 дней, daily-25.05 был бы вытеснен. Для критичных корпусов имеет смысл добавить weekly/monthly tier. Отдельная мини-задача.
- **Логирование callers без accessLog ретроспективно невозможно** — это main learning. Любой новый production endpoint должен иметь access log с момента создания.
## Cross-refs
- [[../entities/books-vds]] § Стек, ES — stack 33 inventory.
- [[../sources/books-vds-backup-daily-kreknin-2026-05-25]] — backup pipeline который сделал spasibo daily-25.05 snapshot.
- [[../../tasks/restore-elasticsearch-indices-books-vds]] — incident response trace (этой сессии).
- [[../../tasks/migrate-elasticsearch-to-books-vds]] — original migration playbook (применим для reindex-from-remote fallback).

View File

@@ -24,6 +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)
- [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

@@ -2,6 +2,8 @@
Append-only log of wiki operations (ingests, promotions, lints, migrations).
## [2026-05-28] ingest | concepts/es-destructive-delete-incident-2026-05-26 — RCA для удаления 3 user-индексов на canonical ES `elasticsearch.kzntsv.site` (books VDS stack 33) во время cutover-prep. Caller identity unrecoverable (audit log = X-Pack платный, traefik accessLog был выключен). 2 preventive controls applied + verified: ES env `action.destructive_requires_name=true` + traefik JSON accessLog. Источник: `.tasks/restore-elasticsearch-indices-books-vds.md` § Closure note.
## 2026-05-21
- bootstrap: empty wiki skeleton (CLAUDE.md, index.md, log.md, overview.md, raw/README.md) committed