# 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" } }