作者:匿名
发布:29天前

0. 目标

将 Let's Encrypt 申请的泛域名证书 fullchain.cer (证书链,包含你的域名证书和中间证书) 和 bktai.com.key (私钥) 安装到 Windows 11 并配置给 IIS,同时实现 PowerShell 脚本自动化,主要分为以下几个步骤:

  1. 将 .cer 和 .key 文件合并为 .pfx 文件:IIS 需要 PFX (Personal Information Exchange) 格式的证书,它包含了证书和私钥。Let's Encrypt 默认提供的是 PEM 格式的证书和私钥。
  2. 导入 PFX 证书到 Windows 证书存储区:将生成的 PFX 文件导入到 Windows 的“个人”证书存储区(LocalMachine\My)。
  3. 配置 IIS 网站绑定:将导入的证书绑定到 IIS 网站的 HTTPS 端口。

下面是详细的 PowerShell 脚本实现步骤和代码:

1. 前提条件

  • OpenSSL: 你需要在 Windows 上安装 OpenSSL,因为 PowerShell 本身不直接支持将 .cer.key 文件合并为 .pfx。你可以从 https://slproweb.com/products/Win32OpenSSL.html下载并安装。确保将 OpenSSL 的 bin 目录添加到系统的 PATH 环境变量中,或者在脚本中指定 OpenSSL 可执行文件的完整路径。
  • 管理员权限: 运行此脚本需要管理员权限。
  • IIS WebAdministration 模块: 确保 IIS 的 PowerShell 管理模块已安装。如果没有,可以通过以下命令安装:
    Install-WindowsFeature -Name Web-Scripting-Tools
  • 证书文件: 确保 fullchain.cerbktai.com.key 文件位于脚本可访问的路径。

2. PowerShell 脚本

# Run and test in Windows Powershell 5.1
# 运行通过20250529
# Define variables
$CertFilePath = "D:\Test\fullchain.cer" # Path to your fullchain.cer file
$KeyFilePath = "D:\Test\bktai.com.key"  # Path to your bktai.com.key file
$PfxOutputFilePath = "D:\Test\bktai.com.pfx" # Output path for the generated PFX file
$PfxPassword = "yourpassword" # Set a strong password for the PFX file, PLEASE CHANGE THIS!
$WebsiteName = "test.bktai.com" # Your IIS website name (e.g., "Default Web Site")
$IPAddress = "*" # IP address for IIS binding, * means all unassigned IP addresses
$Port = 443 # HTTPS port
$HostHeader = "bktai.com" # Your primary domain name
$WildcardHostHeader = "*.bktai.com" # Wildcard domain name (if needed for separate binding)

# Ensure OpenSSL executable is in PATH, or specify full path
# If OpenSSL is not in your system's PATH, uncomment the line below and set the correct path.
$OpenSslPath = "D:\Jop\OpenSSL-Win64\bin\openssl.exe"

# Helper function to convert Hex String to Byte Array (compatible with older .NET)
function Convert-HexStringToByteArray {
    param(
        [Parameter(Mandatory=$true)]
        [string]$HexString
    )
    $cleanedHexString = $HexString -replace "[^0-9a-fA-F]" # Remove any non-hex characters
    if ($cleanedHexString.Length % 2 -ne 0) {
        throw "Hex string must have an even number of characters."
    }
    $bytes = New-Object byte[] ($cleanedHexString.Length / 2)
    for ($i = 0; $i -lt $cleanedHexString.Length; $i += 2) {
        $bytes[$i/2] = [System.Convert]::ToByte($cleanedHexString.Substring($i, 2), 16)
    }
    return $bytes
}

# 1. Convert .cer and .key files to .pfx file
Write-Host "Starting conversion of .cer and .key files to .pfx file..."
try {
    # Check if OpenSSL is available
    if (-not (Get-Command openssl -ErrorAction SilentlyContinue)) {
        # If $OpenSslPath is defined and exists, use it. Otherwise, throw an error.
        if (Test-Path $OpenSslPath) {
            $OpenSslExecutable = $OpenSslPath
        } else {
            Write-Error "OpenSSL not found in PATH and specified path '$OpenSslPath' is invalid. Please ensure OpenSSL is installed and added to system PATH, or specify the correct full path."
            exit 1
        }
    } else {
        $OpenSslExecutable = "openssl" # Use openssl from PATH
    }

    $opensslArgs = "pkcs12 -export -out `"$PfxOutputFilePath`" -inkey `"$KeyFilePath`" -in `"$CertFilePath`" -passout pass:`"$PfxPassword`""
    
    $process = Start-Process -FilePath $OpenSslExecutable -ArgumentList $opensslArgs -NoNewWindow -Wait -PassThru -ErrorAction Stop
    $process.WaitForExit()
    if ($process.ExitCode -ne 0) {
        throw "OpenSSL command failed with exit code: $($process.ExitCode)"
    }
    Write-Host "Successfully generated PFX file: $PfxOutputFilePath"
} catch {
    Write-Error "Failed to convert certificate: $($_.Exception.Message)"
    exit 1
}

# 2. Import PFX certificate to Windows Certificate Store
Write-Host "Starting import of PFX certificate to Windows Certificate Store..."
try {
    $securePassword = ConvertTo-SecureString -String $PfxPassword -AsPlainText -Force
    Import-PfxCertificate -FilePath $PfxOutputFilePath -CertStoreLocation Cert:\LocalMachine\My -Password $securePassword -ErrorAction Stop
    Write-Host "Successfully imported PFX certificate."
} catch {
    Write-Error "Failed to import certificate: $($_.Exception.Message)"
    exit 1
}

# 3. Get the thumbprint of the newly imported certificate
Write-Host "Getting thumbprint of the newly imported certificate..."
try {
    # Let's Encrypt wildcard certificate subject usually ends with *.yourdomain.com
    # The fullchain.cer might contain both primary and wildcard domains.
    # We look for a certificate whose subject matches either the primary or wildcard domain.
    $certificate = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Subject -like "CN=$HostHeader*" -or $_.Subject -like "CN=$WildcardHostHeader*" } | Sort-Object NotBefore -Descending | Select-Object -First 1

    if (-not $certificate) {
        throw "No matching certificate found. Please check certificate subject or if import was successful."
    }
    $thumbprint = $certificate.Thumbprint
    Write-Host "Retrieved certificate thumbprint: $thumbprint"
} catch {
    Write-Error "Failed to get certificate thumbprint: $($_.Exception.Message)"
    exit 1
}

# 4. Configure IIS website binding
Write-Host "Starting IIS website binding configuration..."
try {
    # Ensure WebAdministration module is loaded. Using -SkipEditionCheck for broader compatibility.
    Import-Module WebAdministration -ErrorAction Stop 

    # Get target IIS website
    $site = Get-Website -Name $WebsiteName -ErrorAction Stop
    if (-not $site) {
        throw "IIS website '$WebsiteName' does not exist."
    }

    # Remove old HTTPS bindings (optional, needed if updating certificate)
    Write-Host "Checking and removing old HTTPS bindings..."
    # Correct variable referencing with ${Port}
    $existingBindings = Get-WebBinding -Name $WebsiteName | Where-Object { $_.protocol -eq "https" -and ($_.bindingInformation -like "*:${Port}:${HostHeader}" -or $_.bindingInformation -like "*:${Port}:${WildcardHostHeader}") }
    foreach ($binding in $existingBindings) {
        Write-Host "Removing old binding: $($binding.bindingInformation)"
        Remove-WebBinding -Name $WebsiteName -BindingInformation $binding.bindingInformation -Protocol "https" -ErrorAction SilentlyContinue
    }

    # Add new HTTPS binding for the primary domain
    Write-Host "Adding primary domain HTTPS binding: $HostHeader"
    New-WebBinding -Name $WebsiteName -IPAddress $IPAddress -Port $Port -HostHeader $HostHeader -Protocol "https" -SslFlags 1 -ErrorAction Stop # SslFlags 1 enables SNI
    
    # Bind certificate to IIS HTTPS port
    Write-Host "Binding certificate to IIS website..."
    $sslBindingPath = "IIS:\SslBindings\$($IPAddress)!$Port"
    
    Write-Host "Converting thumbprint string '$thumbprint' to byte array..."
    try {
        # Clean the thumbprint: remove spaces and convert to uppercase to ensure correct hex conversion
        $thumbprintCleaned = $thumbprint.Replace(" ", "").ToUpper()
        $certificateHash = Convert-HexStringToByteArray -HexString $thumbprintCleaned
        Write-Host "Converted byte array length: $($certificateHash.Length)"

        Set-ItemProperty -Path $sslBindingPath -Name "SslCertificateHash" -Value $certificateHash -ErrorAction Stop
        Set-ItemProperty -Path $sslBindingPath -Name "SslCertStoreName" -Value "My" -ErrorAction Stop # Ensure certificate is in "My" (Personal) store

        Write-Host "Certificate successfully bound to IIS HTTPS port."

    } catch {
        Write-Error "Failed to bind certificate to IIS: $($_.Exception.Message)"
        throw $_.Exception # Re-throw the exception to be caught by the outer try-catch
    }

    Write-Host "HTTPS binding configured successfully for IIS website '$WebsiteName'!"

} catch {
    Write-Error "Failed to configure IIS binding: $($_.Exception.Message)"
    exit 1
}

Write-Host "Certificate installation and IIS configuration completed."

3. 如何使用和自动化

  1. 保存脚本: 将上述代码保存为 .ps1 文件,例如 Install-LetsEncryptCert.ps1
  2. 修改变量: 根据你的实际情况修改脚本顶部的变量:
    • $CertFilePath: fullchain.cer 的完整路径。
    • $KeyFilePath: bktai.com.key 的完整路径。
    • $PfxOutputFilePath: 生成 .pfx 文件的目标路径。
    • $PfxPassword: 强烈建议使用一个强密码,并在实际部署时通过更安全的方式管理(例如,Windows Credential Manager 或 Azure Key Vault)。
    • $WebsiteName: 你的 IIS 网站的名称。
    • $HostHeader: 你的主域名 (例如 bktai.com)。
    • $WildcardHostHeader: 你的泛域名 (例如 *.bktai.com)。
  3. 运行脚本:
    • 以管理员身份打开 Windows PowerShell, 注意不是你自己另外安装的,是Windows 10,11,Windows Server自带的那个。
    • 导航到脚本所在的目录。
    • 运行脚本:.\Install-LetsEncryptCert.ps1
    • 执行后是这样的:
Starting conversion of .cer and .key files to .pfx file...
Successfully generated PFX file: D:\Test\bktai.com.pfx
Starting import of PFX certificate to Windows Certificate Store...
   PSParentPath:Microsoft.PowerShell.Security\Certificate::LocalMachine\My
Thumbprint                                Subject
----------                                -------
4BFF8345B5BF323C9D432454358237A542  CN=bktai.com
Successfully imported PFX certificate.
Getting thumbprint of the newly imported certificate...
Retrieved certificate thumbprint: 4BFF803972B3B34234BC9D432C5459558237A542
Starting IIS website binding configuration...
Checking and removing old HTTPS bindings...
Removing old binding: *:443:bktai.com
Adding primary domain HTTPS binding: bktai.com
Binding certificate to IIS website...
Converting thumbprint string '4BFF2803972B23423432C595584234237A542' to byte array...
Converted byte array length: 20
Certificate successfully bound to IIS HTTPS port.
HTTPS binding configured successfully for IIS website 'test.bktai.com'!
Certificate installation and IIS configuration completed.
  1. 自动化周期性执行:
    • Windows 任务计划程序 (Task Scheduler): 这是在 Windows 上实现周期性自动执行的最佳方式。
      • 打开“任务计划程序”。
      • 创建基本任务或创建任务。
      • 触发器: 设置为证书续订周期(例如,Let's Encrypt 证书通常 90 天过期,你可以在 60 天或 75 天时执行)。
      • 操作:
        • 程序/脚本: powershell.exe
        • 添加参数(可选): -ExecutionPolicy Bypass -File "C:\path\to\your\Install-LetsEncryptCert.ps1" (将路径替换为你的脚本实际路径)。Bypass 允许脚本运行,即使执行策略设置为更严格的。
      • 运行身份: 确保任务以拥有管理员权限的用户账户运行(例如,选择“使用最高权限运行”)。
    • Let's Encrypt 客户端: 许多 Let's Encrypt Windows 客户端(如 Certify The Web, win-acme, Posh-ACME)已经内置了自动续订和 IIS 集成功能,强烈建议使用这些工具来简化整个过程,它们通常会处理好证书的获取、安装、PFX 转换和 IIS 绑定,并自动创建计划任务。

4. 脚本说明和注意事项

  • openssl 命令:
    • pkcs12 -export: 指定输出为 PKCS#12 格式(即 PFX)。
    • -out "$PfxOutputFilePath": 输出 PFX 文件的路径。
    • -inkey "$KeyFilePath": 输入私钥文件。
    • -in "$CertFilePath": 输入证书文件(fullchain.cer 包含你的证书和中间证书)。
    • -passout pass:"$PfxPassword": 设置 PFX 文件的密码。
  • Import-PfxCertificate: PowerShell 内置的 cmdlet,用于将 PFX 证书导入到 Windows 证书存储区。Cert:\LocalMachine\My 是将证书导入到“本地计算机”的“个人”存储区,这是 IIS 使用证书的正确位置。
  • 获取证书指纹: 导入证书后,你需要获取其唯一的指纹 (Thumbprint) 才能在 IIS 中引用它。脚本通过 Get-ChildItem -Path Cert:\LocalMachine\My 查找最近导入的、主题匹配的证书。
  • WebAdministration 模块: 这是 IIS 的 PowerShell 管理模块,提供了 Get-Website, Get-WebBinding, New-WebBinding, Remove-WebBinding 等 cmdlet。
  • New-WebBindingSslFlags 1: SslFlags 1 启用了服务器名称指示 (SNI)。对于泛域名证书,通常会用 SNI 来区分不同的子域名,并为它们提供正确的证书。
  • IIS:\SslBindings: IIS 的 SSL 绑定实际上是基于 HTTP.sys 的,PowerShell 通过 IIS:\SslBindings PSDrive 来管理这些绑定。Set-ItemProperty 用于将证书的指纹和存储名称与特定的 IP:Port 绑定关联起来。
  • 安全性:
    • PFX 密码: 在脚本中硬编码密码是不安全的做法。在生产环境中,请考虑使用 PowerShell 的 Get-Credential 来交互式输入密码,或者从更安全的位置(如环境变量、安全存储或 Azure Key Vault)获取密码。对于自动化,Get-Credential 不适用,你可能需要使用一种更复杂但更安全的方式来传递密码。
    • 权限: 确保运行脚本的用户或计划任务账户具有足够的权限来修改证书存储和 IIS 配置。
  • 幂等性: 脚本在添加新绑定之前会尝试移除旧的 HTTPS 绑定。这使得脚本具有一定程度的幂等性,即多次运行不会导致重复的绑定,但要确保移除的逻辑符合你的预期。
  • 错误处理: 脚本中加入了 try-catch 块和 ErrorAction Stop 来捕获和处理潜在的错误,这对于自动化脚本非常重要。

通过使用上述脚本和自动化方法,你可以实现 Let's Encrypt 泛域名证书在 Windows IIS 上的自动化安装和续订。然而,再次强调,对于 Let's Encrypt 证书的自动化管理,使用专门的 Windows 客户端工具(如 Certify The Web 或 win-acme)通常会更简单、更健壮。

Tags: Powershell   证书  
24天前
159
1
0