简介:win-acme.v2.2.9.1701.x64.pluggable.zip 是面向 Windows 网站管理员、运维与系统集成商的免费 SSL 证书自动化工具包,重点解决 Let's Encrypt 等证书的申请、续订与部署问题,也适合需要为 IIS、nginx 等环境快速配置 HTTPS 的用户。压缩包共 28 个文件、约 35.58MB,以 21 个 PowerShell 脚本和 2 个批处理文件为主,辅以 wacs.exe 主程序、JSON/XML 配置及 DAT、TXT 等数据文件,整体结构偏脚本化,便于按场景调用。资源已有 401 人学习下载,核心价值在于提供可离线获取的完整工具链与默认配置,并附带覆盖 Exchange、RDS、Azure 及常见 DNS 服务商等证书导入场景的脚本,可帮助读者减少部署阻力、快速搭建证书管理流程。
1. 给 Windows 上的 nginx 签免费 SSL,我最后把这件事交给了 win-acme
在 Windows 上运行 nginx,同时还要给站点配置免费 SSL,真正麻烦的地方在于证书续期之后由谁来执行 reload,申请证书本身倒不难。我前后试过几种方案,最后稳定下来的组合是 win-acme 2.2.9.1701 的 x64 pluggable 包,配合 nginx 的 webroot 验证。这套组合的好处是:不需要改动 nginx 的编译参数,也不需要把 80 端口交给别的服务,证书直接以 pem 格式保存到磁盘上,nginx 引用两个文件就能跑起来。但它也有不体面的地方:如果第一次装的路径或参数猜错,验证阶段会直接给你返回 404,而日志里写的原因往往和真实原因相差很远。这篇内容按我第一次装在一台 Windows Server 上的顺序记录,从选择哪个包、解压到哪、settings.json 修改哪几行,到续期后如何让 nginx 重新读取证书,以及装坏之后按什么顺序排查。
2. 环境先摆清楚:win-acme 2.2.9.1701 动手前要确认的四件事
2.1 pluggable 包和 trimmed 包:文件名长的那份才是完整版
同一个版本号在发布页上通常有两份 x64 压缩包,名字中只差一个词。pluggable 是带全套插件的那份,验证插件(filesystem、selfhosting、dns、tls-alpn)、存储插件(pemfiles、pfx、certificate store)、安装插件(iis、none)都在里面;trimmed 是精简过的,只留最常见的一小部分。给 nginx 用,又要让证书直接以 pem 文件保存到磁盘,就得选 pluggable,因为 pemfiles 这个存储插件在精简包里不一定存在。我见过有人拿着 trimmed 包在命令行里反复试--store pemfiles,程序只提示一句“找不到指定的存储类型”,然后就开始怀疑版本有问题,实际上换包就能解决。判断方法很简单,解压后看目录里 dll 的数量和文件名前缀,插件目录齐全的就是完整包。另外不要同时把两个版本的 win-acme 解压到同一个目录里覆盖安装,2.x 的配置文件和缓存目录结构在不同小版本之间改过,混着用会出现“证书续期成功但文件没更新”的奇怪现象。
2.2 x64 与系统版本:能不能跑,先看这两行
这个包只跑 64 位 Windows,32 位系统不用尝试了。系统版本上,Windows 10、Windows 11、Windows Server 2016 及以上都没问题;Server 2012 R2 能跑,但老机器的根证书库经常没更新全,握手阶段容易报一些看不出原因的 TLS 错误,我的做法是先把系统补丁打全再动手。动手前用两行命令确认,比装到一半才发现问题省事得多:
# 确认是不是 64 位系统,False 就直接换机器 [Environment]::Is64BitOperatingSystem # 看系统版本号,6.1 是 2008 R2,6.3 是 2012 R2,10.0 是 2016 之后 [System.Environment]::OSVersion.Version第一行返回布尔值,第二行返回版本对象。如果第一行是 False,后面所有步骤都不用看了。版本号这一项主要用来判断系统是否过旧,2012 R2 对应的 6.3 属于“能跑但要小心”的范围,实际部署时我倾向于把它换到 2016 以上的机器上签发,再把证书文件同步回去,这样可以省去很多在旧系统上排查 TLS 问题的时间。
还有一点容易忽略:win-acme 2.2 这一代用的是自包含构建,压缩包里已含运行时,不需要单独安装 .NET 运行时。但这意味着程序目录会比较大,解压时不要以为下错了包。
2.3 解压路径与目录规划:不要放在 Program Files
这是我最想强调的一条。放在C:\Program Files下面,程序写日志、写缓存、生成 settings.json 都需要管理员权限,交互式运行时会弹出 UAC,计划任务运行时又会因为权限上下文不同而写入失败,表现就是“手动跑没问题,定时跑就没反应”。我一般固定在C:\win-acme,路径里不要有空格、不要有中文、不要有括号。空格这一条尤其关键,后面挂计划任务时,命令行参数被空格截断是高频问题。
目录规划我按下面这张表来:
| 路径 | 用途 | 备注 |
|---|---|---|
| C:\win-acme | 程序本体、settings.json、日志 | 不要放系统目录 |
| C:\win-acme\renew.log | 自己写的续期日志 | 由批处理追加 |
| C:\nginx\html | nginx 站点根目录,同时作为 webroot | 需要有写入权限 |
| C:\nginx\ssl | 证书 pem 保存目录 | nginx 进程要能读 |
webroot 和 ssl 这两个目录必须提前建好,不要指望程序帮你创建。ssl 目录如果不存在,签发过程会在保存文件那一步失败,而前面的验证阶段明明已经成功了,这会让人误以为是验证出了问题。
2.4 nginx 侧要提前处理好的两件事:80 端口和验证路径
win-acme 的 filesystem 验证原理很朴素:它在 webroot 下写一个随机文件名,然后让证书颁发机构从公网访问http://你的域名/.well-known/acme-challenge/文件名,能读到内容就算通过。所以 nginx 这边必须满足两个条件:80 端口能从外网访问,并且/.well-known/acme-challenge/这个路径不会被任何规则拦截或重定向。
很多人的 nginx 配置里有一条“所有 http 一律 301 到 https”,这条规则会把验证请求也一起重定向走,验证必然失败。正确做法是给验证目录单独开一个优先级更高的 location 放行:
server { listen 80; server_name example.com; root C:/nginx/html; # 验证目录走 ^~ 前缀匹配,优先级高于后面的正则 location ^~ /.well-known/acme-challenge/ { default_type "text/plain"; root C:/nginx/html; } location / { return 301 https://$host$request_uri; } }^~的作用是命中这个前缀后不再匹配后面的正则 location,这样即使下面有location ~ /之类的规则,也拦不住验证请求。root必须和后面传给 win-acme 的--webroot完全一致,差一层目录就会 404。Windows 上 nginx 的路径一律写正斜杠,反斜杠要转义,容易出错,不如从一开始就统一。改完配置别忘了nginx -s reload,并且真的用浏览器访问一次http://你的域名/.well-known/acme-challenge/test.txt(先手动放一个测试文件进去),能返回内容再往下走,这一步能省掉后面大量猜谜。
3. 签发第一张证书:一条 wacs 命令把验证和保存到磁盘串起来
3.1 首次运行先生成 settings.json,改三处就够
第一次直接双击wacs.exe会进交互式菜单,不用急着在里面走完整个流程,让它把settings.json和日志文件生成出来就行。生成出来的文件里字段不少,实际需要动的只有三处:
{ "Client": { "ConfigurationPath": ".", "LogPath": ".", "CachePath": "." }, "ScheduledTask": { "Name": "win-acme renew", "StartBoundary": "03:20:00", "RandomDelay": "00:30:00" }, "Security": { "RsaKeyBits": 2048 } }Client下面这三个路径控制配置、日志、缓存的存放位置,填.表示跟着程序目录走,这样整个 win-acme 目录可以打包搬走,换机器时直接拷过去就能用。ScheduledTask里的StartBoundary是计划任务每天触发的时间,默认值往往在白天,我一般改到凌晨三点多,再留半小时随机延迟,避免多台机器同时打同一个接口。RandomDelay不要设成 0,同一批机器整齐划一地续期没有好处。RsaKeyBits保持 2048 就行,改成 4096 会让每次握手多花一点 CPU,nginx 这边感知不明显,收益也不大。
改完保存,先不要急着签发正式证书。
3.2 命令行签发:先用测试环境跑通,再换正式环境
win-acme 的交互式向导适合第一次熟悉流程,但真正部署时我建议直接用命令行,因为参数可以写进脚本、可以版本化、可以复制到第二台机器。下面这条是我实际用的,带--test表示走测试环境,签出来的证书浏览器不信任,但流程完全一样:
cd C:\win-acme .\wacs.exe ` --target manual ` --host example.com ` --validation filesystem ` --webroot C:\nginx\html ` --store pemfiles ` --pemfilespath C:\nginx\ssl ` --friendlyname "example.com" ` --installation none ` --accepttos ` --emailaddress ops@example.com ` --test这里使用的是 PowerShell 的续行符,也就是反引号;如果写在 bat 里要换成^,或者干脆写成一整行。--target manual表示手动指定目标,不走 IIS 发现;--host可以写多个,用空格分隔或者重复写这个参数;--validation filesystem配合--webroot就是前面说的文件验证;--store pemfiles加--pemfilespath决定证书落在哪个目录;--friendlyname决定生成文件的文件名前缀,不给的话会拿第一个域名当名字,多域名场景下容易分不清;--installation none很关键,它告诉程序“签完就结束,不要往任何服务里安装”,因为我们自己会改 nginx 配置;--accepttos是同意服务条款,非交互运行必须有;--emailaddress用于到期提醒和找回。
用测试环境跑通一次的意义在于:验证阶段的 404、403、端口不通这些问题,在测试环境里暴露出来不需要付出代价。正式环境对同一组域名每周的重复签发是有限额的,反复失败会把额度耗光,然后只能等待。这个限制我遇到过,之后再没省过--test这一步。
确认测试环境能出证书之后,把--test去掉再跑一遍,其余参数原样不动。
3.3 参数逐个对照:哪些必须改,哪些不要动
| 参数 | 示例值 | 作用 | 什么时候需要改 |
|---|---|---|---|
| --target | manual | 指定证书目标来源 | 用 IIS 站点自动发现时改成 iis,nginx 场景保持 manual |
| --host | example.com | 要签的域名 | 必改,多个域名重复写 |
| --validation | filesystem | 验证方式 | 80 端口不通、只有 DNS 权限时换成 dns 相关插件 |
| --webroot | C:\nginx\html | 验证文件写入目录 | 必须和 nginx 的 root 一致 |
| --store | pemfiles | 证书存储格式 | 要 pfx 给 IIS 用就换,nginx 保持 pemfiles |
| --pemfilespath | C:\nginx\ssl | pem 输出目录 | 按实际规划改 |
| --friendlyname | example.com | 输出文件名前缀 | 建议显式指定,多域名时尤其重要 |
| --installation | none | 签完是否安装到服务 | nginx 场景必须是 none |
| --test | 无值 | 使用测试环境 | 调试阶段加,正式签发去掉 |
这张表里最容易出错的是--webroot和--installation两项。前者错一层目录就是 404,后者设错会触发 IIS 相关的安装步骤,在没有 IIS 的机器上直接报错退出。
3.4 生成的文件怎么接进 nginx 配置
签发成功后,C:\nginx\ssl下会出现几个文件,nginx 只需要其中两个:
| 文件 | 内容 | 用途 |
|---|---|---|
| example.com-chain.pem | 站点证书 + 中间证书 | ssl_certificate |
| example.com-key.pem | 私钥 | ssl_certificate_key |
| example.com-crt.pem | 只有站点证书,不含链 | 某些老客户端需要 |
-chain.pem是带完整链的,nginx 的ssl_certificate应该指向它,不要指向-crt.pem,否则部分客户端会报证书链不完整。私钥文件权限要收一收,至少保证只有运行 nginx 的账户和 Administrators 能读。
对接的 server 块大概长这样:
server { listen 443 ssl; server_name example.com; # 全链证书,不要用 -crt.pem ssl_certificate C:/nginx/ssl/example.com-chain.pem; ssl_certificate_key C:/nginx/ssl/example.com-key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; root C:/nginx/html; index index.html; }改完nginx -t测一下配置语法,再 reload。nginx -t在 Windows 上同样要cd /d C:\nginx之后执行,否则它可能读到另一份配置。
4. 续期不只是换个文件:计划任务、钩子和 nginx reload
4.1 默认计划任务长什么样,为什么我不用它
win-acme 在签发成功之后会自动注册一个 Windows 计划任务,名字通常带win-acme renew前缀,每天固定时间跑一次wacs.exe --renew。查看命令:
Get-ScheduledTask | Where-Object { $_.TaskName -like "win-acme*" } | Format-List TaskName, State这个任务本身没问题,续期逻辑也可靠,问题在于它只管续期,不管 nginx。证书文件被覆盖了,nginx 进程仍然持有旧证书的内存副本,直到下一次 reload 或者重启。表现就是:文件修改时间更新了,浏览器里看到的还是旧证书,很多人会在这个环节怀疑是不是续期没生效,然后去看文件、看日志、折腾半天。
我的做法是不动 win-acme 的续期逻辑,但把它包进一个自己的批处理里,续期完立刻 reload nginx,并且记录退出码。
4.2 自己写一个 renew-and-reload 批处理
@echo off setlocal set WACS=C:\win-acme\wacs.exe set NGINX_DIR=C:\nginx set LOG=C:\win-acme\renew.log "%WACS%" --renew if errorlevel 1 ( echo [%date% %time%] renew failed code=%errorlevel% >> "%LOG%" exit /b 1 ) rem nginx 必须在自己的前缀目录下执行,否则找不到 logs\nginx.pid cd /d "%NGINX_DIR%" nginx.exe -s reload if errorlevel 1 ( echo [%date% %time%] reload failed code=%errorlevel% >> "%LOG%" exit /b 2 ) echo [%date% %time%] renew and reload ok >> "%LOG%" exit /b 0几个细节值得说。--renew只处理即将到期或者已经到期的证书,不会每次都重新签发,所以每天跑一次是安全的。if errorlevel 1的判断要在命令执行的下一行立刻做,中间插了别的命令,errorlevel就会被覆盖。cd /d "%NGINX_DIR%"那一行是血泪经验,nginx 的-s参数依赖它能找到logs\nginx.pid,不在正确的工作目录下执行,会报CreateFile() "...nginx.pid" failed,看起来像是 nginx 崩了,实际上只是路径不对。退出码区分成 1 和 2,是为了在计划任务的“上次运行结果”里一眼看出是续期失败还是 reload 失败。
然后把计划任务指到这个 bat 上:
$action = New-ScheduledTaskAction -Execute "C:\win-acme\renew-and-reload.bat" $trigger = New-ScheduledTaskTrigger -Daily -At 3:20am Register-ScheduledTask -TaskName "win-acme renew + nginx reload" ` -Action $action -Trigger $trigger -User "SYSTEM" -RunLevel Highest用 SYSTEM 账户跑的好处是没有密码过期问题,坏处是它对某些用户目录没有访问权限,所以C:\win-acme和C:\nginx\ssl这两个目录不要放在用户目录下。注册完手动Start-ScheduledTask触发一次,看renew.log里有没有多出一行 ok,这一步比等第二天凌晨验证快得多。
4.3 日志与退出码:出问题先看这三个地方
win-acme 自己的日志在程序目录下的win-acme.log,按天滚动,记录每次运行的详细过程,包括验证请求的 URL、响应内容、证书保存路径。这是第一手资料,比任何猜测都准。
第二处是 Windows 事件查看器里的“应用程序”日志,程序运行异常退出时会有记录,尤其是 .NET 层面的报错。
第三处是 nginx 自己的logs/error.log,reload 失败、证书读取失败都会记在这里。三处对照着看,基本能定位到是哪一环出了问题。加--verbose参数可以让 win-acme 输出更多细节,但不要长期开着,日志会膨胀得很快。
5. 排查与避坑:五类常见问题的现象、原因和处理顺序
5.1 现象:验证阶段返回 404,日志里写的是 Authorization failed
原因几乎总是 webroot 和 nginx 的 root 没对上。程序把验证文件写进了C:\nginx\html,但 nginx 里那个 server 块的 root 指向的是C:\nginx\html\site,或者反过来。还有一种变体是 nginx 配置里用了 alias,路径映射关系更绕,验证请求落到别的目录去了。
解决方式是先手动验证:在 webroot 下建一个test.txt,然后从外网访问http://域名/.well-known/acme-challenge/test.txt。能读到就走下一步,读不到就先修 nginx 配置,别急着重跑 wacs,重跑了也还是 404。
5.2 现象:验证返回 403 或者 Invalid response,但文件确实存在
原因是 http 到 https 的强制跳转把验证请求带走了。证书颁发机构只走 80 端口,一跳转就失败。前面 2.4 里那个location ^~ /.well-known/acme-challenge/就是为了解决这个问题。另一个少见但存在的原因是 nginx 上装了某些访问控制规则,对.well-known路径也做了限制,或者开启了防盗链,请求没有带 Referer 被拒绝。
排查时可以直接看 nginx 的access.log,看验证请求到底有没有到达、返回码是多少,这比看 wacs 的日志更直接。
5.3 现象:程序卡在自检阶段,或者在验证前就报连接失败
这是 80 端口被占用的典型表现。Windows 上占用 80 端口的东西比想象的多:IIS 的 Default Web Site、SQL Server Reporting Services、某些桌面客户端的本地服务,甚至系统自带的某些组件。定位命令:
# 找出占用 80 端口的进程号 netstat -ano | findstr ":80" # 用进程号反查是哪个程序 Get-Process -Id <上面查到的PID> | Select-Object Id, ProcessName, Path查到之后判断是停掉它,还是让 nginx 换端口。如果必须保留那个服务,可以让 nginx 监听别的端口,前面再加一层转发,但那样验证路径就要额外处理,我不太推荐,能停就停。
5.4 现象:续期成功,文件时间也更新了,浏览器还是旧证书
原因是没有 reload,或者 reload 了但不是同一个 nginx 实例。前者用 4.2 的批处理解决。后者更隐蔽:机器上装了两份 nginx,一个在服务里跑,一个在命令行里跑,nginx -s reload发给了后者,前者岿然不动。确认方法是在logs/nginx.pid里看进程号,和任务管理器里实际监听 443 的进程号对一下。
还有一种情况是浏览器和中间层缓存。用openssl s_client直接连服务器看证书的 NotAfter,能排除掉客户端缓存干扰。
5.5 现象:手动运行批处理一切正常,计划任务里返回 0x1
九成是路径问题。计划任务的工作目录默认是C:\Windows\System32,批处理里所有相对路径都失效,包括wacs.exe的路径和 webroot 的路径。解决办法是批处理里全部用绝对路径,并且开头加cd /d切到程序目录。如果路径里有空格,bat 里引用时要加引号,set赋值时也要加。
剩下的一成是权限:SYSTEM 账户对某些目录没有写权限,或者 ssl 目录的 ACL 里把 SYSTEM 排除了。用icacls看一眼目录权限,比反复改代码快。
6. 装完怎么验证跑通:三个动作确认这套环境能长期活着
装完之后我会做的第一件事,是故意制造一次续期,看整条链路是否真的闭合。用--force强制重新签发,不去等 60 天:
cd C:\win-acme .\wacs.exe --renew --force # 紧接着看证书文件的修改时间和 nginx 错误日志的最后几行 Get-Item C:\nginx\ssl\example.com-chain.pem | Select-Object LastWriteTime Get-Content C:\nginx\logs\error.log -Tail 5--force会忽略到期判断直接走一次完整流程,包括验证、签发、写入磁盘。跑完之后证书文件的修改时间应该更新,接着手动触发那个批处理或者等计划任务,确认 nginx 完成 reload,error.log末尾出现reconfiguring相关的记录。
第二件事是把三个关键指标列成一张表,每次上线都过一遍:
| 验证项 | 命令或方法 | 期望结果 |
|---|---|---|
| 证书链完整 | openssl s_client -connect 域名:443 -servername 域名 | 输出里能看到完整链和正确的 NotAfter |
| 到期时间合理 | 浏览器点锁图标看有效期 | 剩余时间接近 90 天 |
| 续期任务在册 | Get-ScheduledTask 查任务状态 | State 为 Ready,上次运行结果 0x0 |
| 验证路径通 | 外网访问 /.well-known/acme-challenge/test.txt | 返回文件内容而不是 301/403 |
| 私钥可读 | 用 nginx -t 校验配置 | syntax is ok, test is successful |
第三件事是做一个“黑匣子”备份:把C:\win-acme整个目录和settings.json打成一个包,放到另一台机器或者离线存储上。这套东西真正坏掉的时候,最省事的恢复方式就是把包解压回去,改一下 webroot 路径,重跑一次签发。我自己吃过没备份的亏,那次配置文件写坏之后,凭记忆重建参数花了两个小时,从那以后我每装一台 Windows 加 nginx 的机器,都会在收尾阶段强制走一遍--force续期加目录打包这两个动作,缺一个都不算装完。希望帮到你。
本文还有配套的精品资源,点击获取