diff --git a/.tasks/NEXT_SESSION.md b/.tasks/NEXT_SESSION.md index 3b54b5c..08bd057 100644 --- a/.tasks/NEXT_SESSION.md +++ b/.tasks/NEXT_SESSION.md @@ -1,45 +1,51 @@ --- -_last_updated_: 2026-06-05T00:30:00+03:00 -session_id: 2026-06-05-iis-migration-full-cutover +_last_updated_: 2026-06-05T02:00:00+03:00 +session_id: 2026-06-05-iis-cutover-and-winacme-renewal --- # Next session handoff -_Сессия: **FULL CUTOVER достигнут** — все 25 hostname (tandemmebel + вся зона snolla.com) authoritative -на RUVDS `80.64.31.36`. Source НЕ заглушен (user-decision: 7-day soak warm rollback). Зафиксировано в -`.tasks/iis-migration-to-ruvds.md`. Ничего не висит._ +_Сессия: (1) full DNS cutover на RUVDS достигнут; (2) поднят постоянный win-acme HTTP-01 auto-renewal +на RUVDS IIS — дедлайн cert-expiry 2026-07-22 СНЯТ. Source НЕ заглушен (warm rollback). Всё проверено, +ничего не висит._ ## Recent commits -- `539f1a07` task(iis-migration): tandemmebel DNS cutover 2026-06-05 -- (+ commit этой сессии с full-cutover записью) +- `9cd6d4e2` task(iis-migration): full DNS cutover reached 2026-06-05 +- (+ commit этой сессии: win-acme renewal pipeline + wiki concept) ## Что сделано (iis-migration, 05.06) -- ✅ **Full cutover:** все 25 hostname → RUVDS на authoritative ns1.reg.ru. На windows-source - (`94.19.247.14`) authoritative не осталось ничего. Вся зона `snolla.com` (apex + 10 субдоменов, - независимые A) переехала вслед за tandemmebel. -- ✅ Хвост кэша: `on.snolla.com` + `tandemmebel.snolla.com` ещё `94.19.247.14` на Google 8.8.8.8 - (TTL 86400, дотекает; 1.1.1.1/Yandex уже RUVDS). Дренаж до ~24ч. -- ✅ RUVDS smoke: `snolla.com` 200, `tandemmebel.snolla.com` 200. `maljarka` 502 (pre-existing CMS, не cutover). +- ✅ **Full cutover:** все 25 hostname authoritative на RUVDS `80.64.31.36`. На windows-source ничего authoritative. +- ✅ **win-acme auto-renewal** на RUVDS: 25-SAN LE cert (до **2026-09-03**) во всех SNI-биндингах + + SYSTEM scheduled task `win-acme-renew-snolla` (renew 55д до expiry). RUVDS больше НЕ зависит от + домашнего traefik по сертификатам. + - Gotcha решён: OWIN-catch-all CMS жрал `/.well-known/acme-challenge/` → challenge вынесен в отдельное + IIS-приложение (пул «No Managed Code») + патч шаблона `C:\win-acme\Web_Config.xml`. + - Артефакты: `scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1` + `winacme-Web_Config.xml`; + recipe в `.wiki/concepts/winacme-iis-owin-catchall-http01.md`. + - Доступ к RUVDS: SSH `~/.ssh/ruvds-iis-migration` → `Administrator@80.64.31.36` (elevated сессия). ## Спроси user'а -- **Decommission `snolla` site** — ждём soak (~2026-06-12) + drain кэша, или форсировать раньше? - Когда решит: elevated PS → `Stop-Website snolla` → архив `C:\sites\snolla` (8.66 ГБ) → kreknin → - cleanup temp SSH-ключей + FW-правил (`ssh-from-source`/`smb-from-source` на RUVDS). -- **LE-renewal на RUVDS** — теперь DNS полностью на RUVDS → можно win-acme + HTTP-01. Дедлайн ~2026-07-15 - (certs expire 2026-07-22). Поднимать? -- `maljarka.tandemmebel.ru` 502 — чинить CMS-тенант отдельно? (pre-existing, не блокер). +- **Decommission `snolla` site** на windows-source — ждём soak (~2026-06-12) + drain кэша, или раньше? + Когда решит: elevated PS → `Stop-Website snolla` → архив `C:\sites\snolla` (8.66 ГБ) → kreknin + + cleanup temp SSH-ключей/FW-правил. (win-acme на RUVDS от этого не зависит — он самостоятелен.) +- `maljarka.tandemmebel.ru` 502 — чинить CMS-тенант отдельно? (pre-existing, не блокер; HTTPS-cert у него валиден). ## Не делать (preemptive guards) -- **Не `Stop-Website snolla` пока хвост кэша течёт** — `on.`/`tandemmebel.snolla.com` ещё резолвятся - на windows у части юзеров (Google). Остановка сейчас = частичный downtime до drain. -- **Не глушить до soak** без явного запроса — user выбрал 7-day warm rollback. -- IIS-правки требуют **elevated shell** (текущий `vitya` — не admin; W3SVC running). -- **branch ahead от origin** — push без явного grant'а (Rule 4). Этой сессии grant НЕ давали. +- **Не `--test` в win-acme под SSH** — включает интерактив, виснет. Для staging — `--baseuri `. +- **Не трогать `acme-challenge` IIS-приложение / пул «No Managed Code» / патченый `Web_Config.xml`** — + на них держится авто-renewal. Detail: `.wiki/concepts/winacme-iis-owin-catchall-http01.md`. +- **Не `Stop-Website snolla` пока хвост кэша течёт / без явного запроса** (warm rollback). +- IIS-правки — elevated shell. **branch ahead от origin** — push без явного grant'а (Rule 4); grant НЕ давали. ## Memory updates за сессию -- (нет нового файла) +- (нет нового файла; знание — в wiki concept + task decisions log) + +## ⚠️ Sensitive (НЕ в git) + +- Корневой `C:\sites\snolla\web.config` содержит plaintext-креды (MSSQL/SMTP/Azure/API-ключи). Pre-existing, + не вводилось этой сессией. Если будешь декоммишить/архивировать — не плодить копии в трекаемых местах. diff --git a/.tasks/iis-migration-to-ruvds.md b/.tasks/iis-migration-to-ruvds.md index c3737cb..c008a64 100644 --- a/.tasks/iis-migration-to-ruvds.md +++ b/.tasks/iis-migration-to-ruvds.md @@ -62,6 +62,7 @@ Migrate IIS hosting from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK) - **На windows source 94.19.247.14** (16): `labtools.ru` + `www.labtools.ru`, `snolla.com` + 12× *.snolla.com (incl `rimiz.snolla.com`), `maljarka.tandemmebel.ru`, `tandemmebel.ru` + `www.tandemmebel.ru` (scope exception). TTL 86400s — не lowered. - **Live smoke** через real DNS: `kupimknigi.spb.ru`/`www.emspb.ru`/`www.pilorama98.ru`/`www.labtools.pro` → 200 OK + correct per-tenant title. Bare-domain 3 redirects работают (emspb/pilorama98/labtools.pro). `rimiz.ru` / `www.rimiz.ru` → 404 — pre-existing CMS-defect (тоже 404 на source), не migration regression. - **2026-06-05 (tandemmebel cutover — post-closure follow-up):** user (владелец tandemmebel) сам перенастроил DNS в reg.ru. Проверено `Resolve-DnsName`: `tandemmebel.ru` + `www.tandemmebel.ru` + `maljarka.tandemmebel.ru` → `80.64.31.36` (RUVDS) на authoritative ns1.reg.ru + 8.8.8.8 + 1.1.1.1 + Yandex. apex+CNAME сработал (www/maljarka follow apex автоматом). Smoke через real DNS: `tandemmebel.ru`/`www` → **200 OK** correct content; `maljarka.tandemmebel.ru` → **502** (pre-existing CMS-дефект, на source ровно так же — не регрессия cutover'а, см. Open questions). **Снимает прежнюю scope-exception 2026-05-24** (tandemmebel больше НЕ остаётся на windows). **Source НЕ заглушен** — user-decision держать как warm rollback: сайт `snolla` — общий catch-all, 11 `*.snolla.com` (incl `tandemmebel.snolla.com`) ещё резолвятся на `94.19.247.14`, полный `Stop-Website` невозможен; индивидуальное снятие 3 HTTPS-биндингов отложено до full-site decommission. Текущий баланс: **14 hostnames на RUVDS / 11 на windows-source**. +- **2026-06-05 (LE auto-renewal pipeline построен — win-acme):** на RUVDS поднят постоянный self-renewing HTTP-01 pipeline (win-acme v2.2.9). Один **25-SAN cert** (store WebHosting, Issuer LE YR2, valid до **2026-09-03**) установлен во все 25 SNI-биндинга; **scheduled task `win-acme-renew-snolla`** (SYSTEM, daily, renew 55д до expiry). **Закрывает дедлайн cert-expiry 2026-07-22** и снимает зависимость RUVDS от домашнего traefik по сертификатам. Главный gotcha: OWIN-catch-all CMS (`owin:HandleAllRequests=true`) перехватывал `/.well-known/acme-challenge/` → решено выносом challenge-пути в **отдельное IIS-приложение в пуле «No Managed Code»** + патч шаблона `C:\win-acme\Web_Config.xml` (``). Staging + prod валидация всех 25 хостов зелёная; живые HTTPS-эндпоинты отдают новый cert (проверено TLS-смоком, incl maljarka/rimiz). Скрипт `scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1` + шаблон `winacme-Web_Config.xml`. Полный recipe: [[../.wiki/concepts/winacme-iis-owin-catchall-http01]]. - **2026-06-05 (FULL CUTOVER достигнут):** проверка всех 25 hostname против authoritative ns1.reg.ru — **все → `80.64.31.36` (RUVDS), на windows-source authoritative не осталось ничего**. Вся зона `snolla.com` (apex + 10 субдоменов, независимые A-записи) переехала. Хвост кэша: `on.snolla.com` + `tandemmebel.snolla.com` ещё `94.19.247.14` на Google 8.8.8.8 (TTL 86400, дотекает; 1.1.1.1/Yandex уже RUVDS). RUVDS smoke: `snolla.com` 200 (default лендинг), `tandemmebel.snolla.com` 200 (tenant content). **Разблокирует decommission** `snolla` site + LE-renewal через win-acme+HTTP-01 (теперь challenge не отскочит на windows). **Source НЕ заглушен** — user-decision: 7-day soak warm rollback + ждём drain кэша. Целевой decommission ~2026-06-12; cert-renewal deadline ~2026-07-15 (certs expire 2026-07-22). - **2026-05-25 (close decision):** user-decision close — несмотря на 16 hostnames pending DNS swap + source IIS:8089 still live + LE renewal pipeline pending + cleanup pending, **task закрывается** на этом этапе. Reason: Phase 1 (RUVDS infra + scp + IIS recreate + cert import + 9 DNS swaps) выполнен и live; остальное — coordination/wait/decommission character, не agent-work load. Future actions перечислены в Closure note ниже, не trackable как `iis-migration-to-ruvds` continuation. @@ -71,7 +72,7 @@ Migrate IIS hosting from [[../entities/windows-recovery-host]] (DESKTOP-NSEF0UK) - [ ] **Source IIS state backup НЕ снят** (skip'нули — appcmd требует elevated source-shell которого не было). Acceptable risk — source IIS всё ещё running как rollback. Если RUVDS proves stable за 1 неделю → можно decommission source без forensic snapshot'а. - [ ] **CMS-side 502/404 на 4 hostnames** — `maljarka.tandemmebel.ru` / `rimiz.ru` / `www.rimiz.ru` / `rimiz.snolla.com`. На source тоже не работают (return default page). Pre-existing dead-routes от `[iis-traefik-dead-routes-cleanup]` 2026-05-21. Изоляция не блокер для migration; защититься от cutover-blame — отдельная investigation если нужно. - [ ] **Backup strategy для RUVDS** — расширить `vds-backup-rsync-kreknin` cron как третий source? Решение отложено до после full cutover. -- [ ] **LE renewal на RUVDS** — текущие certs valid до 2026-07-22 (~60 дней). До expiry нужен permanent renewal pipeline: +- [x] **LE renewal на RUVDS** — ✅ DONE 2026-06-05 (win-acme HTTP-01, см. Decisions log + [[../.wiki/concepts/winacme-iis-owin-catchall-http01]]). Выбран Option A (win-acme + HTTP-01 после full cutover). Новый 25-SAN cert до 2026-09-03, авто-renewal через SYSTEM scheduled task. Дедлайн 2026-07-22 снят. ~~Историческая формулировка ниже:~~ текущие certs valid до 2026-07-22 (~60 дней). До expiry нужен permanent renewal pipeline: - Option A: win-acme (wacs.exe) standalone-на-RUVDS с DNS-01 (manual TXT на reg.ru — нет reg.ru-plugin) или HTTP-01 (но только после DNS swap'а — иначе LE challenge bounce'нется на home). - Option B: cron-job который re-extract'ит из traefik acme.json + scp upload + Import-PfxCertificate на RUVDS. Pollute source's traefik lifecycle. - Recommendation: Option A win-acme + HTTP-01 после full cutover (~95 days margin до cert expiry — 60 days minus DNS-stabilization window). @@ -105,9 +106,7 @@ Task **closed 🟢** per user decision. Phase 1 — RUVDS infra + cert pipeline 2. **Source IIS НЕ decommission'ен.** `localhost:8089` snolla site + port 80 catch-all still live на windows-recovery-host. Не трогать пока 16 hostnames на 94.19.247.14. После full cutover + 7-day soak — `Stop-Website snolla` + archive `C:\sites\snolla` (8.66GB) → kreknin. -3. **LE renewal pipeline НЕ построен.** Текущие RUVDS PFX certs valid до 2026-07-22 (~57 days от 2026-05-25). До этой даты нужен renewal-mechanism: - - Recommend: win-acme на RUVDS + HTTP-01 challenge — **возможно только после full DNS cutover** (HTTP-01 bounce'нется на windows source pre-cutover, LE не сможет verify owners). - - Soft deadline: ~2026-07-15 (week of margin до cert expiry). +3. ~~**LE renewal pipeline НЕ построен.**~~ → **DONE 2026-06-05** (win-acme HTTP-01 после full cutover, как и рекомендовалось). Новый 25-SAN LE cert до 2026-09-03 + SYSTEM auto-renew task. Recipe: [[../.wiki/concepts/winacme-iis-owin-catchall-http01]]. 4. **Temp SSH key cleanup не сделан.** Оставлены (могут понадобиться для будущих push'ей на RUVDS): - `~/.ssh/ruvds-iis-migration` + `.pub` на source workstation diff --git a/.wiki/concepts/traefik-acme-json-to-iis-cert-import.md b/.wiki/concepts/traefik-acme-json-to-iis-cert-import.md index 58b8e8c..a5bcbc4 100644 --- a/.wiki/concepts/traefik-acme-json-to-iis-cert-import.md +++ b/.wiki/concepts/traefik-acme-json-to-iis-cert-import.md @@ -10,6 +10,8 @@ updated: 2026-05-24 Recipe для one-shot переноса Let's Encrypt certs из traefik `acme.json` (single-file ACME store) в IIS на Windows host с SNI multi-binding'ами. Использован при миграции [[../entities/ruvds-iis-host]] 2026-05-23 для 14 LE certs → 25 HTTPS hostnames bound via SNI. **Это не renewal pipeline**, а bootstrap-перенос — для long-term renewal recommend `win-acme` standalone на IIS host'е (см. footnote). +> **Update 2026-06-05 — SUPERSEDED для renewal'а.** Этот ручной PFX-перенос больше не на критическом пути: на RUVDS поднят постоянный self-renewing win-acme HTTP-01 pipeline — см. [[winacme-iis-owin-catchall-http01]]. RUVDS больше не зависит от домашнего traefik по сертификатам. Этот recipe оставлен как reference для bootstrap-сценария / если HTTP-01 недоступен. + ## Когда применять - Migration: traefik-on-A → IIS-on-B, certs нужны до того как ACME-validation с нового host'а станет возможной (DNS ещё указывает на старый host, HTTP-01 challenge bounce'нется). diff --git a/.wiki/concepts/winacme-iis-owin-catchall-http01.md b/.wiki/concepts/winacme-iis-owin-catchall-http01.md new file mode 100644 index 0000000..4abf066 --- /dev/null +++ b/.wiki/concepts/winacme-iis-owin-catchall-http01.md @@ -0,0 +1,94 @@ +--- +title: win-acme HTTP-01 авто-renewal на IIS под OWIN-catch-all CMS +status: live +tags: [windows, iis, ruvds, letsencrypt, win-acme, acme, http-01, owin, cms, ops] +related: [[ruvds-iis-host]], [[traefik-acme-json-to-iis-cert-import]], [[windows-server-2025-core-bootstrap]], [[iis-migration-2026-05-19-postmortem]] +--- + +# win-acme HTTP-01 авто-renewal на IIS под OWIN-catch-all CMS + +Постоянный self-renewing LE-pipeline на RUVDS IIS-хосте ([[ruvds-iis-host]]) для сайта `snolla` +(25 hostname через SNI). **Заменяет** ручной метод [[traefik-acme-json-to-iis-cert-import]] +(экспорт PFX из домашнего traefik `acme.json` + ручной `Import-PfxCertificate`) — тот делал +RUVDS зависимым от домашней машины по сертификатам и требовал ручного продления. + +Поднято 2026-06-05, после full DNS cutover на RUVDS (HTTP-01 challenge до cutover'а отскакивал +на windows-source — LE не мог достучаться). + +## Что развёрнуто + +- **win-acme v2.2.9** в `C:\win-acme\` (скачан с GitHub releases, `x64.pluggable`). +- Один **SAN-cert на 25 hostname** в store `WebHosting`, установлен во все 25 `*:443:` SNI-биндинга. +- **Scheduled task `win-acme-renew-snolla`** — daily 09:00 + random delay 4h, runs as **SYSTEM**, + `wacs.exe --renew --baseuri https://acme-v02.api.letsencrypt.org/`. Renewal due 55 дней до expiry. +- Idempotent setup-скрипт: `scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1` (фазы `download`/`app`/`probe`/`task`/`verify`). + +## Главный gotcha: OWIN-catch-all жрёт `/.well-known/acme-challenge/` + +Сайт = **MoreThenCms** на OWIN. В корневом `web.config`: `owin:HandleAllRequests=true` + +handler `Owin path="*"` + `runAllManagedModulesForAllRequests="true"` → **весь** трафик уходит в +managed OWIN-pipeline. HTTP-01 токен (статический файл) отдаётся не статикой, а CMS → `404`/`500`/redirect. +LE-валидация падает (`Invalid response ...: 500`). + +Снятие Owin-**handler**'а в дочернем `web.config` НЕ помогает — OWIN перехватывает на уровне +**модуля** (HandleAllRequests). Что НЕ сработало: +- web.config с `` — рерайта-то и нет, маршрутит managed-pipeline. +- web.config с `` в обычном (managed) пуле — модуль всё равно жив. + +### Рабочее решение: отдельное IIS-приложение в пуле «No Managed Code» + +`.well-known/acme-challenge` вынесен в **отдельное IIS Application** под сайтом `snolla` с +app-pool'ом `acme-challenge`, у которого `managedRuntimeVersion=''` (**No Managed Code**). В таком +пуле .NET/OWIN не стартует вообще — токен отдаётся нативным `StaticFileModule`. + +Нюанс: в дочернем web.config всё равно нужно **``** — +иначе унаследованный managed Owin-handler даёт `500` (managed handler в unmanaged пуле). Плюс +extensionless-mime (токены без расширения). Проверенный web.config: + +```xml + + + + + + + + + +``` + +Результат: токен отдаётся `200` для **любого** Host — даже для деградировавших CMS-тенантов +(`maljarka.tandemmebel.ru`/`rimiz.ru`, которые на app-уровне отдают `502`/`404`), т.к. статика +обходит CMS-роутинг. + +### Патч шаблона win-acme + +win-acme при filesystem-валидации **перезаписывает** дочерний web.config своим шаблоном +`C:\win-acme\Web_Config.xml` (а при `CleanupFolders=true` потом удаляет токен+web.config). Дефолтный +шаблон делает `` + mime, но **не снимает Owin-handler** → снова `500`. + +Durable-фикс = **пропатчить сам шаблон** `C:\win-acme\Web_Config.xml`, добавив `` ++ `runAllManagedModulesForAllRequests="false"`. Трекаемая копия: `scripts/iis-migration-to-ruvds/winacme-Web_Config.xml`. +Тогда на каждом renewal win-acme сам кладёт рабочий web.config в (постоянное) unmanaged-приложение, +валидирует, чистит. Между renewal'ами папка пустая — трафика на неё нет. + +## Процедура де-риска (повторяемо) + +1. `--validationmode http-01 --validation filesystem` локальный probe токена через `localhost` + `Host:`-заголовок (обходит home-DPI middlebox). +2. **Staging** через `--baseuri https://acme-staging-v02.api.letsencrypt.org/` (НЕ `--test` — он включает интерактивные промпты, которые вешают non-interactive SSH). pemfiles store + `--installation none` — без правок IIS. +3. Прод: `--store certificatestore --installation iis --installationsiteid 1`. + +## Грабли + +- **`--test` ≠ staging-only.** `--test` тащит интерактив («Try in default browser?», «Quit?») → виснет под SSH. Для staging без интерактива — `--baseuri `. +- **pemfiles path должен существовать** заранее, иначе abort до валидации. +- **Scheduled task руками, не через win-acme.** `--notaskscheduler` при выпуске; таск создаётся отдельно как SYSTEM (win-acme-овский setup может спросить креды интерактивно). win-acme потом пишет «Scheduled task not configured yet» — косметика, ищет *свой* таск. +- win-acme renewal'ы раздельны по `--baseuri` (разные ConfigurationPath): staging-renewal не трогается прод-таском. Staging-renewal всё равно убран для чистоты. + +## Команда боевого выпуска (reference) + +``` +C:\win-acme\wacs.exe --source iis --siteid 1 --validationmode http-01 --validation filesystem ^ + --webroot C:\sites\snolla --store certificatestore --installation iis --installationsiteid 1 ^ + --accepttos --emailaddress --force --notaskscheduler +``` diff --git a/.wiki/index.md b/.wiki/index.md index b46a955..156c0da 100644 --- a/.wiki/index.md +++ b/.wiki/index.md @@ -48,6 +48,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever - [vbox-windows-stability-tuning](concepts/vbox-windows-stability-tuning.md) — VirtualBox + Windows-гость — нюансы стабильности cross-hypervisor миграции - [vds-kzntsv-dhcp-outage-2026-05-28](concepts/vds-kzntsv-dhcp-outage-2026-05-28.md) — DHCP outage 2026-05-28 на vds-kzntsv — диагностика + recovery runbook (link UP но без IPv4 → статика + тикет хостеру) - [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published +- [winacme-iis-owin-catchall-http01](concepts/winacme-iis-owin-catchall-http01.md) — win-acme HTTP-01 авто-renewal LE на IIS под OWIN-catch-all CMS — challenge в отдельном No-Managed-Code приложении + патч шаблона; заменяет ручной PFX-импорт - [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18 - [windows-server-2025-core-bootstrap](concepts/windows-server-2025-core-bootstrap.md) — Win Server 2025 Core (RUVDS) — bootstrap для IIS-хоста: default-blockers + transfer-методов матрица diff --git a/.wiki/log.md b/.wiki/log.md index 85841a0..d53bab1 100644 --- a/.wiki/log.md +++ b/.wiki/log.md @@ -2,6 +2,8 @@ Append-only log of wiki operations (ingests, promotions, lints, migrations). +## [2026-06-05] ingest | concepts/winacme-iis-owin-catchall-http01 — постоянный self-renewing LE pipeline на RUVDS IIS (win-acme v2.2.9, 25-SAN cert, SYSTEM scheduled task). Главное: OWIN-catch-all CMS жрёт `/.well-known/acme-challenge/` → решено отдельным IIS-приложением в пуле «No Managed Code» + патч шаблона `Web_Config.xml` (snять Owin-handler). Заменяет ручной [[traefik-acme-json-to-iis-cert-import]] для renewal'а; снимает зависимость RUVDS от домашнего traefik. Закрывает дедлайн cert-expiry 2026-07-22 (новый cert до 2026-09-03). Источник: `.tasks/iis-migration-to-ruvds.md` Decisions log 2026-06-05. + ## [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 diff --git a/scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1 b/scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1 new file mode 100644 index 0000000..45c1610 --- /dev/null +++ b/scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1 @@ -0,0 +1,159 @@ +# 03-ruvds-winacme.ps1 — win-acme setup + HTTP-01 challenge-path probe on RUVDS IIS host. +# Idempotent. Run elevated on RUVDS (80.64.31.36) over SSH. +# Phase switch: -Phase download|probe|cleanprobe +param([string]$Phase = 'probe') + +$ErrorActionPreference = 'Stop' +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$dest = 'C:\win-acme' +$root = 'C:\sites\snolla' +$chDir = Join-Path $root '.well-known\acme-challenge' + +function Download-WinAcme { + if (Test-Path (Join-Path $dest 'wacs.exe')) { Write-Output "wacs already present: $dest\wacs.exe"; return } + $api = 'https://api.github.com/repos/win-acme/win-acme/releases/latest' + $rel = Invoke-RestMethod -Uri $api -Headers @{ 'User-Agent' = 'ruvds-setup' } + $asset = $rel.assets | Where-Object { $_.name -match 'x64\.pluggable\.zip$' } | Select-Object -First 1 + if (-not $asset) { throw "no x64.pluggable.zip asset in $($rel.tag_name)" } + $zip = Join-Path $env:TEMP $asset.name + Write-Output "downloading $($rel.tag_name): $($asset.name)" + Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -Headers @{ 'User-Agent' = 'ruvds-setup' } + New-Item -ItemType Directory -Force -Path $dest | Out-Null + Expand-Archive -Path $zip -DestinationPath $dest -Force + Remove-Item $zip -Force + Write-Output ("installed: " + (Get-Item (Join-Path $dest 'wacs.exe')).VersionInfo.ProductVersion) +} + +function Test-ChallengePath { + New-Item -ItemType Directory -Force -Path $chDir | Out-Null + $token = 'acme-probe-' + (Get-Random) + $tokenFile = Join-Path $chDir $token + Set-Content -Path $tokenFile -Value $token -NoNewline -Encoding ascii + Write-Output "token file: $tokenFile (content='$token')" + + $hosts = 'snolla.com','tandemmebel.ru','maljarka.tandemmebel.ru','rimiz.ru' + Write-Output "--- WITHOUT challenge web.config (baseline CMS behaviour) ---" + Probe-Hosts $hosts $token + + # Mirror the proven /admin escape: remove the catch-all Owin handler so OWIN stops + # intercepting this path, add extensionless-token mime, drop managed-module overhead. + $webcfg = @' + + + + + + + + + + + + + +'@ + Set-Content -Path (Join-Path $chDir 'web.config') -Value $webcfg -Encoding utf8 + Write-Output "--- WITH challenge web.config (win-acme style) ---" + Probe-Hosts $hosts $token +} + +function Probe-Hosts($hosts, $token) { + foreach ($h in $hosts) { + try { + $r = Invoke-WebRequest "http://localhost/.well-known/acme-challenge/$token" -Headers @{ Host = $h } -UseBasicParsing -TimeoutSec 15 -MaximumRedirection 0 + $ok = ($r.Content.Trim() -eq $token) + Write-Output (" [{0}] HTTP {1} match={2} body='{3}'" -f $h, $r.StatusCode, $ok, ($r.Content -replace '\s+',' ').Substring(0,[Math]::Min(40,$r.Content.Length))) + } catch { + $resp = $_.Exception.Response + if ($resp) { Write-Output (" [{0}] HTTP {1} loc={2}" -f $h, [int]$resp.StatusCode, $resp.Headers['Location']) } + else { Write-Output (" [{0}] ERR: {1}" -f $h, $_.Exception.Message) } + } + } +} + +function Setup-ChallengeApp { + Import-Module WebAdministration + $pool = 'acme-challenge' + if (-not (Test-Path "IIS:\AppPools\$pool")) { New-WebAppPool -Name $pool | Out-Null; Write-Output "created app pool $pool" } + Set-ItemProperty "IIS:\AppPools\$pool" -Name managedRuntimeVersion -Value '' # No Managed Code + Set-ItemProperty "IIS:\AppPools\$pool" -Name autoStart -Value $true + New-Item -ItemType Directory -Force -Path $chDir | Out-Null + # No-Managed-Code pool + drop the inherited catch-all Owin handler (else it 500s in an + # unmanaged pool), serve extensionless ACME tokens as text/plain. Verified combo. + $appcfg = @' + + + + + + + + + + + + + + +'@ + Set-Content -Path (Join-Path $chDir 'web.config') -Value $appcfg -Encoding utf8 + $vpath = '/.well-known/acme-challenge' + $app = Get-WebApplication -Site 'snolla' | Where-Object { $_.path -eq $vpath } + if (-not $app) { + New-WebApplication -Site 'snolla' -Name '.well-known/acme-challenge' -PhysicalPath $chDir -ApplicationPool $pool -Force | Out-Null + Write-Output "created IIS application $vpath (pool=$pool, No Managed Code)" + } else { + Set-ItemProperty "IIS:\Sites\snolla$vpath" -Name applicationPool -Value $pool + Write-Output "IIS application $vpath exists (pool ensured=$pool)" + } +} + +function Probe-Once { + New-Item -ItemType Directory -Force -Path $chDir | Out-Null + $token = 'acme-probe-' + (Get-Random) + Set-Content -Path (Join-Path $chDir $token) -Value $token -NoNewline -Encoding ascii + Write-Output "probe token: $token" + Probe-Hosts ('snolla.com','tandemmebel.ru','www.tandemmebel.ru','maljarka.tandemmebel.ru','rimiz.ru','kupimknigi.spb.ru') $token +} + +function Clean-Probe { + if (Test-Path $chDir) { Get-ChildItem $chDir -Filter 'acme-probe-*' | Remove-Item -Force -EA SilentlyContinue; Write-Output "removed probe tokens" } +} + +function Setup-RenewTask { + $wacs = 'C:\win-acme\wacs.exe' + $taskName = 'win-acme-renew-snolla' + $action = New-ScheduledTaskAction -Execute $wacs -Argument '--renew --baseuri https://acme-v02.api.letsencrypt.org/' + $trigger = New-ScheduledTaskTrigger -Daily -At 9am + try { $trigger.RandomDelay = 'PT4H' } catch {} + $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest + $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 2) + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description 'Auto-renew Let''s Encrypt cert for IIS site snolla (25 SNI bindings) via HTTP-01.' -Force | Out-Null + $t = Get-ScheduledTask -TaskName $taskName + $info = $t | Get-ScheduledTaskInfo + Write-Output ("task '{0}' state={1} nextRun={2}" -f $taskName, $t.State, $info.NextRunTime) + + # remove the staging renewal so a stray --renew against staging does nothing + $stagingRenewals = 'C:\ProgramData\win-acme\acme-staging-v02.api.letsencrypt.org\Renewals' + if (Test-Path $stagingRenewals) { Remove-Item "$stagingRenewals\*" -Recurse -Force -EA SilentlyContinue; Write-Output "cleared staging renewals" } + if (Test-Path 'C:\win-acme\stagingtest') { Remove-Item 'C:\win-acme\stagingtest' -Recurse -Force -EA SilentlyContinue; Write-Output "removed stagingtest pemfiles" } +} + +function Verify-Certs { + Import-Module WebAdministration + # show the cert now bound (thumbprint -> subject/expiry) + $b = Get-WebBinding -Port 443 | Select-Object -First 1 + $hash = (Get-Item "IIS:\SslBindings\*!443!*" -EA SilentlyContinue | Select-Object -First 1).Thumbprint + Get-ChildItem Cert:\LocalMachine\WebHosting | Sort-Object NotAfter -Descending | Select-Object -First 3 | + ForEach-Object { Write-Output ("WebHosting cert: {0} | NotAfter={1} | Issuer={2} | SAN-count via DnsNameList={3}" -f $_.Thumbprint, $_.NotAfter, $_.Issuer, $_.DnsNameList.Count) } +} + +switch ($Phase) { + 'download' { Download-WinAcme } + 'probe' { Download-WinAcme; Test-ChallengePath } + 'app' { Download-WinAcme; Setup-ChallengeApp; Probe-Once } + 'task' { Setup-RenewTask; Verify-Certs } + 'verify' { Verify-Certs } + 'cleanprobe' { Clean-Probe } + default { throw "unknown phase $Phase" } +} diff --git a/scripts/iis-migration-to-ruvds/winacme-Web_Config.xml b/scripts/iis-migration-to-ruvds/winacme-Web_Config.xml new file mode 100644 index 0000000..92d7194 --- /dev/null +++ b/scripts/iis-migration-to-ruvds/winacme-Web_Config.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + +