Windows下win-acme配nginx免费SSL自动续期实战
简介win-acme 是一款面向 Windows 平台的开源免费 SSL 证书自动获取与部署工具适合网站管理员、系统集成商及需要为 IIS、nginx 等 Web 服务器快速启用 HTTPS 的用户。此压缩包为 v2.2.9.1701 x64 pluggable 版本共 28 个文件约 35.58MB以 21 个 ps1 脚本和 2 个 bat 批处理为主另含 wacs.exe 主程序、settings_default.json 配置、public_suffix_list.dat 域名后缀列表、Web_Config.xml 示例及 version.txt 等Scripts 目录还提供 Exchange、RDS、SQL、Azure 等导入脚本便于按场景扩展自动化部署。目前已有 404 人学习下载可作为官方下载较慢时的替代来源也适合参考脚本组织方式完成证书申请、安装与续订配置。1. 到期提醒响起来之前win-acme 配 nginx 的免费 SSL 续期链路去年帮朋友收拾他挂在 Windows 上的站点nginx 反代两台内网服务证书是活动领的免费 SSL一年期到期当天浏览器红脸他才想起来。这类场景在 Windows 上很常见机器不能重装 Linuxnginx 又确实比 IIS 好管证书却只能手工去后台点续期。win-acme v2.2.9.1701 x64 pluggable 这个包解决的就是这段链路——在 Windows 上按 ACME 协议申请和续期证书把 PEM 文件落到磁盘再交给 nginx 的 ssl_certificate 加载。pluggable 是发行形态不带 .NET 运行时包体小适合目标机已有运行时的环境。适合的人很明确Windows Server 上跑 nginx 的运维、从 IIS 迁过来还在补证书自动化的人以及给内网测试域名配 HTTPS 又不想为此单开一台 Linux 的开发者。2. 拆开 pluggable 包v2.2.9.1701 的文件构成与运行前提2.1 pluggable 和完整版差在哪先看目录再决定要不要用win-acme 的发布包分两种形态self-contained 自带 .NET 运行时解压双击就能跑代价是包体大pluggable 依赖目标机已安装的运行时主程序本身很小。标题里x64说明这是 64 位构建32 位系统直接排除别在旧机器上折腾。pluggable 这个词容易被误解成「功能残缺」实际不是主程序的申请、续期、安装插件逻辑是一致的差别在运行时依赖和部分可选插件的分发方式。解压后先别急着双击拿资源管理器看一眼目录结构心里有个数文件或目录作用备注wacs.exe主程序入口所有命令行参数都从这里进一批*.dll依赖库缺一个都可能在启动阶段报错*.runtimeconfig.json运行时声明打开能看到它要哪个主版本的 .NETsettings.json默认配置首次运行后生成存邮箱、接受条款等Logs运行日志排查问题的第一现场Cache账户与订单缓存删掉会导致重新注册账户那份*.runtimeconfig.json值得打开看一眼结构大致如下version字段以实际文件为准不同构建目标会不一样{ runtimeOptions: { framework: { name: Microsoft.NETCore.App, version: 6.0.0 } } }这段 JSON 的作用是告诉宿主「我需要哪个框架」不是让你改的。看到Microsoft.NETCore.App就说明目标机得装对应的 .NET 运行时如果写的是Microsoft.WindowsDesktop.App那还得装桌面运行时。很多人跳过这一步直接双击闪退之后去翻事件日志其实答案就写在这个文件里。2.2 跑起来之前的三项自检架构、运行时、写权限我一般会在 PowerShell 里先过三条命令确认环境没硬伤再谈申请证书。顺序别乱架构不对后面全白搭。# 1) 确认操作系统是 64 位x64 包在 32 位系统上无法启动 [Environment]::Is64BitOperatingSystem # 2) 列出已安装的 .NET 运行时确认存在 runtimeconfig 里声明的主版本 dotnet --list-runtimes # 3) 给程序目录授予写权限settings.json / Logs / Cache 都会落在 exe 同级目录 icacls C:\tools\win-acme /grant NT AUTHORITY\SYSTEM:(OI)(CI)M第一条返回True才继续。第二条如果提示dotnet不是内部或外部命令说明运行时没装或者没进 PATH这时候别去改环境变量硬撑老老实实装运行时更快。第三条是给计划任务铺路win-acme 默认以 SYSTEM 身份跑续期任务程序目录如果没有写权限续期时生成的临时文件、日志、缓存全部会失败而交互式运行又恰好正常这种「手跑成功、计划任务失败」的差异十有八九是权限。注意不要把程序解压到C:\Program Files下面。那个目录受 UAC 保护交互式运行时文件会被虚拟化到用户目录计划任务再跑就找不到同一份settings.json行为完全不一致。放在C:\tools\这类普通目录最省事。自检通过之后先跑一次.\wacs.exe --help。这一步不是为了看帮助是为了确认参数名。win-acme 在 2.0/2.1 到 2.2 之间改过参数命名早期写--target manual2.2 里对应的是--source manual照抄网上老教程很容易在第一行就报「未知参数」。以本机--help输出为准比记博客靠谱。2.3 首次运行从交互式向导生成 settings.json第一次运行建议走交互式把常用默认值写进settings.json后面再用纯命令行减少每次敲一长串参数的负担。# 进入程序目录并以交互模式启动 cd C:\tools\win-acme .\wacs.exe向导里会依次问几件事创建证书还是续期、域名来源、验证方式、证书存储方式、安装方式。第一次可以选M走完整选项域名来源选 manual 手工输入验证方式选 filesystem存储选 pemfiles安装方式先选 none目的是先把证书文件拿到手确认整条链路能通再去接 nginx 的自动部署。向导跑完settings.json里会留下接受条款、联系邮箱这类默认值后续命令行就不用反复带--accepttos。这个文件不要在用编辑器打开的同时让程序去写Windows 下文件锁会让写入失败程序报错信息又不一定直白。改配置的习惯是先停掉正在跑的任务改完保存再启动。Logs目录会按日期生成日志文件命名里带时间戳。第一次跑完就进去翻一眼确认订单创建、挑战下发、验证通过、证书下载四个阶段都有记录。这一步看起来多余等真出问题的时候你会感谢自己提前认识了日志长什么样。3. 验证方式怎么选HTTP-01、DNS-01 与 webroot 的落地差别3.1 三条验证路径的边界先选对再谈参数ACME 协议要证明你控制这个域名常见三条路HTTP-01 让 CA 来访问http://域名/.well-known/acme-challenge/xxxDNS-01 让你在 DNS 里加一条 TXT 记录TLS-ALPN-01 走 443 特殊握手Windows 上 nginx 组合用得少。win-acme 把 HTTP-01 又拆成几种实现selfhosting 是它自己临时监听 80 端口filesystem 是把挑战文件写进你指定的 webroot 由已有 web 服务器响应manual 是它把文件内容打出来你自己放。验证方式适用条件能否签通配符常见失败点filesystem已有 nginx/IIS 在跑 80否webroot 路径写错、301 跳转吃掉挑战selfhosting80 端口空闲否与 nginx 抢 80 端口manual无法自动响应人工放文件否忘记放文件、放错目录DNS-01有 DNS 提供商 API 或能改 TXT是API 密钥权限不足、TXT 未生效选型逻辑很直接只签单域名、80 端口对外可达走 filesystem 最省心要签通配符*.example.com只能走 DNS-01这时候 pluggable 包的优势体现出来——DNS 提供商插件按需放进程序目录即可不用为了一个提供商换整个发行包。常见做法是从插件页下载对应提供商的 zip解压到 win-acme 目录下重启程序后在验证方式列表里就能看到。3.2 filesystem 验证把 challenge 目录挂进 nginx既然机器上已经有 nginx 占着 80filesystem 是首选。先确认 nginx 的站点根目录别凭记忆写。# 查看 nginx 编译前缀据此推断默认站点根目录 C:\nginx\nginx.exe -V 21 | Select-String prefix输出里会带--prefix之类的信息结合conf/nginx.conf里的root指令确定一个真实存在的目录比如C:/nginx/html。然后在这个 server 块里显式放行挑战路径并且保证它不会被强制跳转 HTTPS 吃掉server { listen 80; server_name example.com; # ACME 挑战必须走明文 80且不能被 301 到 https location ^~ /.well-known/acme-challenge/ { root C:/nginx/html; default_type text/plain; try_files $uri 404; } location / { return 301 https://$host$request_uri; } }^~的作用是命中前缀后不再匹配正则 location避免被后面的规则抢走default_type text/plain保证 CA 拿到的是纯文本不会因为 MIME 类型被拒root后面用正斜杠nginx 在 Windows 上对反斜杠的解析不稳。放行之后先手工建一个测试文件用浏览器访问http://example.com/.well-known/acme-challenge/test.txt能读到内容再往下走这一步能提前排掉一半的 404。配置改完先nginx -t再nginx -s reload确认没语法错误。然后回到 win-acme 跑申请# filesystem 验证 pemfiles 存储证书输出到独立目录便于 nginx 引用 .\wacs.exe --source manual --host example.com --validation filesystem --webroot C:\nginx\html --store pemfiles --pemfilespath C:\certs\example.com --accepttos --emailaddress opsexample.com逐项说清楚--source manual表示域名手工输入不依赖 IIS 站点探测--validation filesystem选定写文件验证--webroot指向 nginx 的站点根win-acme 会在其下自动创建.well-known/acme-challenge/--store pemfiles指定输出 PEM 格式而不是 PFX这是 nginx 需要的--pemfilespath是输出目录建议和 nginx 配置目录分开避免误删--accepttos表示接受 CA 的服务条款只在首次需要--emailaddress用于到期提醒和账户找回。跑完去C:\certs\example.com看文件应该有四份下一章会逐个对应到 nginx 指令。3.3 selfhosting 与 manual两种兜底姿势selfhosting 让 win-acme 自己临时监听 80前提是 80 此刻空闲。机器上跑着 nginx 就一定冲突报错通常是「地址已被占用」。真要用的场景是 nginx 临时停掉做迁移或者证书申请放在独立的维护窗口。命令形态# 先停 nginx 释放 80验证完再启动 C:\nginx\nginx.exe -s quit .\wacs.exe --source manual --host example.com --validation selfhosting --store pemfiles --pemfilespath C:\certs\example.com --accepttos --emailaddress opsexample.com C:\nginx\nginx.exe-s quit是优雅退出等现有连接处理完直接taskkill会留下端口 TIME_WAIT短时间内 selfhosting 仍然绑不上。manual 更原始win-acme 会把挑战文件的文件名和内容打印在控制台你手工放到 webroot 下按回车让它继续验证。适合前面挂着 CDN、回源路径不可控的情况。缺点是必须有人守着续期时无法全自动只能当应急手段。我一般把它留作「第一次摸不清环境时先跑通」的过渡方案跑通之后立刻换回 filesystem。4. 把 PEM 交给 nginx存储格式、脚本钩子与 reload 时序4.1 pemfiles 产出的四个文件分别对应哪条指令申请成功后输出目录里会看到四份文件名字都以域名开头文件内容nginx 里用在哪example.com-crt.pem域名证书叶子单独引用较少example.com-key.pem私钥ssl_certificate_keyexample.com-chain.pem中间证书链一般不用单独配example.com-fullchain.pem叶子 中间链ssl_certificate关键点是ssl_certificate要指向 fullchain不能只指 crt。只配叶子证书桌面浏览器可能因为能自动补链而正常手机端和部分命令行客户端会直接报「证书链不完整」。这是血泪经验排查时容易被「我这台机器好好的」误导。对应配置server { listen 443 ssl; server_name example.com; ssl_certificate C:/certs/example.com/example.com-fullchain.pem; ssl_certificate_key C:/certs/example.com/example.com-key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; }路径同样用正斜杠。私钥文件建议收紧 ACL只给运行 nginx 的账户和管理员读权限icacls一条命令的事别省。4.2 installation script让续期之后自动搬运并校验证书能签下来只是前半程真正的自动化在「续期之后 nginx 怎么知道」。win-acme 的 installation 插件支持 script 类型证书落盘后调用你指定的脚本把路径作为参数传进去。# 在申请命令后追加 installation script把三个路径传给部署脚本 .\wacs.exe --source manual --host example.com --validation filesystem --webroot C:\nginx\html --store pemfiles --pemfilespath C:\certs\example.com --installation script --script C:\tools\win-acme\deploy-nginx.ps1 --scriptparameters -FullChainPath {FullChainPath} -KeyPath {KeyPath} -CertPath {CertPath} --accepttos --emailaddress opsexample.com--installation script声明安装方式--script是脚本绝对路径--scriptparameters里的花括号占位符由 win-acme 在运行时替换成实际文件路径常见的有{CertPath}、{KeyPath}、{ChainPath}、{FullChainPath}、{StorePath}具体以本机--help列出的为准。脚本本身要做三件事复制、校验、reload。顺序不能反。param( [Parameter(Mandatory $true)][string]$FullChainPath, [Parameter(Mandatory $true)][string]$KeyPath, [Parameter(Mandatory $true)][string]$CertPath ) $target C:\nginx\conf\certs New-Item -ItemType Directory -Path $target -Force | Out-Null # 先写临时文件再替换避免 nginx 读到半个文件 Copy-Item $FullChainPath (Join-Path $target fullchain.pem.tmp) -Force Copy-Item $KeyPath (Join-Path $target privkey.pem.tmp) -Force Move-Item (Join-Path $target fullchain.pem.tmp) (Join-Path $target fullchain.pem) -Force Move-Item (Join-Path $target privkey.pem.tmp) (Join-Path $target privkey.pem) -Force # 先校验配置失败就退出保留 nginx 正在使用的旧证书 C:\nginx\nginx.exe -t if ($LASTEXITCODE -ne 0) { Write-Error nginx -t 失败已中止部署 exit 1 } C:\nginx\nginx.exe -s reload if ($LASTEXITCODE -ne 0) { exit 1 }param块声明三个必填参数和命令行占位符一一对应。先写.tmp再Move-Item覆盖是为了防止复制过程中 nginx 恰好 reload 读到不完整文件nginx -t返回非零就exit 1win-acme 会把这次安装标记为失败并写日志人能看到-s reload让 master 进程重新加载配置worker 平滑替换不断连接。脚本退出码很关键不要图省事省略否则失败被吞掉下次发现问题又是几个月后。4.3 续期任务的时序为什么改了脚本要重新跑一次win-acme 会在系统里注册一个计划任务默认名类似win-acme renew每天触发一次由程序自己判断证书是否进入续期窗口通常在到期前 30 天内。这意味着 installation script 的配置是存在任务参数里的改了脚本路径或者占位符得让任务重新生成一次否则它还在调旧路径。做法是把带--installation script的完整命令再跑一遍。win-acme 检测到已有续期配置会走更新流程把新参数写回任务。跑完用下面这条确认任务存在且上次执行结果正常Get-ScheduledTask -TaskName win-acme renew | Get-ScheduledTaskInfo | Select-Object LastRunTime, LastTaskResult, NextRunTimeLastTaskResult为0表示成功非零去Logs目录对时间点找日志。NextRunTime只是任务触发时间不代表那天一定会续期程序内部还有到期判断别把它当成证书到期日。另一个时序细节nginx 在 Windows 上没有 systemd-s reload依赖 pid 文件找到 master 进程。如果 nginx 是用别的账户启动的SYSTEM 身份跑的计划任务可能没有权限发信号表现是脚本复制成功、reload 静默失败。这种场景要么让 nginx 也以 SYSTEM 启动要么在脚本里显式指定 pid 路径别默认它一定能找到。5. 避坑与排查五个在 Windows 加 nginx 组合上翻车的现场5.1 现象selfhosting 验证一直超时日志写「无法绑定 80 端口」原因很直接nginx 正占着 80selfhosting 想再监听一次必然失败。很多人以为是防火墙去开入站规则白忙半天。解决改用 filesystem 验证让 nginx 自己响应挑战文件确实要用 selfhosting就先nginx -s quit释放端口验证完再启动。判断方法也简单netstat -ano | findstr :80看占用进程 PID对得上 nginx 就别怀疑别的。5.2 现象证书申请成功桌面浏览器正常手机打开提示证书链不完整原因是 nginx 的ssl_certificate指向了-crt.pem而不是-fullchain.pem。桌面浏览器有 AIA 补链机制能自己去下载中间证书手机端和部分客户端不会。解决把ssl_certificate改成 fullchain 路径nginx -t后 reload。验证方式是openssl s_client -connect example.com:443 -servername example.com看输出里证书链是否包含中间证书只有一张叶子就是没配对。5.3 现象续期任务日志显示成功nginx 还在用旧证书原因通常是 installation script 没被触发或者脚本里 reload 失败但退出码是 0。改了脚本参数没重新生成计划任务也会导致任务仍调用旧脚本。解决先看Logs里续期那次的安装阶段记录确认脚本被调用再手工执行一次脚本看nginx -t和-s reload的输出最后用Get-ScheduledTask确认任务参数已是新版。脚本里所有外部命令都要检查$LASTEXITCODE这是唯一可靠的失败信号。5.4 现象filesystem 验证返回 404日志里挑战文件路径和预期对不上原因是--webroot指到了 nginx 的配置目录而不是站点根目录或者 nginx 里挑战 location 的root和它不一致。还有一种隐蔽情况location /的 301 跳转规则写在挑战 location 前面正则匹配把请求劫走了。解决先用浏览器或curl -I http://example.com/.well-known/acme-challenge/随便一个文件名确认请求落到哪个目录再看 nginx 访问日志里的实际路径。挑战 location 用^~前缀匹配放在 301 规则之前两处root指向同一个物理目录。5.5 现象pluggable 包双击闪退事件日志提示缺少框架原因是目标机没装对应主版本的 .NET 运行时pluggable 包本身不带。有人从完整版机器上拷贝 dll 过来凑版本对不上照样崩。解决打开*.runtimeconfig.json看声明的主版本去装对应运行时或者换 self-contained 发行包体积大但省事。判断是不是这个原因用 PowerShell 直接在控制台跑.\wacs.exe --help闪退时控制台通常会打印一行缺失框架的提示比双击看事件日志快得多。6. 上线前的 dry-run用 staging 与日志把续期演练一遍正式证书有频率限制同一域名一周内重复签发次数有限拿正式环境试错很容易把自己锁在门外。我的习惯是先用--test走 staging 环境把整条链路跑通再切正式。# --test 走 staging签出的证书不被浏览器信任仅用于链路演练 .\wacs.exe --source manual --host example.com --validation filesystem --webroot C:\nginx\html --store pemfiles --pemfilespath C:\certs\staging --installation script --script C:\tools\win-acme\deploy-nginx.ps1 --scriptparameters -FullChainPath {FullChainPath} -KeyPath {KeyPath} -CertPath {CertPath} --test --accepttos --emailaddress opsexample.com--test是全局开关加在任何命令后面都会切到 staging 端点--pemfilespath换成独立目录避免演练产物和正式证书混在一起被 nginx 误加载。演练要看的是四件事订单能不能创建、挑战文件能不能被访问、脚本有没有被调用、nginx -t和 reload 是否返回 0。四件事都过再删掉--test重跑一次正式申请。staging 证书装到 nginx 上浏览器会报警这是正常的别当成配置错误去改。日志是排查的黑匣子Logs目录下按日期分文件按阶段记录。看的时候抓几个关键词就够日志关键词含义该看什么Authorize挑战下发与验证挑战路径、返回状态码Order订单状态是否 pending 卡住Download证书下载是否拿到完整链Installation安装插件执行脚本退出码、参数替换结果续期任务不用天天盯但每月抽一分钟看一眼上次执行结果和证书剩余天数比到期当天救火便宜得多# 查看计划任务上次执行结果与下次触发时间 Get-ScheduledTask -TaskName win-acme renew | Get-ScheduledTaskInfo | Select-Object LastRunTime, LastTaskResult, NextRunTime # 直接读证书到期时间比看任务状态更接近事实 (Get-PfxCertificate -FilePath C:\nginx\conf\certs\fullchain.pem).NotAfter第二条对 PEM 文件不一定适用更稳的方式是openssl x509 -in fullchain.pem -noout -enddate装个轻量 openssl 就行。从那以后我每次给新机器配这套东西都强制先走一遍--test加手工 reload再切正式绝不省略。吃过一次亏正式证书签下来直接覆盖脚本里 reload 那行写错路径nginx 还在用旧证书等发现时已经是到期后第三天。希望帮到你。本文还有配套的精品资源点击获取