如何在 Windows 服务器上更换 SSL 证书
前言
相信各位在使用(云)服务器部署并发布 HTTPS 网站的时候,都遇到过这一个看似不起眼,但很多时候又很令人不舒服的事情:服务器上网站的 SSL 证书过期了。让我们先来聊聊 SSL 证书吧:SSL 证书是一种数字证书,类似于驾驶证、护照和营业执照的电子副本,它由受信任的数字证书颁发机构在验证服务器身份后颁发,具有服务器身份验证和数据传输加密功能;服务器在部署了 SSL 证书后,可以确保用户在浏览器上输入的信息和从服务器上查询的信息在传输链路上是高强度加密传输的,在现有算力水平下几乎不可能被非法篡改和窃取;同时,SSL 证书向网站访问者证明了服务器的真实身份,相当于确认了“你访问的网站的确是你想访问的网站”。
既然 SSL 证书是一种身份证明,还能在非对称加密过程中起到重要作用,若 SSL 证书过期后会发生什么呢?答案是:站点的运营方在技术上感知不强,但访问站点的用户会立刻发觉有问题。就拿 Anawaert Blog 来说,笔者在 Firefox 上进行访问的时候,浏览器立刻会提示:“当心,似乎有异常情况”。
点击“高级”选项后,我们能看到 Firefox 对本次访问拦截相关的技术性解释,并且给出了一个“继续访问”的选项。
笔者相信大部分用户在遇到这种情况的时候体验是比较糟糕的,相当于自己什么都没干,但是就是莫名其妙地被打断了访问,而且还被提示有不安全的风险。纵使有部分用户非常清楚您的网站是安全的,单纯只是因为 SSL 证书过期了,但大部分用户在遇到这类提示后,就会选择停止并退出访问这个网站 —— 毕竟不是人人都喜欢“无视风险继续安装”。实际上,因为 SSL 证书的过期还引起过一些有意思的事故,其中不乏一些国际大厂:2024 年 11 月,大量 Apple Music 的用户反馈在使用 Apple Music 的时候收到了“此服务器的证书无效”的警告,虽然 Apple Music 应用本身还能正常启动,但是歌曲无法正常播放,严重影响了一众听歌人的正常使用。搞笑的是,在当时苹果主张将 SSL/TLS 证书有效期从 398 天缩短到 45 天,但自己却忘记了及时更换 SSL 证书……
既然保证 SSL 证书的有效在现代互联网环境中是相当重要的,那么接下来,就让我们来看看如何为服务器进行 SSL 证书更换吧。本文使用的服务器运行的是 Windows Server 2019 Datacenter 操作系统,网站托管使用 IIS 10.0,而本地使用的是一台运行着 macOS Sonoma 14.8.7 的 MacBook。本文将不使用远程桌面程序,而是主要使用 CLI 对服务器进行操作与交互,因此在理论上可迁移至其他同样支持 ssh/scp 命令的本地设备,比如运行着 Debian/Ubuntu 的 Linux 主机等。
准备好可导入 IIS 的 SSL 证书
由于本文中的服务器由腾讯云托管,而 IIS 使用的证书是 .pfx 格式的,因此笔者将在腾讯云控制台的 SSL 证书 页面中下载“IIS(pfx 格式)”:
从腾讯云下载的 IIS 证书是一个 .zip 压缩包,若没有在控制台中专门设置证书密钥,那么压缩包内还会包含一个对应该证书的 .txt 明文密钥文件,本文将默认是上述“证书 + 密钥”的情况。若您从其他其他(云)平台购买了 SSL 证书,或者是可直接通过某些方式获得签发的 SSL 证书,请自行联系相关提供商以获得对应域名的 SSL 证书文件,并设置或获取对应的密钥。
完成下载后,建议在默认下载位置(比如 ~/Downloads/)下新建一个目录,比如 pfx/。使用以下命令新建目录并解压至该目录中:
cd [default/download/dir/] # [default/download/dir] 为您的默认下载位置
mkdir ./pfx/
unzip "*_iis.zip" -d ./pfx/ # 使用 unzip 命令进行解压至 pfx/ 中,笔者默认您的系统已安装了 unzip使用 SSH 将证书文件拷贝至服务器的指定目录
首先使用 ssh 命令登陆服务器:
ssh [username]@[server.com] # [username] 为服务器上(管理员)的账户名称;[server.com] 是服务器地址,可以是 IPv4 地址也可以是域名在这里,笔者使用的是系统内置的管理员账户,且由于 anawaert.com 已绑定服务器 IP,因此笔者直接使用域名作为地址。
熟悉的 Windows PowerShell CLI 界面出现后,表明我们已经成功通过 SSH 连入服务器。笔者已经提前在用户目录下创建一个名为 ssl_cert\ 的文件夹,专门用于存放 SSL 证书文件:
当然,这些都是已经被导入到 IIS 过的证书了,可以直接删除,使 ssl_cert\ 变成一个空目录。使用 pwd 命令来获得当前目录的绝对路径,并记下来,稍后会有用。接着输入 exit 来断开 SSH 链接,让我们先回到 macOS 的 zsh。
在上一小节,我们将多个网站的 SSL 证书文件解压到了 [默认下载位置]/pfx/ 中,那么,使用 scp 命令将解压后的证书文件拷贝至服务器的 ~\ssl_cert\ 中:
scp -r [dir1] [dir2] ... [username]@[server.com]:['target/dir/'] # [dirx] 为第 x 个目录的路径(也可以使用通配符),若有空格,请使用引号来进行包含;[username]@[server.com] 为刚才的服务器登陆用户与地址;['target/dir/'] 为服务器上的目标路径,为了避免 Unix/Linux 与 Windows 路径之间的转义问题,target/dir/ 也建议使用单引号,并使用绝对路径此时,再通过 ssh 命令登陆服务器,检查一下 ~\ssl_cert\ 是否真的有 .pfx 拷入:
显然,新的 SSL 证书已经被拷贝至 ~\ssl_cert\ 中。那么接下来,我们就要基于这些 .pfx 证书,以及 IIS 提供的 PowerShell 接口来实现指定网站 SSL 证书的替换了。
使用 PowerShell 来实现 SSL 证书的替换
接下来的绝大部分操作都是在服务器上的 Windows PowerShell 环境中执行。PowerShell 的语法格式与 zsh/bash 有很大不同,可以归结为 Command-Name -Parameters ... 风格,比如 ls 命令实际上是 Get-ChildItem -Path . 的别名,因此接下来笔者将全面使用符合 PowerShell 规范的命令格式,而不再使用简写与别名。
首先,导入 WebAdministration 模块,用于获取当前 IIS 中的站点名称、状态以及绑定信息:
Import-Module WebAdministration为了避免 Windows PowerShell 5.1 中较旧的 PSReadLine 在 SSH 的长命令交互中抛出 System.IndexOutOfRangeException 异常,我们还是先移除 PSReadLine 模块:
Remove-Module PSReadLine在移除了 PSReadLine 模块之后,请尽可能保证命令被一次性输入进终端,因为此时交互窗口仅仅保留了最基本的输入能力。可以这样操作:先将命令粘贴至文本编辑器,在文本编辑器中修改完成后,再粘贴进终端窗口。
然后,在 PowerShell 中初始化一个对象数组,每个数组元素分别存放站点名称、对应主机名以及对应的新 SSL 证书所在目录:
$sites = @(
# PowerShell 使用 [type/attribute] 来标记对象类型或特性,此处 [pscustomobject] 为类型注解
[pscustomobject]@{
SiteName = '[IIS-Site-Name]' # [IIS-Site-Name] 为实际的站点名称
HostName = '[Host-Name]' # [Host-Name] 为实际的站点主机名(域名)
CertDir = "[New-Cert-Dir]" # [New-Cert-Dir] 准备用于该站点进行替换的 SSL 证书所在目录,笔者在此处使用了双引号
}
)若您不清楚服务器上的 IIS 所有站点的具体名称和运行状态,可以使用以下命令进行获取:
Get-Website | Format-Table Name, State, PhysicalPath -AutoSize笔者有三个站点需要更换证书,分别是博客、文档和主页,因此可以像下图这样为 $sites 变量赋值:
重点来了,使用以下命令段来完成从读取证书文件、对应密钥到切换绑定的全流程,具体步骤解析请看注释内容:
# 使用 foreach 语句遍历处理所有站点,并获得返回结果
$results = foreach ($site in $sites) {
# 从每个目录中各取一个 .pfx 文件和 .txt 密钥明文
$pfxFile = Get-ChildItem -LiteralPath $site.CertDir -Filter '*.pfx' -File | Select-Object -First 1
$passwordFile = Get-ChildItem -LiteralPath $site.CertDir -Filter '*.txt' -File | Select-Object -First 1
# 读取对应证书的密钥,为了字符串安全还是进行一下转换
$passwordText = Get-Content -LiteralPath $passwordFile.FullName -Raw
$securePassword = ConvertTo-SecureString $passwordText -AsPlainText -Force
# 获取当前 IIS 的 HTTPS 绑定
$binding = Get-WebBinding -Name $site.SiteName -Protocol 'https' -Port 443 -HostHeader $site.HostName
# 获取当前站点绑定的旧指纹
$oldThumbprint = [string]$binding.certificateHash
# 开始导入新证书,并将现有绑定切换到新证书
$newCertificate = Import-PfxCertificate -FilePath $pfxFile.FullName `
-CertStoreLocation 'Cert:\LocalMachine\My' -Password $securePassword -ErrorAction Stop |
Where-Object { $_.HasPrivateKey } | Select-Object -First 1
$binding.AddSslCertificate($newCertificate.Thumbprint, 'My')
# 本次循环的返回对象($results 数组的元素)
[pscustomobject]@{
SiteName = $site.SiteName
HostName = $site.HostName
OldThumbprint = $oldThumbprint
NewThumbprint = $newCertificate.Thumbprint
NewNotAfter = $newCertificate.NotAfter
}
}若无 PowerShell 报错,则代表已成功完成 $sites 中所有网站的 SSL 证书替换。当然,为了明文密钥不在上下文中泄露,还可以执行一下下面这条语句:
# 非必需,但中间用到了 $passwordText 和 $securePassword 这两个变量,最好还是进行删除
Remove-Variable passwordText, securePassword验证新证书的替换及后处理
新开一个终端窗口(不要断开先前窗口中与服务器的 SSH 连接),在 zsh 中,使用以下 OpenSSL 命令来验证网站实际返回的证书信息:
openssl s_client -connect [website-host-name]:443 -servername [website-host-name] </dev/null 2>/dev/null |
openssl x509 -noout -subject -issuer -dates -serial -fingerprint -sha256 # [website-host-name] 为您网站的实际主机名,比如 blog.anawaert.com以 blog.anawaert.com 为例,重点关注 notBefore 与 notAfter 字段,可以发现已经变成了新的 90 天有效期,说明我们的确成功地为网站更换了 SSL 证书:
大功告成!接下来就是一些收尾工作了。回到先前与服务器建立了 SSH 连接的终端窗口中,运行以下 PowerShell 命令来删除 IIS 中的旧证书:
$results.OldThumbprint | Sort-Object -Unique |
ForEach-Object {
Remove-Item "Cert:\LocalMachine\My\$_" -Force
}至于是否需要删除被我们使用 scp 命令拷贝到服务器的 .pfx 证书文件或对应的明文密钥文件,各位读者可以根据自己的需要进行处理,笔者此处就不再展开了。
总结
本文以运行 Windows Server 2019 Datacenter 与 IIS 10.0 的远程服务器为例,完整介绍了如何在 macOS 上通过 SSH 上传新的 PFX 证书及密钥,并借助 Windows PowerShell 批量导入证书、更新多个 IIS 站点的 HTTPS 绑定,最后使用 OpenSSL 验证证书是否生效并清理旧证书。
当然,这只是开始的一步,真实的(自动化)运维比本文中所介绍的要复杂得多。实际上,本文中用到的 zsh 命令或 PowerShell 命令可以分别写成 .sh 与 .ps1 脚本,未来只需修改站点信息对应的数组,即可实现“一键换证”。本文中的 PowerShell 命令段也有不少可以提升的地方,比如使用 try...catch...finally 块套在 foreach 循环外面会更好 —— 毕竟谁也不希望命令在中间执行出问题了以后还会顺序执行吧,但碍于篇幅所限,以及命令内容的直观性,笔者最后还是决定不加入更多的错误处理与兜底措施,这些部分留给各位读者自行发挥。若有更多关于 SSH、SSL 证书和服务器交互相关的内容,欢迎各位在下方评论区留言、讨论,感谢您的阅读!