Files
admin/.wiki/concepts/traefik-acme-json-to-iis-cert-import.md
vitya 3c7ca3416b 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>
2026-06-05 21:08:41 +03:00

127 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Экспорт LE certs из traefik acme.json в IIS (PFX + SNI bindings)
type: concept
tags: [traefik, iis, letsencrypt, certs, pfx, sni, windows]
sources: [../sources/iis-migration-to-ruvds-2026-05-23.md]
updated: 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\My` store.
## Recipe (per cert)
### 1. Распаковать пару PEM из acme.json
```powershell
$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)
```powershell
$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`
```powershell
$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)**:
```powershell
$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
```powershell
# Список 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.json` permission** на 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'и реально привязаны (PowerShell `Get-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]]