简介:win-acme.v2.2.9.1701.x64.pluggable.zip 面向 Windows 平台网站管理员与系统集成商,用于简化 Let's Encrypt 等免费 SSL 证书的申请、部署与续订,可配合 IIS、nginx 等 Web 服务器使用,缓解官方下载缓慢的问题。压缩包共 28 个文件,大小约 35.58MB,以 21 个 PowerShell 脚本和 2 个批处理文件为主,另含可执行程序、配置文件、域名列表及 Web 配置示例,便于按不同服务场景调用自动化脚本。现有 392 人学习下载,说明其在证书自动化管理方向具有一定参考价值。读者可从中获得可运行的工具核心、默认配置模板与多类导入脚本,用于快速搭建证书签发流程、调整参数并接入现有服务器环境,减少手工申请和续期带来的重复操作。
1. 证书到期那天才发现没人管:win-acme 在 Windows + nginx 上到底解决什么
一台 Windows 机器上跑着 nginx,前台证书是当初手工贴上去的,到期日只记在某个人脑子里——这种情况我碰到过不止一次。win-acme v2.2.9.1701 x64 pluggable 要做的事很具体:在 Windows 上向 Let's Encrypt 申请免费 SSL 证书,把签下来的文件按 nginx 能读的格式落地,再挂一个计划任务定期续期。听着不复杂,真装起来坑不少:pluggable 包不自带运行时,.NET 版本对不上直接闪退;80 端口被 nginx 自己占着,验证方式选错必然失败;签下来的是 pfx,nginx 根本不认;续期成功了但没人 reload,站点照样报过期。这篇按我自己的装机顺序走一遍,从版本选择、运行时补齐、首次签发、接进 nginx,一直到强制续期演练,每一步都给可抄的命令和参数。
2. 装之前把环境列清:pluggable 包、.NET 运行时与目录权限
这一章不做任何签发动作,只把「装在哪、依赖什么、谁来读文件」这三件事定死。我见过太多人卡在第一步:双击 wacs.exe 弹个黑框就没了,然后开始怀疑包是坏的。九成情况是运行时没装或者装错架构。
2.1 pluggable 和 self-contained 的区别,先认清手里这个 zip
win-acme 的发行包一直有两种形态,文件名里就写着。带pluggable的是「可插拔」版,程序本体不含 .NET 运行时,体积小,启动时去系统里找已安装的运行时;另一种通常标self-contained,把运行时一起打包进去,体积大几十兆,但拷过去就能跑。你这儿拿到的是win-acme.v2.2.9.1701.x64.pluggable.zip,x64是架构,pluggable是形态,两个信息都不能忽略。
| 对比项 | pluggable | self-contained |
|---|---|---|
| 依赖 | 需预装 .NET Desktop Runtime x64 | 无外部运行时依赖 |
| 包体积 | 小 | 大(含运行时) |
| 升级方式 | 换 exe 即可,运行时不动 | 整包替换 |
| 适用场景 | 服务器上已有对应 .NET 运行时 | 干净机器、离线交付、不想动系统运行时 |
我一般优先选 pluggable:服务器上往往已经因为别的服务装过 .NET,不重复占空间,而且以后换 win-acme 版本只要替换目录,运行时那层不用碰。代价是版本必须对齐——这也是下一节要处理的。如果目标机器是完全干净的、或者运维规范不允许往系统里装运行时,直接换 self-contained 包,别在这上面耗时间。判断依据很简单:先看服务器上有没有对应主版本的 .NET Desktop Runtime,有就走 pluggable,没有且不方便装就换包。
2.2 解压路径、架构对齐与目录权限
路径这件事看着琐碎,实际是两个坑的源头。第一,别放在C:\Users\某某\Downloads下面。win-acme 的计划任务是以 SYSTEM 账户跑的,用户目录下的路径在 SYSTEM 上下文里可能根本访问不到,续期任务会静默失败。第二,路径里不要有空格和中文,钩子脚本、nginx 配置、计划任务参数三层转义下来,一个空格就能让整条链路断掉。
我固定的做法是解到C:\win-acme,解压后目录里应该能看到wacs.exe、若干*.dll、*.json配置文件。确认一下 exe 的架构:
# 查看 wacs.exe 的目标架构,确认和系统对齐 $p = "C:\win-acme\wacs.exe" $fs = [IO.File]::OpenRead($p) $br = New-Object IO.BinaryReader($fs) $fs.Seek(0x3C, 'Begin') | Out-Null $peOffset = $br.ReadInt32() $fs.Seek($peOffset + 4, 'Begin') | Out-Null $machine = $br.ReadUInt16() $br.Close(); $fs.Close() switch ($machine) { 0x8664 { "x64 版本,可装在 64 位系统" } 0x014c { "x86 版本,只能装在 32 位系统" } default { "未知架构: 0x{0:X}" -f $machine } }这段读的是 PE 头里的 Machine 字段,0x8664对应 x64,0x014c对应 x86。作用只有一个:确认下载的包和系统架构一致。64 位系统装 x86 版不是完全跑不起来,但一旦涉及证书存储、IIS 插件这些要读系统证书库的操作,就会出现「明明装了却读不到」的玄学现象。
权限方面分两块。C:\win-acme本身给 Administrators 和 SYSTEM 完全控制就行,普通用户不需要写权限。真正要小心的是证书输出目录——后面 PEM 文件会落在那里,nginx 进程要能读私钥。如果 nginx 是用 nssm 之类的工具注册成服务、以 LocalSystem 身份跑的,默认权限够用;如果为了降低风险改用某个低权限账户跑 nginx,就必须显式给那个账户授读取权限,否则 nginx 启动时报cannot load certificate key,日志里只给你一行Permission denied。
注意:证书输出目录绝对不要放在 nginx 的站点根目录(比如
C:\nginx\html)下面。私钥文件一旦落在 web 根目录,理论上可以被当成静态文件下载走,这是实打实的安全事故,不是理论风险。
2.3 把 .NET Desktop Runtime 补齐:版本、架构、验证三步
v2.2.x 这一代的 pluggable 包编译在 .NET 6 上,需要的是.NET Desktop Runtime,注意不是 ASP.NET Core Runtime,也不是最基础的 .NET Runtime。win-acme 里用到 WinForms 做证书选择、对话框这类交互,缺了 Desktop 那一层,启动时就会报框架缺失。
最省事的判断方式其实不是查文档,而是直接双击wacs.exe。缺运行时的时候,控制台窗口会打印类似You must install .NET to run this application或者Framework 'Microsoft.WindowsDesktop.App', version '6.0.0' was not found的信息,里面会明确写出需要的主版本号和架构。按那行提示去装对应版本就行,比猜准。
装之前先看机器上已经有什么:
# 列出已安装的 .NET 运行时,重点看 Microsoft.WindowsDesktop.App 的主版本 dotnet --list-runtimes # 如果 dotnet 命令不存在,说明连运行时都没装,直接去装 Desktop Runtime # 装完后再次执行,期望看到类似: # Microsoft.WindowsDesktop.App 6.0.x [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]dotnet --list-runtimes输出的每一行是「框架名 版本 [安装路径]」。要找的是Microsoft.WindowsDesktop.App,并且主版本号(6.x / 8.x)要和 wacs.exe 要求的对上。次版本号(6.0.25 还是 6.0.36)不影响,运行时向前兼容同一主版本内的补丁。架构上必须装 x64,和 exe 保持一致;混装 x86 运行时时,dotnet --list-runtimes会多列出一组[Program Files (x86)]路径下的条目,容易看花眼。
验证是否装对,别只看安装程序说成功,直接跑一次:
# 只打印版本和帮助,不触发任何证书操作 C:\win-acme\wacs.exe --version能正常打印版本号,说明运行时这层通了。这一步过不去,后面所有章节都不用看。
3. 第一次签发:用命令行把 HTTP-01 验证跑通
环境通了之后进入签发。我的习惯是先用命令行而不是向导——向导点错一步要重来一遍,命令行改个参数重跑就行,而且命令能直接存档成文档。
3.1 先拿 --test 跑一遍,把参数拼写和域名解析验证掉
win-acme 支持--test,作用是走 Let's Encrypt 的测试环境,签出来的证书浏览器不信任,但整个验证、签发、落地的流程和正式环境完全一致。第一次装必须走这个,原因很实际:Let's Encrypt 对正式环境有频率限制,同一域名一周内重复签发次数超了会被临时封,参数拼错重试几次就可能撞上。测试环境宽松得多,随便折腾。
完整命令长这样:
C:\win-acme\wacs.exe ^ --target manual ^ --host example.com,www.example.com ^ --validation filesystem ^ --webroot C:\nginx\html ^ --store pemfiles ^ --pemfilespath C:\certs ^ --accepttos ^ --emailaddress ops@example.com ^ --test逐段说清楚:
--target manual表示手动指定域名,不让程序自己去扫 IIS 站点。Windows 上如果装了 IIS,win-acme 默认会去枚举 IIS 绑定,扫出来的域名跟你真正想签的未必一致,显式写 manual 更可控。
--host后面跟域名列表,逗号分隔,不要空格。第一个域名会成为证书的主名(Subject),其余进 SAN 扩展。www和裸域写在一起签一张证书,省一次验证。
--validation filesystem指定验证方式为「写文件」,win-acme 会把验证文件写进--webroot指定的目录下的.well-known\acme-challenge\,然后由 Let's Encrypt 从公网访问http://你的域名/.well-known/acme-challenge/随机串来校验。这是和 nginx 共存时最稳的方式,原因下一节展开。
--store pemfiles是最关键的一个参数,指定证书落地格式为 PEM 文件而不是 pfx。--pemfilespath是输出目录。
--accepttos表示接受服务条款,非交互运行时必须带,否则卡在确认提示上。--emailaddress填到期提醒邮箱,别填 noreply 之类的假地址,证书快过期时的提醒邮件是真的有用。
参数名以wacs.exe --help的输出为准,v2.2.x 里这套拼写是能用的。第一次跑建议加--test,等整条链路包括钩子脚本都验证通过,把--test去掉再跑一次,拿正式证书。
3.2 filesystem 验证与 nginx 的 /.well-known 放行
验证方式一共有几种,Windows + nginx 这个组合下要避开selfhosting。它的原理是 win-acme 自己起一个临时 HTTP 监听去响应验证请求,问题是 80 端口已经被 nginx 占着——绑不上,直接失败。报错大概是Failed to bind to port 80或者验证超时,第一次见容易误判成防火墙问题。
filesystem反过来,它不抢端口,只往一个目录里扔文件,剩下的交给已经在跑的 nginx。所以 nginx 那边必须保证两件事:/.well-known/acme-challenge/这个路径能访问到C:\nginx\html下的真实文件;这个路径不能被重定向到 https。
server { listen 80; server_name example.com www.example.com; # ACME 验证目录必须直出静态文件,优先级要高于下面的 301 location ^~ /.well-known/acme-challenge/ { root C:/nginx/html; default_type text/plain; try_files $uri =404; } # 其余请求照常跳 https location / { return 301 https://$host$request_uri; } }location ^~的前缀匹配优先级高于正则和普通前缀,这样即使别处写了location /做跳转,验证路径也不会被带走。root C:/nginx/html里的路径用正斜杠,nginx 在 Windows 上认这个写法,反斜杠反而容易出问题。default_type text/plain是保险,某些精简过的 mime.types 不认识无扩展名文件,返回的 Content-Type 不对时验证端可能拒收。try_files $uri =404让文件不存在时老实返回 404,而不是绕到别的 location 去。
改完配置先nginx.exe -t检查语法,再nginx.exe -s reload生效。然后手工造一个测试文件验证链路:
# 在验证目录下放一个探针文件 "probe-ok" | Out-File -Encoding ascii C:\nginx\html\.well-known\acme-challenge\probe.txt # 从本机访问,确认返回 probe-ok 而不是 301 或 404 Invoke-WebRequest -Uri "http://example.com/.well-known/acme-challenge/probe.txt" -MaximumRedirection 0第二个命令用-MaximumRedirection 0,一旦发生跳转会直接抛异常,比自动跟随重定向更容易发现问题。返回内容里有probe-ok就说明链路通了,可以把探针文件删掉。这一步能挡掉后面大概一半的失败。
3.3 落地成 PEM:输出文件长什么样,私钥为什么不加密
--store pemfiles会在--pemfilespath目录下生成一组文件,文件名前缀默认是主域名。我这边实际会看到这些东西:
| 文件名 | 内容 | nginx 用不用 |
|---|---|---|
example.com-chain.pem | 站点证书 + 中间证书,按顺序拼好 | 用,配给ssl_certificate |
example.com-key.pem | 私钥 | 用,配给ssl_certificate_key |
example.com-crt.pem | 只有站点证书,不含中间证书 | 一般不用 |
example.com-chain-only.pem | 只有中间证书 | 排查链问题时用 |
重点是别把-crt.pem配给 nginx。少了中间证书,桌面浏览器往往能靠缓存或者 AIA 自动补齐,看起来一切正常,但移动端、微信内置浏览器、部分 Java 客户端会直接报证书链不完整。这是那种「我这儿好好的啊」的经典翻车场景,排查起来能耗掉半天。配-chain.pem就不会有这个问题。
私钥加密这块,win-acme 在向导里会问要不要给私钥加密码。走命令行pemfiles存储时不涉及交互,默认落的是明文私钥。nginx 这边确实也支持加密私钥,但每次 reload 都要交互输入密码,作为服务跑的时候直接卡死。所以我的做法是私钥不加密,改用文件系统 ACL 控制访问——把C:\certs的权限收紧到 SYSTEM、Administrators 和 nginx 运行账户三个主体,其他一律不给。
4. 把证书接进 nginx:钩子脚本、reload 与续期计划任务
证书签下来只是半程。这一章解决「签完之后自动有人管」的问题,否则三个月后照样过期。
4.1 钩子脚本:让 win-acme 签完就调用你
win-acme 提供--script和--scriptparameters两个参数,前者指定一个可执行文件或脚本,后者传参。证书签发或续期成功后,它会把参数替换成实际值再调用这个脚本。最常用的占位符有几个:{CacheFile}是 pfx 格式的缓存文件路径,{StorePath}是 PEM 输出目录,{RenewalId}是续期任务编号,{SubjectCommonName}是证书主域名。完整列表在--help里能看到,版本之间有细微差异,以本机输出为准。
我一般写一个 .bat 而不是 .ps1。原因是 PowerShell 脚本受执行策略限制,计划任务以 SYSTEM 跑的时候,执行策略、模块路径都可能和你在交互式终端里不一样,多了两层不确定性。bat 虽然土,但没有这些问题。
@echo off setlocal enabledelayedexpansion set "NGINX_DIR=C:\nginx" set "LOG_FILE=C:\certs\deploy.log" echo [%date% %time%] deploy start, args=%* >> "%LOG_FILE%" rem nginx 用相对路径找 conf 和 pid,必须先切到安装目录 cd /d "%NGINX_DIR%" || ( echo [%date% %time%] cd failed >> "%LOG_FILE%" exit /b 1 ) rem 先做配置检查,配置有问题就不要 reload,避免把在跑的进程搞挂 nginx.exe -t >> "%LOG_FILE%" 2>&1 if errorlevel 1 ( echo [%date% %time%] nginx -t failed, skip reload >> "%LOG_FILE%" exit /b 1 ) nginx.exe -s reload >> "%LOG_FILE%" 2>&1 if errorlevel 1 ( echo [%date% %time%] reload failed >> "%LOG_FILE%" exit /b 1 ) echo [%date% %time%] deploy done >> "%LOG_FILE%" exit /b 0几个细节值得说。cd /d "%NGINX_DIR%"这行不能省:nginx 在 Windows 上是用编译时前缀加上相对路径去找conf/nginx.conf和logs/nginx.pid的,如果计划任务在C:\Windows\System32下调用它,找不到 pid 文件,-s reload就变成一个静默的空操作,退出码还是 0。这是最阴的一种失败——日志里一切正常,证书就是不生效。
nginx.exe -t放在 reload 前面,是先验证再动作。配置文件写错了直接 reload,nginx 会拒载新配置继续用旧的,表面上「没崩」,实际上新证书没生效,你以为搞定了其实没有。先-t拿到明确失败信号,比事后猜强。
日志一定要写。计划任务里的脚本出错是没有任何界面的,不落日志等于黑匣子。%*把 win-acme 传进来的占位符参数原样记下来,排查时能确认参数替换是否符合预期。
调用的时候这样拼:
C:\win-acme\wacs.exe ^ --renew ^ --force ^ --script "C:\certs\deploy-nginx.cmd" ^ --scriptparameters "{RenewalId} {SubjectCommonName}"--renew表示走续期流程,--force强制立即执行而不是等到临期。这两个参数只用来演练和验证,日常续期不需要--force。--scriptparameters里的占位符会被替换成真实值再传给脚本,脚本里用%1%2接。我这里其实没在脚本里用这两个参数,纯粹是记进日志方便追溯是哪一次续期触发的部署。
4.2 nginx.conf 里两个 ssl_certificate 的落点
证书文件就位之后,改 443 的 server 块:
server { listen 443 ssl; http2 on; server_name example.com www.example.com; # 必须用 chain 文件(站点证书 + 中间证书),不要用 crt ssl_certificate C:/certs/example.com-chain.pem; ssl_certificate_key C:/certs/example.com-key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; location / { root C:/nginx/html; index index.html; } }路径统一用正斜杠,nginx 在 Windows 上能正确处理。ssl_certificate指向-chain.pem,原因前面说过——它包含站点证书和中间证书,客户端能拿全整条链。ssl_certificate_key指向-key.pem,两个文件必须配对,混用旧版本的私钥和新版本的证书会导致 nginx 启动失败,报key values mismatch,这个报错信息算比较友好的,一眼能看懂。
http2 on;是 nginx 1.25.1 之后的新写法,老版本写listen 443 ssl http2;。版本差异这里容易翻车,nginx -v先确认一下自己跑的是哪个版本再抄配置,否则-t会直接报unknown directive "http2"。
改完照例先检查:
cd /d C:\nginx nginx.exe -t nginx.exe -s reload-t的输出会明确告诉你配置语法有没有问题、证书文件能不能读到。这一步通过,443 就基本能起来了。
4.3 续期计划任务:别让两套任务互相打架
win-acme 默认会在 Windows 任务计划程序里创建一个续期任务,名字大概是win-acme renew (acme-v02.api.letsencrypt.org),触发时间每天固定跑一次,每次运行时检查有没有证书进入续期窗口(一般是到期前 30 天左右),有就续,没有就退出。这个机制本身够用,不用你再写一个。
先确认它在不在:
# 查 win-acme 创建的计划任务,确认存在且状态为 Ready Get-ScheduledTask | Where-Object { $_.TaskName -like "*win-acme*" } | Select-Object TaskName, State, @{n='RunAs';e={$_.Principal.UserId}} # 看最近一次执行结果和下次运行时间 Get-ScheduledTaskInfo -TaskName "win-acme renew (acme-v02.api.letsencrypt.org)"State是Ready表示任务正常待命。RunAs一般是SYSTEM,这意味着脚本里所有相对路径、用户环境变量、映射的网络驱动器都不可用——这就是 4.1 里那句cd /d的必要性来源。Get-ScheduledTaskInfo的LastTaskResult是 0 表示上次跑成功了,非 0 就得去看 win-acme 自己的日志。
如果你有统一的任务编排系统,想把续期收归到自己的调度里,就在签发和续期命令里都加上--notaskscheduler,win-acme 就不会再自己建任务。关键是别两边都留着。两套任务同时跑,撞在一起的时候,一边在写证书文件,另一边在 reload nginx,运气不好就 reload 到一个写了一半的文件,nginx 直接起不来。这种事故我见过一次,之后就把这条当成硬规矩:要么用自带的,要么用自己的,绝不允许并存。
win-acme 自己的执行日志默认落在wacs.exe同级的Log目录下,按日期分文件。续期失败的第一现场永远在那里,看LastTaskResult不如直接翻日志来得快。
5. 排查与避坑:五条踩过的记录
这一章都是真踩过的,按「现象 → 原因 → 解决」写,遇到对得上的直接抄解决方案。
5.1 80 端口被占用,验证直接超时
现象:命令跑起来之后停在验证阶段,等一两分钟报验证失败,提示大概是连接超时或者 refused。防火墙、安全组都查过了,公网访问http://域名/是通的。
原因:--validation选成了selfhosting。这种方式要 win-acme 自己监听 80 端口,但 80 已经被 nginx 占着,绑不上;绑不上就没法响应验证请求,验证端只能超时。
解决:改成--validation filesystem,配合--webroot指向 nginx 站点根目录,让 nginx 去服务验证文件。如果因为某些原因必须用自托管验证,那就得错峰——先停 nginx,跑签发,再起 nginx。生产上这么干显然不合适,所以还是 filesystem。
5.2 验证请求被 301 跳到 https,一直失败
现象:探针文件在本机用curl http://127.0.0.1/.well-known/acme-challenge/probe.txt能拿到,但用域名访问就被跳到 https,验证始终过不去。
原因:站点为了全站 https,写了一条return 301 https://$host$request_uri;的兜底规则,把/.well-known/也一起带走了。Let's Encrypt 的验证器不跟随跨协议跳转,看到 301 直接判定失败。
解决:在 80 的 server 块里,把/.well-known/acme-challenge/的 location 放到跳转规则前面,并用^~修饰符保证前缀匹配优先。这个改动只影响验证路径,其他请求照常跳转,对 SEO 和用户无感。
5.3 钩子脚本在交互终端里能跑,计划任务里静默失败
现象:手工执行一次wacs.exe --renew --force一切正常,证书更新、nginx reload 都成功。等计划任务触发的时候,证书文件确实更新了,但 nginx 用的还是旧的,站点显示的到期时间没变。
原因:计划任务以 SYSTEM 身份在C:\Windows\System32下启动脚本,脚本里写的是相对路径调用nginx.exe -s reload。nginx 找不到 pid 文件,reload 变成空操作,退出码还是 0,所以从任务计划的结果看是「成功」。
解决:脚本开头强制cd /d C:\nginx,用绝对路径调用nginx.exe。另外把每次执行的输出和退出码都追加到日志文件,出问题时有据可查。从那以后我写这类钩子脚本,第一行一定是切目录,第二行一定是开日志。
5.4 续期成功但 nginx 还在用旧证书
现象:日志里 win-acme 明确写了续期成功,C:\certs下的文件修改时间也是新的,但浏览器里看证书还是旧的,重启 nginx 之后就正常了。
原因:--script参数压根没配,或者早期配了后来换版本时漏掉了。win-acme 只管把文件写下来,它不知道谁在用这些文件,更不会主动去通知 nginx。
解决:补上--script和--scriptparameters。已经存在的续期任务可以用--renew --force重新跑一次把钩子挂上去。配完之后务必做一次强制续期演练,确认从「文件更新」到「nginx reload」整条链是通的,别等到真过期那天才发现钩子没挂。
5.5 私钥权限过宽或加密,nginx 起不来
现象:nginx -t报cannot load certificate key "C:/certs/example.com-key.pem",后面跟Permission denied或者bad decrypt。
原因:两种可能。一是 nginx 以低权限账户运行,C:\certs目录没给这个账户读权限;二是私钥被加了密码,nginx 启动时没有交互界面输入密码。
解决:权限问题用icacls明确授权,只给必要的主体:
# 关掉继承,只保留 SYSTEM、Administrators 和 nginx 运行账户 icacls "C:\certs" /inheritance:r icacls "C:\certs" /grant "SYSTEM:(OI)(CI)R" icacls "C:\certs" /grant "Administrators:(OI)(CI)F" icacls "C:\certs" /grant "NGINX_SVC:(OI)(CI)R"/inheritance:r移除从上级目录继承来的 ACE,避免某个宽松的父目录权限把私钥暴露出去。(OI)(CI)表示对象继承和容器继承,让目录下新建的证书文件自动带上同样权限,这样每次续期生成新文件不用重新授权。私钥加密的问题只能重新签发一次,落地时不加密。
6. 装完怎么验证:跑一次强制续期演练才算完
证书装上、nginx 起来了、浏览器小锁出现了,这些都不算装完。真正要验证的是「三个月后它会不会自己续上」。这件事唯一的验证方式就是手动触发一次完整的续期流程,然后检查每一环的产物。
第一步,强制续期:
C:\win-acme\wacs.exe --renew --force--force会跳过「是否临近过期」的判断,立刻走一遍完整的签发、落地、调用钩子。命令结束后先看C:\certs\deploy.log,里面应该有本次的时间戳、nginx -t的输出和 reload 的结果。日志里出现deploy done才算钩子那一段通了。
第二步,确认证书文件确实是新的:
Get-ChildItem C:\certs\*.pem | Select-Object Name, Length, LastWriteTimeLastWriteTime应该是刚才那一分钟。如果时间没变,说明 win-acme 那一段就没成功,回头去看C:\win-acme\Log下当天的日志。
第三步,从外部视角看站点实际在用的证书。本地文件对不代表线上对:
# 抓取 443 上真实返回的证书,看颁发者和到期时间 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates输出里notAfter应该是刚续的新日期。如果还是旧日期,说明 nginx reload 没生效或者 reload 到了旧配置,回到 5.3 和 5.4 排查。没有 openssl 环境的话用 PowerShell 也行:
# 不跟随跳转,直接读 TLS 握手拿到的证书 $req = [Net.HttpWebRequest]::Create("https://example.com") $req.AllowAutoRedirect = $false $req.Timeout = 8000 $req.GetResponse() | Out-Null $cert = $req.ServicePoint.Certificate [PSCustomObject]@{ Subject = $cert.Subject Issuer = $cert.Issuer Expires = $cert.GetExpirationDateString() }第四步,把这张表从头到尾对一遍,任何一项没打勾都别急着收工:
| 检查项 | 期望结果 | 不通过时看哪里 |
|---|---|---|
wacs.exe --version | 正常打印版本号 | .NET Desktop Runtime 是否装对架构和主版本 |
| 探针文件 HTTP 访问 | 返回文件内容,无跳转 | nginx 的/.well-knownlocation 优先级 |
C:\certs下 PEM 文件 | -chain.pem和-key.pem齐全且时间新 | 存储参数是否写成了 pemfiles |
nginx -t | syntax is ok, test is successful | 证书路径、http2指令版本差异 |
deploy.log | 有本次时间戳和deploy done | 钩子脚本路径、执行账户权限 |
| 外部抓取证书 | notAfter为最新日期 | reload 是否真的执行了 |
| 计划任务状态 | Ready,LastTaskResult为 0 | 任务是否重复创建、账户上下文 |
最后还有一件容易忽略的事:把--test留下的痕迹清掉。测试环境签的证书浏览器不信任,如果之前用--test跑过正式域名,得确认C:\certs下现在放的是正式证书而不是测试证书,最直接的判断是看颁发者——测试环境的颁发者名字里带 staging 字样。我一般会在正式签发完成后,直接从外部抓一次证书确认颁发者,避免把测试证书留在生产上。
从那以后我每次装完这套东西,都会强制走一遍--renew --force加外部抓证书,不管当天是不是刚签过。因为真正会出问题的从来不是「第一次能不能签下来」,而是三个月后那个没人盯着的凌晨,续期任务跑了、文件写了、nginx 没 reload。演练一次花五分钟,能省掉一次半夜被告警叫起来。希望帮到你。
本文还有配套的精品资源,点击获取