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>
7.7 KiB
title, type, tags, sources, updated
| title | type | tags | sources | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Экспорт LE certs из traefik acme.json в IIS (PFX + SNI bindings) | concept |
|
|
2026-05-24 |
traefik acme.json → IIS cert import
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'нется).
- Bootstrap пилотного IIS-сервера с готовыми certs для smoke testing до DNS swap'а.
Не применять для regular renewal — каждый renewal цикл traefik будет генерировать новый cert, и manual re-export не масштабируется. После full DNS swap → win-acme HTTP-01 на IIS-side directly.
Входные данные
acme.json— обычно в<traefik-data>/letsencrypt/acme.json. Содержит JSON с массивомCertificates[].domain.main,.sans,.certificate(base64 PEM bundle),.key(base64 PEM private key).- Target IIS host с PowerShell + OpenSSL (для cert conversion) + access to
Cert:\LocalMachine\Mystore.
Recipe (per cert)
1. Распаковать пару PEM из acme.json
$acme = Get-Content C:\path\to\acme.json | ConvertFrom-Json
# Resolver key — обычно "letsencrypt" или имя из traefik.yml
$resolver = $acme.letsencrypt
foreach ($cert in $resolver.Certificates) {
$domain = $cert.domain.main
$sans = $cert.domain.sans # может быть $null
$safeName = $domain -replace '\*','wildcard'
[System.Convert]::FromBase64String($cert.certificate) | Set-Content "C:\temp\$safeName.crt" -AsByteStream
[System.Convert]::FromBase64String($cert.key) | Set-Content "C:\temp\$safeName.key" -AsByteStream
}
2. Convert PEM pair → PFX (OpenSSL)
$pfxPass = 'pfximport' # temp passphrase, only for Import step
& openssl pkcs12 -export `
-inkey "C:\temp\$safeName.key" `
-in "C:\temp\$safeName.crt" `
-out "C:\temp\$safeName.pfx" `
-name $domain `
-password "pass:$pfxPass"
3. Import PFX → Cert:\LocalMachine\My
$securePass = ConvertTo-SecureString $pfxPass -AsPlainText -Force
$imported = Import-PfxCertificate `
-FilePath "C:\temp\$safeName.pfx" `
-CertStoreLocation 'Cert:\LocalMachine\My' `
-Password $securePass
# $imported.Thumbprint — нужен для следующего шага
4. SNI binding в IIS (per hostname)
Один cert может покрывать несколько SANs — для каждого hostname создать separate binding с SslFlags=1 (SNI):
$allHosts = @($domain) + @($sans | Where-Object { $_ })
foreach ($hostname in $allHosts) {
# 4a. Create binding without cert
New-WebBinding -Name 'snolla' `
-IPAddress '*' -Port 443 -Protocol 'https' `
-HostHeader $hostname -SslFlags 1 # 1 = SNI
# 4b. Attach cert via thumbprint (нужен netsh-стиль через IIS:\)
$binding = Get-WebBinding -Name 'snolla' -Port 443 -HostHeader $hostname
$binding.AddSslCertificate($imported.Thumbprint, 'My')
}
SslFlags=1 обязателен. Без SNI один port 443 = один cert per IP — Windows откажется bind'ить второй cert на тот же
*:443. С SNI можно навесить ~unlimited hostnames на один*:443.
Verification
# Список bindings:
Get-WebBinding -Port 443 | Format-Table protocol, bindingInformation, sslFlags
# Сертификат за binding:
$binding.attributes['certificateHash'].Value # thumbprint
$binding.attributes['certificateStoreName'].Value # "My"
# External smoke (с другого host'а — home middlebox может mangle Host header):
curl -k --resolve "$hostname:443:80.64.31.36" "https://$hostname/" -I
# Должен вернуть chain LE R13 + correct Subject CN
HTTP/2 negotiate'ится auto (IIS 10 + Windows Server 2019+ → ALPN supported из коробки).
Gotchas
acme.jsonpermission на Linux:chmod 600, traefik не запустится с другим. На Windows copy-out для conversion'а — нужны Admin perms.- PEM bundle order в acme.json:
certificateполе содержит leaf + chain (Let's Encrypt R13 + ISRG Root X1). OpenSSL pkcs12 импортит весь bundle корректно — IIS отдаст полный chain. - PFX password — только для transit между PEM и cert store, после import не используется.
pfximport(или любой placeholder) — OK; не путать с pass для encrypted PFX-on-disk storage. - HostHeader case-sensitivity в
New-WebBinding: IIS lowercase'ит автоматически, но скрипт принимай canonical lowercase. - Wildcard SANs: если cert
*.example.com— нужно отдельный binding для каждого конкретного subdomain'а (Windows не делает wildcard matching на binding'ах автоматически). Либо использовать default-cert на listener'е (netsh http add sslcert) — но это break'нет multi-cert SNI scenario. - Cert duplicates: повторный import того же cert'а создаёт duplicate в
Cert:\LocalMachine\My. Idempotency — check thumbprint exists перед import'ом. netsh http show sslcert— для low-level audit что binding'и реально привязаны (PowerShellGet-WebBindingиногда показывает stale state после ручных правок IIS Manager'ом).
Limits / когда recipe мал
- >30 hostnames —
Import-PfxCertificate+New-WebBindingцикл становится медленным (~5 sec/cert). Batch'ить черезappcmd add binding /commit:apphost. - TLS 1.3 — IIS 10 на Server 2022+ supports; на Server 2019 — Edge case. Не зависит от recipe — от Windows feature.
- Renewal — этот recipe не покрывает. Variant A (win-acme HTTP-01 standalone) предпочтительнее, см. ../sources/iis-migration-to-ruvds-2026-05-23 Outstanding (post-soak).
Cross-refs
- Use case: ../sources/iis-migration-to-ruvds-2026-05-23 Phase 6
- Source acme.json расположение: traefik-on-windows-host setup, см. traefik-on-windows-docker-desktop
- Long-term replacement:
win-acme(wacs.exe) standalone — пока не documented as concept; reference https://www.win-acme.com/ - Bootstrap-host где recipe был применён: ../entities/ruvds-iis-host