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>
6.9 KiB
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:
<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'ами папка пустая — трафика на неё нет.
Процедура де-риска (повторяемо)
--validationmode http-01 --validation filesystemлокальный probe токена черезlocalhost+Host:-заголовок (обходит home-DPI middlebox).- Staging через
--baseuri https://acme-staging-v02.api.letsencrypt.org/(НЕ--test— он включает интерактивные промпты, которые вешают non-interactive SSH). pemfiles store +--installation none— без правок IIS. - Прод:
--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