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 上很常见:机器不能重装 Linux,nginx 又确实比 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运行时声明打开能看到它要哪个主版本的 .NET
settings.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 manual,2.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/xxx;DNS-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 占着 80,filesystem 是首选。先确认 nginx 的站点根目录,别凭记忆写。

# 查看 nginx 编译前缀,据此推断默认站点根目录 & "C:\nginx\nginx.exe" -V 2>&1 | 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 ops@example.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 ops@example.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_key
example.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 ops@example.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 1,win-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, NextRunTime

LastTaskResult为0表示成功,非零去Logs目录对时间点找日志。NextRunTime只是任务触发时间,不代表那天一定会续期,程序内部还有到期判断,别把它当成证书到期日。

另一个时序细节:nginx 在 Windows 上没有 systemd,-s reload依赖 pid 文件找到 master 进程。如果 nginx 是用别的账户启动的,SYSTEM 身份跑的计划任务可能没有权限发信号,表现是脚本复制成功、reload 静默失败。这种场景要么让 nginx 也以 SYSTEM 启动,要么在脚本里显式指定 pid 路径,别默认它一定能找到。

5. 避坑与排查:五个在 Windows 加 nginx 组合上翻车的现场

5.1 现象:selfhosting 验证一直超时,日志写「无法绑定 80 端口」

原因很直接,nginx 正占着 80,selfhosting 想再监听一次必然失败。很多人以为是防火墙,去开入站规则,白忙半天。

解决:改用 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 ops@example.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 还在用旧证书,等发现时已经是到期后第三天。希望帮到你。

本文还有配套的精品资源,点击获取