From 2d307f2dd0e69086115a7156e914f96bac04383a Mon Sep 17 00:00:00 2001 From: vitya Date: Tue, 19 May 2026 15:08:07 +0300 Subject: [PATCH] docs(.wiki): iis-migration rollback + post-mortem + xml-escape concept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 9 — rollback миграции на host-IIS из-за Docker port-loop (host.docker.internal:80 от traefik container резолвится обратно в сам traefik через NAT, https.yml http-catchall middleware отдавал 301 -> TOO_MANY_REDIRECTS). Prod снова через VM. - iis-migration-2026-05-19-postmortem: 10 ошибок миграции + recipe для следующей попытки (backend port НЕ :80, smoke с MaximumRedirection 0, тест из НЕ-LAN, parallel VM x N часов, atomic revert plan) - webconfig-password-xml-escape: новая gotcha — & в conn-string пароле требует & в Web.config (XML reserved char) - iis-host-migration-2026-05-19: Phase 9 rollback chronology + что осталось на хосте inert - snolla-recovery-vm: статус -> active prod (обратно) - windows-recovery-host: host IIS sites -> inert artifacts - recovery-architecture-snapshot: chain снова через VM, traefik backends восстановлены из .bak-phase3 Co-Authored-By: Claude Opus 4.7 (1M context) --- iis-migration-2026-05-19-postmortem.md | 207 +++++++++++++++++++++++++ recovery-architecture-snapshot.md | 92 ++++++----- webconfig-password-xml-escape.md | 71 +++++++++ 3 files changed, 327 insertions(+), 43 deletions(-) create mode 100644 iis-migration-2026-05-19-postmortem.md create mode 100644 webconfig-password-xml-escape.md diff --git a/iis-migration-2026-05-19-postmortem.md b/iis-migration-2026-05-19-postmortem.md new file mode 100644 index 0000000..b036e9d --- /dev/null +++ b/iis-migration-2026-05-19-postmortem.md @@ -0,0 +1,207 @@ +--- +title: IIS host-migration session 2026-05-19 — post-mortem +type: concept +tags: [migration, iis, traefik, docker, postmortem, lessons-learned, gotcha] +sources: [../sources/iis-host-migration-2026-05-19.md] +updated: 2026-05-19 +--- + +# Post-mortem миграции CMS на нативный IIS, 2026-05-19 + +Сессия закончилась **откатом на VM** после ~3 часов сломанного prod-трафика. Этот документ — честный разбор что пошло не так и как избежать в следующей попытке. Авторство ошибок — мои; пользователю — за быстрое обнаружение и за то что заставил откатить. + +## Хронология провала + +1. **Phase 1-3** (утро 2026-05-19) — миграция выполнена технически. Smoke test `Invoke-WebRequest -MaximumRedirection 5` через `https://localhost:4443/` показал 10/11 хостов → 200 OK. **Я объявил success.** +2. **Phase 4-8** — реорг `C:\sites\`, stayer DB-fix, traefik для stayer'ов отключен, VM savestate, wiki commit (`🟢 done`). +3. **Через ~10 минут после commit** пользователь открыл `https://www.pilorama98.ru/` в браузере → `502 Bad Gateway`, потом `TOO_MANY_REDIRECTS` для остальных. +4. Я ~1 час паниковал и каскадно ломал. +5. Пользователь сказал revert. Откат до VM-backend → 200 OK через router. Prod вернулся. + +## Что было реальным корнем + +### 1. Docker port-collision invariant — НЕ ЗНАЛ + +Внутри traefik-контейнера `host.docker.internal:80` резолвится через Docker Desktop NAT **обратно в сам traefik**, потому что traefik publish'ит `host:8000 → container:80`. Docker Desktop port-mapping создаёт замкнутую петлю на published-портах. + +**Симптом:** traefik backend `host.docker.internal:80` (наша host-IIS) фактически возвращает request на traefik's own HTTP entrypoint (:80 inside container). Там действует middleware из `https.yml`: + +```yaml +http-catchall: + rule: hostregexp(`{host:.+}`) + entryPoints: [http] + middlewares: [redirect-to-https] +redirect-to-https: + redirectScheme: + scheme: https + permanent: true +``` + +→ traefik отвечает `301 Location: https:///` → клиент follows → traefik HTTPS entrypoint → backend `:80` → loop. Browser: `TOO_MANY_REDIRECTS`. + +**Опознавательный знак:** `Content-Length: 17` body "Moved Permanently", отсутствует `Server: Microsoft-IIS` — это traefik response, не IIS. Я заметил только под конец. + +### 2. Phase 3 smoke test — ложный позитив + +`Invoke-WebRequest -MaximumRedirection 5` следовал по редиректам и где-то на 2-3-м шаге случайно landed на 200 (вероятно, при определённой комбинации Host header'а CMS возвращал контент). Я принял это за work-good baseline. На самом деле уже тогда был partial loop. + +**Урок:** smoke test должен: +- Использовать `-MaximumRedirection 0` или `--max-redirs 0` чтобы видеть **первый ответ** (без авто-следования). +- Логировать **всю цепочку редиректов** (curl `-L -v`, считать `num_redirects`). +- Распознавать loop по `num_redirects > 3` как warning. + +### 3. Все мои тесты обходили router + +- `curl --resolve domain:4443:127.0.0.1` бьёт traefik **напрямую** на host loopback. Router не в пути. +- Тест **из router'а** `ssh root@192.168.1.1 curl https://snolla.com/` тоже ложный — router DNS resolve'ит в own WAN IP, connect direct без DNAT (hairpin issue) → попадает на router LuCI web :443. Я получил "200 OK" от LuCI и принял за CMS. + +**Реальный путь клиента из публичного интернета:** + +``` +Browser → DNS → public IP 94.19.247.14 (router WAN) + → router NAT PREROUTING DNAT 443 → 192.168.1.143:4443 + → traefik:4443 → backend → ... +``` + +**Урок:** тестировать с НЕ-LAN машины (телефон через мобильный интернет, VPS curl, etc.). Любой тест внутри LAN сети — потенциально ложный. + +### 4. Перепутал источник 301 + +Долго копал в CMS `MoreThenCms.Web\Global.asax.cs` и `MoreThenCms.Api.WebUI\Global.asax.cs` на предмет "force HTTPS"/"primary domain redirect". Это ВСЁ существует в CMS-коде, но не было активным источником loop'а — там logic для www-stripping и canonical, не для HTTP→HTTPS scheme. + +**Реальный источник** был в `traefik/data/custom/https.yml` (middleware). Уже задокументирован в wiki как Pitfall 5 в [[traefik-on-windows-docker-desktop]], но я не сложил 2+2. + +**Урок:** headers != lying. Если response без `Server: Microsoft-IIS` — это не IIS отвечает. Прежде чем копать application код, проверить что response действительно от application. + +### 5. Каскадное реактивное ломание + +После первого `502 Bad Gateway` сделал в течение часа: +- `Restart-WebAppPool snolla` (не помогло — pool жив) +- `Stop-Process w3wp -Force` + restart (не помогло) +- `docker restart traefik` — сделал **хуже** (после рестарта 503, потеряли warm cache) +- Patched traefik 11 yml: `host.docker.internal:80 → 192.168.1.143:80` (то же самое — `192.168.1.143:80` тоже резолвится через NAT обратно в traefik, тот же loop) +- Patched `:80 → :8088` + добавил IIS binding на :8088 (правильное направление в принципе, но без понимания root cause работал вслепую; забыл `Stop+Start Website` чтобы binding applied; потом IIS не listened → 503) + +**Каждый шаг без понимания root cause только ухудшал state.** Должно было быть: при первом 502 → `git stash` traefik yml + revert на `.bak-phase3` + понять разницу между working и broken state. Вместо этого — random fixes. + +**Урок:** **первое непонимание = STOP. Revert. Reproduce. Understand. Then fix.** + +## Бонус-провалы + +### 6. Wiki status "🟢 done" — преждевременный + +Объявил task done через ~5 минут после Phase 3 smoke. Wrote 5 wiki files, 2 commits. На деле prod был сломан через 10 минут — traefik state какое-то время держал работающий ответ (cached connections?), потом deteriorated. + +**Урок:** "done" — это **48+ часов uptime под реальным трафиком** + проверка из публичной сети + zero rollbacks. Не immediate smoke pass. + +### 7. VM savestate — слишком рано + +Phase 8 я заморозил VM **через ~30 минут после Phase 3**. Пользователь сразу сказал mostly "не торопись", но я proceed-нул. Лучшая практика: **держать VM running параллельно как hot fallback** на N часов/дней, только тогда savestate. + +### 8. Реорг C:\sites\ + IIS rename — лишняя работа в той же сессии + +Phase 4 (rename folders, sites, pools, recreate AppPool) добавил **много моментов где могло сломаться**, и сделан в той же сессии что и actual migration. Лучше: миграция → стабильность 24h → реорг + rename как **отдельная мелкая task**. + +### 9. Web.config patch для stostayer.old пропущен в Phase 2 + +В Phase 2 я patched только `C:\sites\MoreThenCms.Web\Web.config` (`sitePath` + conn → localhost). Пропустил `C:\stayer\stostayer.old\web.config` где conn был `Data Source=10.0.2.2` (NAT gateway, не работает на хосте). Это вылезло только в Phase 5 когда я начал stayer'ы дебажить. + +**Урок:** **сначала grep ВСЕ Web.config'и** на patterns (`10.0.2.2`, `host.docker.internal`, абсолютные пути), сделать таблицу "файл → что patch", выполнить **batch**, потом сразу тестировать. Не делать item-by-item. + +### 10. Не использовал git stash / branch protection + +Все patches шли прямо на master (`project-discipline` rule). Это правильно для проекта, но для **prod-trafficchanging operations** (traefik patches) — лучше **сначала bak копия + явный grep diff**, **затем** commit. Я делал именно так с traefik (bak-stamps), но не делал atomic revert на первое 502. + +## Recipe для следующей попытки миграции + +Если делать миграцию заново, **избежать всех 5 главных ошибок**: + +### A. Backend port — НЕ :80 + +Использовать любой порт **отличный от traefik publish-ports** (`:8000, :4443, :8080`). Predictable choices: + +- `:18080` (исторически = VM NAT, теперь свободен после VM-down) — `host.docker.internal:18080` не конфликтует с traefik internals. +- `:8088`, `:8181`, etc. — любой свободный, главное **не совпадающий** с traefik publish. + +Добавить IIS binding к site: +```powershell +New-WebBinding -Name snolla -Protocol http -Port 18080 -IPAddress '*' +Stop-Website snolla; Start-Website snolla # ← КРИТИЧНО, без restart binding не activates +Get-NetTCPConnection -LocalPort 18080 -State Listen # verify +``` + +### B. Smoke test с no-follow + +```powershell +# WRONG (auto-follows, скрывает loop): +Invoke-WebRequest -UseBasicParsing -Headers @{Host=$h} -Uri 'https://localhost:4443/' -MaximumRedirection 5 + +# RIGHT (раскрывает loop): +Invoke-WebRequest -UseBasicParsing -Headers @{Host=$h} -Uri 'https://localhost:4443/' -MaximumRedirection 0 +# или curl: +curl -ksI -H "Host: $h" --max-redirs 0 https://localhost:4443/ +# и проверить FIRST response code + Location header. Если Location == входной URL → loop. +``` + +### C. Тест из НЕ-LAN сети + +Перед commit: +- Тест с **телефона через мобильный интернет** (наибыстрее). +- Или curl с VPS (~$5/mo) через cron-job. +- Или `curl -x` через прокси публичного интернета. +- Хост-side smoke и router-side smoke = **только sanity**, не production-confirmation. + +### D. Параллельный run VM x N часов + +После переключения traefik backend → host: +- VM **не глушим**. +- Мониторим N часов (минимум 24h) логи host-IIS + Application event log + traefik logs. +- Только если ZERO incidents → savestate VM. +- Cleanup VM (`unregistervm --delete`) — через дополнительные несколько дней. + +### E. Atomic revert plan ДО старта + +До любой prod-changing операции: +- `*.bak-pre--` backup для каждого touched-файла. +- Заранее написать revert-скрипт ("если что — paste this"). +- Заявить user'у: "вот revert. Если что — кричи слово stop". +- При первом любом anomaly → revert немедленно, разбираться post-mortem. + +### F. Headers checklist при дебаге + +Когда видим 301/302/502: +1. **Server header** есть `Microsoft-IIS/10.0`? Нет → не IIS отвечает (traefik / какой-то прокси). +2. **Content-Length** какой? Если короткий (~17, ~100) + `text/plain` body — generic redirect от framework, не IIS rendered response. +3. **X-Powered-By: ASP.NET** есть? Нет → не ASP.NET. +4. Сравнить с known-good response (direct `curl http://localhost/`). + +### G. Web.config grep batch перед patches + +```powershell +# найти все hardcoded refs в одном проходе +Get-ChildItem C:\sites,C:\nas-recovery\vm-sites -Recurse -Include *.config | % { + Select-String -Path $_.FullName -Pattern 'Data Source=|inetpub|stayer|sitePath' -EA Silent +} | ft Path, LineNumber, Line -a +``` + +Сделать таблицу `{файл, текущее, нужно}` → проверить с user → одним PowerShell-блоком patch ВСЁ → одним smoke. + +## Что было сделано правильно (хотя бы) + +- **UTF-8 BOM Web.config patches** (паттерн из [[cms-config-rewrite-pattern]]) — работали стабильно. +- **XML-escape `&` в password** для stostayer Web.config — поймали и задокументировали в [[webconfig-password-xml-escape]]. +- **Backup-stamping** (`.bak-phase3`, `.bak-stayer-switch`, `.bak-hostip`) — позволили чисто откатиться. +- **VM не удалена** — savestate сохранил состояние, восстановилось за `startvm` + 90s + `ipconfig /release /renew` recipe из [[vbox-windows-stability-tuning]]. +- **Wiki как append-only журнал** — этот post-mortem пишется тут же, не теряется. + +## Open: что осталось на хосте после revert + +- `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` — ~11 GB, неиспользуется prod. +- IIS sites `snolla` :80+:8088, `stostayer` :8090, `stostayer.old` :8091 + AppPools — нерабочие, не мешают (не на prod-пути). +- traefik backup-stamps (`*.bak-phase3-2026-05-19`, `*.bak-stayer-switch-2026-05-19`, `*.bak-hostip-2026-05-19`) — оставлены. + +Эти артефакты — основа для следующей попытки. Не очищать, пока не сделаем чистую миграцию по recipe выше. + +## Связано + +[[iis-host-migration-2026-05-19]] — chronology до и включая ошибки. [[traefik-on-windows-docker-desktop]] Pitfall 5 — знал, не применил. [[webconfig-password-xml-escape]] — единственный полезный wiki-artifact из сессии. [[recovery-architecture-snapshot]] — обновлён обратно под VM-chain. diff --git a/recovery-architecture-snapshot.md b/recovery-architecture-snapshot.md index 6123530..d7e9371 100644 --- a/recovery-architecture-snapshot.md +++ b/recovery-architecture-snapshot.md @@ -8,9 +8,9 @@ updated: 2026-05-19 # Recovery Architecture Snapshot -Снимок production-инфраструктуры на 2026-05-19, **после миграции основного CMS на нативный IIS хоста** ([[iis-host-migration-2026-05-19]]). Это **рабочее, но временное** состояние — single-point-of-failure на бытовом железе. Замечания по улучшению — [[future-resilient-architecture-goals]]. +Снимок production-инфраструктуры на конец 2026-05-19, **после отката миграции** (попытка миграции на host-IIS не удалась — см. [[iis-migration-2026-05-19-postmortem]]). Prod снова через VM `snolla-recovery`, как было до session start. Это **рабочее, но временное** состояние — single-point-of-failure на бытовом железе. Замечания по улучшению — [[future-resilient-architecture-goals]]. -## Цепочка запроса от клиента до CMS — 10 главных доменов (snolla.com и др.) +## Цепочка запроса от клиента до CMS (после revert 2026-05-19) ``` Клиент (browser) @@ -25,33 +25,36 @@ updated: 2026-05-19 → traefik 2.6.6 на 4443/8000 → TLS termination, certResolver=letsEncrypt из acme.json → match по Host header (file-provider rules в data/custom/*.yml) - → backend = http://host.docker.internal:80/ ← с 2026-05-19 - → Native IIS на хосте :80, site MoreThenCms.Web - → AppPool MoreThenCms.Web (.NET v4.0 Integrated, ApplicationPoolIdentity) - → physicalPath C:\sites\MoreThenCms.Web - → ASP.NET CMS code (.NET Framework 4.8.1 на хосте) + → backend = http://host.docker.internal:18080/ + → host:18080 = VBox NAT port forward → VM:80 + → snolla-recovery VM [[snolla-recovery-vm]] + → IIS на :80, site MoreThenCms.Web (catch-all) + → ASP.NET CMS code (.NET Framework 4.8) → Connection strings: - → MSSQL: Data Source=localhost,1433 (прямо в MSSQL container) - → MinIO/storage: TBD точная схема (вопрос снят пользователем) - → Elasticsearch: не используется CMS (там books-стек) + → MSSQL: Data Source=10.0.2.2:1433 (VBox NAT gateway = host) + → MinIO/storage: вопрос снят пользователем + → Elasticsearch: не используется CMS → MSSQL container на host:1433 → 5 production DB (MoreThenCms, Stayer*, stostayer, TireService) ← HTTP response back through chain ``` -## Цепочка для stostayer / stostayer.old (всё ещё на VM) +## Цепочка для stayer'ов (тоже через VM) ``` Клиент → ... → traefik → backend = host.docker.internal:18180 (stostayer) или :18181 (stostayer.old) → VBox NAT port forward → VM:8080 или VM:8081 - → IIS в [[snolla-recovery-vm]] → C:\stayer\MoreThenCms.Web или C:\stayer\stostayer.old - → Connection strings: - → stostayer: Data Source=89.253.219.2,1433 (внешний production MSSQL — НЕ наш контейнер) - → stostayer.old subapps (calc/price/tireService) — отдельные pools, TBD source + → IIS в [[snolla-recovery-vm]] + → stostayer: Data Source=89.253.219.2,1433 (НЕ работающий внешний сервер — но stayer'ы и так не отвечают, по словам user'а) + → stostayer.old subapps (calc/price/tireService) — отдельные pools ``` -VM также имеет `Snolla.IdentityManager :8089` и неиспользуемые копии главного CMS — публично через traefik не доступны. +**Внимание:** stostayer-DB на `89.253.219.2` мёртвая. В host-копии (`C:\sites\stostayer\Web.config`) уже patched на новый `www.stostayer.ru,1433` (user `stayer_site`) с XML-escape `&` в password — но это inert. При следующей попытке миграции — этот патч уже готов. + +## На хосте параллельно (inert, не на prod-пути) + +См. [[windows-recovery-host]] "Inert" — `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` + IIS sites/pools `snolla, stostayer, stostayer.old` готовы к переключению traefik, но не активны. ## Запущенные docker контейнеры на хосте @@ -66,27 +69,30 @@ VM также имеет `Snolla.IdentityManager :8089` и неиспользу Все на docker network `proxy` (external) — это позволяет traefik резолвить `minio`, `elasticsearch`, `imgproxy-nginx` напрямую по docker DNS. -## Traefik routes (с 2026-05-19) +## Traefik routes (после revert 2026-05-19) -13 client домен-маршрутов в `data/custom/`. **11 главных переключены на хост-IIS :80** ([[iis-host-migration-2026-05-19]]), 2 stostayer остаются на VM: +Все активные routes снова указывают на VM (как было до session). Backups сохранены для следующей попытки миграции. -| File | Hosts | Backend | -|---|---|---| -| snolla.yml | snolla.com + 10 subdomains | **host.docker.internal:80** (host-IIS) | -| rimiz.yml | rimiz.ru, www.rimiz.ru | **host.docker.internal:80** (host-IIS, но CMS отдаёт 404 — известный issue) | -| labtools.yml | labtools.ru, www.labtools.ru | **host.docker.internal:80** | -| labtoolspro.yml | labtools.pro, www.labtools.pro | **host.docker.internal:80** | -| pilorama98.yml | pilorama98.ru, www.pilorama98.ru | **host.docker.internal:80** | -| tandemmebel.yml | tandemmebel.ru, www.tandemmebel.ru | **host.docker.internal:80** | -| emspb.yml | emspb.ru, www.emspb.ru | **host.docker.internal:80** | -| kupimknigi.yml | kupimknigi.spb.ru | **host.docker.internal:80** | -| maljarka.yml | maljarka.tandemmebel.ru | **host.docker.internal:80** | -| sestech.yml | sestech.ru, www.sestech.ru | **host.docker.internal:80** | -| isc-artmaterials.yml | ics-artmaterials.com, www.ics-artmaterials.com | **host.docker.internal:80** | -| oldstostayer.yml | (старый stostayer) | host.docker.internal:18181 (всё ещё VM:8081) | -| stostayer.yml | stostayer.ru или похожий | host.docker.internal:18180 (всё ещё VM:8080) | +| File | Hosts | Backend | Статус | +|---|---|---|---| +| snolla.yml | snolla.com + 10 subdomains | host.docker.internal:18080 → VM | active | +| rimiz.yml | rimiz.ru, www.rimiz.ru | host.docker.internal:18080 → VM | active (но CMS отдаёт 404 — известное) | +| labtools.yml | labtools.ru, www.labtools.ru | host.docker.internal:18080 → VM | active | +| labtoolspro.yml | labtools.pro, www.labtools.pro | host.docker.internal:18080 → VM | active | +| pilorama98.yml | pilorama98.ru, www.pilorama98.ru | host.docker.internal:18080 → VM | active | +| tandemmebel.yml | tandemmebel.ru, www.tandemmebel.ru | host.docker.internal:18080 → VM | active | +| emspb.yml | emspb.ru, www.emspb.ru | host.docker.internal:18080 → VM | active | +| kupimknigi.yml | kupimknigi.spb.ru | host.docker.internal:18080 → VM | active | +| maljarka.yml | maljarka.tandemmebel.ru | host.docker.internal:18080 → VM | active | +| sestech.yml | sestech.ru, www.sestech.ru | host.docker.internal:18080 → VM | active | +| isc-artmaterials.yml | ics-artmaterials.com, www.ics-artmaterials.com | host.docker.internal:18080 → VM | active | +| stostayer.yml | stostayer.snolla.com | host.docker.internal:18180 → VM:8080 | active | +| oldstostayer.yml | old.stostayer.ru | host.docker.internal:18181 → VM:8081 | active | -Backup конфигов до переключения: `*.yml.bak-phase3-2026-05-19`. +**Backup-stamp файлы** (накопились — для следующей попытки): +- `*.yml.bak-phase3-2026-05-19` — original state до Phase 3 (всё указывает на VM). Это **именно та конфигурация что сейчас live** (свежескопировано в active). +- `*.yml.bak-hostip-2026-05-19` — попытка переключения на `host.docker.internal:80` (создала loop, см. [[iis-migration-2026-05-19-postmortem]]). +- `stostayer.yml.bak-stayer-switch-2026-05-19`, `oldstostayer.yml.bak-stayer-switch-2026-05-19` — попытка stayer'ов на host :8090/:8091 (live теперь снова на VM). Плюс file-provider маршруты для инфраструктурных хостов: @@ -123,14 +129,14 @@ Backup конфигов до переключения: `*.yml.bak-phase3-2026-05 ## Известные открытые баги -1. **X-Forwarded headers** не передаются от traefik в IIS → CMS делает редирект с `:4443` в URL. См. [[traefik-on-windows-docker-desktop]] Pitfall 5. После миграции на host-IIS актуально, fix — URL Rewrite + ARR (Phase 5 в [[iis-host-migration-2026-05-19]]). -2. **rimiz.ru → 404** на host-IIS (и, скорее всего, до миграции на VM тоже). CMS-side, не инфра — mapping host header → CMS-сайт в БД. -3. **stostayer / stostayer.old timeout** при прямом обращении к host-IIS на `:8090/:8091`. Phase 2 миграция выполнена технически, но рантайм не отвечает — не разбирались, prod-трафик пока на VM. -4. **acme.json HTTP-01 renewal** будет фейлить для доменов с DNS не на нашем IP — нужен переезд на DNS-01 через REGRU до истечения сертификатов (~3 месяца). -5. **VM один раз "повисла" в сети** через 1-2 часа uptime — лечилось `ipconfig /release/renew` через VBoxManage guestcontrol. Подтверждено повторно в сессии 2026-05-19. Нужен auto-watchdog (или вообще выключить VM после миграции stayer). -6. **C:\inetpub\logs\** в VM растёт (6+ GB к recovery) — нужна ротация. На хосте логи начнут расти аналогично — те же меры понадобятся. -7. **docker.sock provider в traefik** не работает — но не блокирует (всё через file-provider). См. [[traefik-on-windows-docker-desktop]] Pitfall 3. -8. **MinIO/Azure storage** в CMS — вопрос пользователем снят (Q4 в сессии 2026-05-19), оставлено как есть. +1. **X-Forwarded headers** не передаются от traefik в IIS → CMS делает редирект с `:4443` в URL. См. [[traefik-on-windows-docker-desktop]] Pitfall 5. +2. **rimiz.ru → 404**. CMS-side, не инфра — mapping host header → CMS-сайт в БД. +3. **acme.json HTTP-01 renewal** будет фейлить для доменов с DNS не на нашем IP — нужен переезд на DNS-01 через REGRU до истечения сертификатов (~3 месяца). +4. **C:\inetpub\logs\** в VM растёт (6+ GB к recovery). На хосте та же беда если когда-то переключимся. +5. **docker.sock provider в traefik** не работает — но не блокирует (всё через file-provider). См. [[traefik-on-windows-docker-desktop]] Pitfall 3. +6. **stostayer DB в VM указывает на мёртвый 89.253.219.2** — stayer'ы публично возможно не работают полноценно (user сказал "стайеры и на VM не работает" — подтвердил). Host-копия уже patched на новый `www.stostayer.ru,1433`, в VM не правили. +7. **MinIO/Azure storage** в CMS — вопрос пользователем снят, оставлено как есть. +8. **VM network adapter "виснет"** периодически (recipe `ipconfig /release /renew` через guestcontrol в [[vbox-windows-stability-tuning]]). Подтверждено ещё раз в сессии 2026-05-19. ## Single Points of Failure @@ -138,7 +144,7 @@ Backup конфигов до переключения: `*.yml.bak-phase3-2026-05 - Один публичный IP / провайдер - Один WiFi-канал - Один OpenWRT-роутер -- Один VBox VM (single instance, не replicate) +- **Одна VBox VM `snolla-recovery`** — single instance, обслуживает весь CMS-трафик (после revert) - Один MSSQL контейнер (single primary, нет replica) - Один MinIO (single drive, не distributed) diff --git a/webconfig-password-xml-escape.md b/webconfig-password-xml-escape.md new file mode 100644 index 0000000..a4bc4b5 --- /dev/null +++ b/webconfig-password-xml-escape.md @@ -0,0 +1,71 @@ +--- +title: Web.config — XML escape для спецсимволов в connection-string паролях +type: concept +tags: [iis, webconfig, encoding, xml, gotcha] +sources: [../sources/iis-host-migration-2026-05-19.md] +updated: 2026-05-19 +--- + +# Web.config: XML-escape для `&` (и других reserved chars) в пароле + +## Симптом + +После patch Web.config с новым connection-string'ом ASP.NET-сайт отдаёт **HTTP 500** на любой запрос. В Event Viewer / IIS logs — `System.Configuration.ConfigurationErrorsException: Configuration system failed to initialize` или `Unrecognized escape sequence`. На уровне XML парсера — error при чтении Web.config. + +## Корень + +`Web.config` — это **XML-документ**. Атрибуты в нём (``) проходят через XML-парсер до того, как .NET runtime увидит сам connection string. Если в пароле есть **XML-зарезервированный символ**, парсер ломается. + +XML reserved chars (в значениях атрибутов): + +| Символ | XML-entity escape | Когда обязателен | +|---|---|---| +| `&` | `&` | **всегда** (в attribute value и в text content) | +| `<` | `<` | **всегда** | +| `"` | `"` | если значение в `"..."` (наш кейс — атрибуты обычно в двойных кавычках) | +| `'` | `'` | если значение в `'...'` | +| `>` | `>` | формально не обязателен, но safer | + +В именно нашем кейсе из [[iis-host-migration-2026-05-19]]: пароль `^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF` содержит `&` — XML-парсер начинал интерпретировать `&_ioaT...` как entity reference (`&_ioaT;` — невалидное имя entity) → exception → IIS 500. + +## Правильный способ + +```xml + + + + + +``` + +После XML-парсинга .NET runtime получит литеральный `&` в значении атрибута и передаст в SqlConnection — там пароль уже текст, всё ок. + +## Что НЕ требует escape + +- `^`, `$`, `(`, `)`, `{`, `}`, `[`, `]`, `*`, `+`, `?`, `\`, `/`, `.`, `,`, `;`, `:`, `=`, `#`, `@`, `%`, `!`, `~`, `|` — все безопасны в XML attribute value. +- Особое: `;` в пароле в ADO.NET conn-string'е требует обёртки пароля в `'...'` или `"..."` внутри connection-string'а — но это отдельная история. + +## PowerShell-подсказка для patch + +При программной замене conn-string'а — **сразу escape `&` в значение**, не оставляй на потом: + +```powershell +# 💻 Windows host +$plainPassword = '^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF' +$xmlSafePassword = $plainPassword -replace '&', '&' +# и потом подставлять $xmlSafePassword в Web.config + +# или комбинированный escape всех 5 reserved chars: +function Xml-EscapeAttr($s) { + $s -replace '&','&' -replace '<','<' -replace '>','>' -replace '"','"' -replace "'",''' +} +``` + +## Связанные паттерны + +- [[cms-config-rewrite-pattern]] — UTF-8 BOM ловушка при `Set-Content` без `-Encoding utf8` (другая XML/IIS гнойная тема). +- При генерации паролей для DB-логинов CMS — лучше **запретить `&`, `<`, `"`** в генераторе, чтобы не словить эту проблему в будущем (особенно при автоматизированных patches Web.config через CI).