feat(iis-migration): win-acme HTTP-01 auto-renewal on RUVDS IIS

Stood up a permanent self-renewing Let's Encrypt pipeline on the RUVDS
IIS host, replacing the manual traefik acme.json -> PFX import and
closing the 2026-07-22 cert-expiry deadline (new 25-SAN cert valid to
2026-09-03, SYSTEM scheduled task renews 55 days before expiry).

Key obstacle: the MoreThenCms OWIN catch-all (owin:HandleAllRequests)
swallowed /.well-known/acme-challenge/. Solved by carving the challenge
path into a separate IIS application in a No-Managed-Code app pool, plus
patching win-acme's Web_Config.xml template to remove the inherited Owin
handler. Staging + prod validation green for all 25 hostnames; live TLS
smoke confirms the new cert is served (incl degraded maljarka/rimiz).

- scripts/iis-migration-to-ruvds/03-ruvds-winacme.ps1 (idempotent setup)
- scripts/iis-migration-to-ruvds/winacme-Web_Config.xml (patched template)
- .wiki/concepts/winacme-iis-owin-catchall-http01.md (recipe + gotchas)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 21:08:41 +03:00
parent 9cd6d4e22c
commit 3c7ca3416b
8 changed files with 315 additions and 29 deletions

View File

@@ -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:<host>` 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 с `<rewrite><rules><clear/></rules>` — рерайта-то и нет, маршрутит managed-pipeline.
- web.config с `<handlers><remove name="Owin"/></handlers>` в обычном (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 всё равно нужно **`<handlers><remove name="Owin"/></handlers>`** —
иначе унаследованный managed Owin-handler даёт `500` (managed handler в unmanaged пуле). Плюс
extensionless-mime (токены без расширения). Проверенный web.config:
```xml
<configuration>
<system.webServer>
<handlers><remove name="Owin" /></handlers>
<staticContent>
<remove fileExtension="." /><mimeMap fileExtension="." mimeType="text/plain" />
</staticContent>
<modules runAllManagedModulesForAllRequests="false" />
</system.webServer>
</configuration>
```
Результат: токен отдаётся `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). Дефолтный
шаблон делает `<staticContent><clear/>` + mime, но **не снимает Owin-handler** → снова `500`.
Durable-фикс = **пропатчить сам шаблон** `C:\win-acme\Web_Config.xml`, добавив `<remove name="Owin"/>`
+ `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 <staging>`.
- **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 <ops-email> --force --notaskscheduler
```