本文最后更新于 1136 天前,其中的信息可能已经有所发展或是发生改变。
背景
主机是一台 Windows Server 2019 Datacenter (可以视为 Win10 1809),IIS 10.0
IIS 上运行了数个站点,包括一个 Gitea 的反向代理,一个微服务网关,数个管理平台,一个静态资源托管
本文即将使用的工具:WinACME:一款非常强大而且简单易用的工具。
因为在 Windows 的 IIS 中进行 HTTPS 托管,在笔者的多方比较下,WinACME 是最为简单的,它提供了多种自动部署方式,并且能安装计划任务自动延期。但是想要更加愉快的使用,必须进行一番调教。
有哪些问题
- 使用 WinACME 申请泛域名(Wildcard, 俗称野卡) 证书时,必须要用到 DNS 验证方式,WinACME 没有自动修改域名解析记录的任何适合国内使用的集成解决方案。
- 最近几版本支持 HTTP/2,SNI 的 IIS 可以在一个服务器上使用多个证书。
- 使用泛域名的野卡证书会出现多个网站共用一个证书的情况
- WinACME 的自动续期只负责续签证书以及自动注册到 IIS 当中,但不会更新网站中对旧证书的绑定。
WinACME 支持自写脚本扩展,所以我们可以通过写域名DNS自动验证脚本,以及正确的域名证书更新脚本来规避上述问题。
解决方案
由于笔者的域名使用的是阿里云(万网),因此可以使用阿里云的 API 来达到自动添加 DNS 验证 所需要的 TXT 记录。以下是 DNS 验证 自动脚本(PowerShell,可以直接拿去使用,参考自 Posh-ACME 的插件):
# Modified By iEdon (https://iedon.com) param( [string]$Task, [string]$DomainName, [string]$RecordName, [string]$TxtValue ) $AliKeyId = '阿里云API Key' $AliSecretInsecure = '阿里云 Secert' $script:WellKnownDirs = @{ LE_PROD = 'https://acme-v02.api.letsencrypt.org/directory'; LE_STAGE = 'https://acme-staging-v02.api.letsencrypt.org/directory'; } $script:HEADER_NONCE = 'Replay-Nonce' $script:USER_AGENT = "iEdon-Modified; PowerShell/$($PSVersionTable.PSVersion)" $script:COMMON_HEADERS = @{'Accept-Language'='en-us,en;q=0.5'} $script:UseBasic = @{} if ('UseBasicParsing' -in (Get-Command Invoke-WebRequest).Parameters.Keys) { $script:UseBasic.UseBasicParsing = $true } function Get-DateTimeOffsetNow { [CmdletBinding()] param() [System.DateTimeOffset]::Now } function Add-DnsTxtAliyun { [CmdletBinding(DefaultParameterSetName='Insecure')] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, [Parameter(Mandatory,Position=2)] [string]$AliKeyId, [Parameter(ParameterSetName='Secure',Mandatory,Position=3)] [securestring]$AliSecret, [Parameter(ParameterSetName='Insecure',Mandatory,Position=3)] [string]$AliSecretInsecure, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) if ('Insecure' -eq $PSCmdlet.ParameterSetName) { $AliSecret = ConvertTo-SecureString $AliSecretInsecure -AsPlainText -Force } try { $zoneName = Find-AliZone $RecordName $AliKeyId $AliSecret } catch { throw } Write-Debug "Found zone $zoneName" $recShort = $RecordName -ireplace [regex]::Escape(".$zoneName"), [string]::Empty if ($recShort -eq $RecordName) { $recShort = '@' } try { $queryParams = "DomainName=$zoneName","RRKeyWord=$recShort","ValueKeyWord=$TxtValue",'TypeKeyWord=TXT' $response = Invoke-AliRest DescribeDomainRecords $queryParams $AliKeyId $AliSecret } catch { throw } if ($response.TotalCount -gt 0) { Write-Debug "Record $RecordName already contains $TxtValue. Nothing to do." } else { Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue" $queryParams = "DomainName=$zoneName","RR=$recShort","Value=$TxtValue",'Type=TXT' Invoke-AliRest AddDomainRecord $queryParams $AliKeyId $AliSecret | Out-Null } } function Remove-DnsTxtAliyun { [CmdletBinding(DefaultParameterSetName='Insecure')] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, [Parameter(Mandatory,Position=2)] [string]$AliKeyId, [Parameter(ParameterSetName='Secure',Mandatory,Position=3)] [securestring]$AliSecret, [Parameter(ParameterSetName='Insecure',Mandatory,Position=3)] [string]$AliSecretInsecure, [Parameter(ValueFromRemainingArguments)] $ExtraParams ) if ('Insecure' -eq $PSCmdlet.ParameterSetName) { $AliSecret = ConvertTo-SecureString $AliSecretInsecure -AsPlainText -Force } try { $zoneName = Find-AliZone $RecordName $AliKeyId $AliSecret } catch { throw } Write-Debug "Found zone $zoneName" $recShort = $RecordName -ireplace [regex]::Escape(".$zoneName"), [string]::Empty if ($recShort -eq $RecordName) { $recShort = '@' } try { $queryParams = "DomainName=$zoneName","RRKeyWord=$recShort","ValueKeyWord=$TxtValue",'TypeKeyWord=TXT' $response = Invoke-AliRest DescribeDomainRecords $queryParams $AliKeyId $AliSecret } catch { throw } if ($response.TotalCount -gt 0) { Write-Verbose "Removing TXT record for $RecordName with value $TxtValue" $id = $response.DomainRecords.Record[0].RecordId Invoke-AliRest DeleteDomainRecord @("RecordId=$id") $AliKeyId $AliSecret | Out-Null } else { Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do." } } function Save-DnsTxtAliyun { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments)] $ExtraParams ) } function Invoke-AliRest { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$Action, [Parameter(Position=1)] [string[]]$ActionParams, [Parameter(Mandatory,Position=2)] [string]$AccessKeyId, [Parameter(Mandatory,Position=3)] [securestring]$AccessSecret ) $apiBase = 'https://alidns.aliyuncs.com' $allParams = $ActionParams + @( "AccessKeyId=$AccessKeyId", "Action=$Action", 'Format=json', 'SignatureMethod=HMAC-SHA1', "SignatureNonce=$((New-Guid).ToString())", 'SignatureVersion=1.0', "Timestamp=$((Get-DateTimeOffsetNow).UtcDateTime.ToString('yyyy-MM-ddTHH\%3Amm\%3AssZ'))", "Version=2015-01-09" ) | Sort-Object $strToSign = [uri]::EscapeDataString($allParams -join '&') $strToSign = "GET&%2F&$strToSign" Write-Debug $strToSign $stsBytes = [Text.Encoding]::UTF8.GetBytes($strToSign) $secPlain = (New-Object PSCredential "user",$AccessSecret).GetNetworkCredential().Password $secBytes = [Text.Encoding]::UTF8.GetBytes("$secPlain&") $hmac = New-Object Security.Cryptography.HMACSHA1($secBytes,$true) $sig = [Convert]::ToBase64String($hmac.ComputeHash($stsBytes)) $sigUrl = [uri]::EscapeDataString($sig) Write-Debug $sig $uri = "$apiBase/?$($allParams -join '&')&Signature=$sigUrl" Invoke-RestMethod $uri @script:UseBasic -EA Stop } function Find-AliZone { [CmdletBinding()] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$AliKeyId, [Parameter(Mandatory,Position=2)] [securestring]$AliSecret ) if (!$script:AliRecordZones) { $script:AliRecordZones = @{} } if ($script:AliRecordZones.ContainsKey($RecordName)) { return $script:AliRecordZones.$RecordName } $pieces = $RecordName.Split('.') for ($i=1; $i -lt ($pieces.Count-1); $i++) { $zoneTest = "$( $pieces[$i..($pieces.Count-1)] -join '.' )" Write-Debug "Checking $zoneTest" try { $response = Invoke-AliRest DescribeDomains @("KeyWord=$zoneTest") $AliKeyId $AliSecret if ($response.TotalCount -gt 0) { $script:AliRecordZones.$RecordName = $response.Domains.Domain[0].DomainName # or PunyCode? return $script:AliRecordZones.$RecordName } } catch { throw } } throw "No zone found for $RecordName" } if ($Task -eq 'create'){ Add-DnsTxtAliyun $RecordName $TxtValue $AliKeyId $AliSecretInsecure } if ($Task -eq 'delete'){ Remove-DnsTxtAliyun $RecordName $TxtValue $AliKeyId $AliSecretInsecure }
然后还有一个问题便是野卡证书的自动续签问题,可以利用这个 installation 脚本(PowerShell):
# Script By iEdon (https://iedon.com) Import-Module WebAdministration $bindings = Get-Item IIS:\SslBindings\* foreach ($binding in $bindings) { if ( ($binding.Store -eq "WebHosting") -and ( ($binding.Host.indexof("域名xxx.com") -ne -1) -or ($binding.Sites -eq "网站名XXXX") )) { $binding | Remove-Item } } $CertShop=$args[0] $newCert = Get-Item -Path "Cert:\LocalMachine\WebHosting\$CertShop" (Get-WebBinding -Name "网站A" -Protocol "https").AddSslCertificate($newCert.GetCertHashString(), "WebHosting") (Get-WebBinding -Name "网站B" -Protocol "https").AddSslCertificate($newCert.GetCertHashString(), "WebHosting") (Get-WebBinding -Name "网站C" -Protocol "https").AddSslCertificate($newCert.GetCertHashString(), "WebHosting") (Get-WebBinding -Name "网站D" -Protocol "https").AddSslCertificate($newCert.GetCertHashString(), "WebHosting") (Get-WebBinding -Name "网站E" -Protocol "https").AddSslCertificate($newCert.GetCertHashString(), "WebHosting") (Get-WebBinding -Name "网站F" -Protocol "https").AddSslCertificate($newCert.GetCertHashString(), "WebHosting")
此脚本会自动更新 IPv4 任意主机上 443 端口的虚拟主机上的绑定信息。
此后,可以正常使用 Win-ACME,操作为:
- M (Create new certificate (full options)),
- 4 (Manual input),
- Enter comma-separated list of host names, starting with the common name: *.iedon.com(你的野卡 Wildcard 域名),
- Suggested FriendlyName is ‘[Manual] 你刚输入的域名’, press enter to accept or type an alternative: <直接回车>,
- 选择 3 ([dns-01] Create verification records with your own script),
- Path to script that creates DNS records: C:\Server\WinACME\dns.ps1 (上文的阿里云 DNS 自动验证脚本),
- How to delete records after validation: 3 (Do not delete),
- Input parameters for create script, or enter for default “create {Identifier} {RecordName} {Token}”: <直接回车>,
- What kind of private key should be used for the certificate?: 2 (RSA Key),
- How would you like to store the certificate?: 3 (Windows Certificate Store),
- Would you like to store it in another way too?: 3 (No additional storage steps required),
- Which installation step should run first?: 3 (Start external script or program),
- 然后输入上文第二个自动续期用的安装脚本直接回车即可。
我换域名了。。。umrhe.com
OK~ 已经更新
我换域名了。。。umrhe.com
OK~ 已经更新