简介:win-acme.v2.1.18-x64 是一套面向 Windows 网站管理员、个人站长与运维学习者的免费 SSL 证书申请与管理工具,主要用于自动化获取、安装和续订 Let's Encrypt 证书,解决 HTTPS 部署中证书申请繁琐、到期易遗忘等问题。压缩包共 27 个文件,约 29.58MB,包含可执行程序、核心运行库、PowerShell 脚本、配置与说明文件等;其中 wacs-x64.exe 为入口程序,ps1 脚本便于批处理和自动化,json、xml 与 txt 文件则承载配置、默认参数及使用说明。已有 334 人学习下载,说明其在中小站点 HTTPS 配置场景中具有一定参考价值。借助包内完整组件,读者可了解 ACME 协议在 Windows 环境下的落地方式,掌握证书申请、IIS 安装、自动续订、多域名与通配符证书管理及日志排查思路,适合希望低成本提升站点安全性的用户按需查阅和实践。
1. 证书不等于域名:win-acme 真正替你省掉的是哪一步
搜「免费域名」找到这个包的人不少,解压打开一看发现它不给域名——这是个挺贵的误解。域名得自己注册、一年一续,谁也免不了;win-acme 解决的是域名到手之后那件更烦人的事:给它配一张浏览器认的证书,并且在到期前自动换掉。v2.1.18 x64 是 Windows 平台上的 ACME 客户端,解压即用,不依赖运行时,也不是只能服务 IIS。它适合三类人:手上一两台 Windows 服务器跑 IIS 或自建 Web 服务的运维、把内网服务对公网开放的小团队、以及被「证书还有三天过期」的告警半夜叫起来过的人。后面按「包里有什么 → 怎么签第一张 → 怎么让它自己续 → 卡住了看哪里」走一遍。
2. 拆包与原理:ACME 在 Windows 上到底做了几件事
先把这套东西的边界讲清楚:它是一个客户端,不是一个 CA。签发动作发生在远端的 ACME 目录服务上,本地这侧负责的是生成密钥对、发起订单、完成域名验证、把签发结果落成文件或写进证书存储、再安排下一次续期。理解这条链路,后面所有报错你都能对号入座——是验证没过、是签发被拒、还是签发成功但没部署出去。
2.1 解压后两处目录,别混着看
x64 包解压出来通常是wacs.exe加同目录的几个依赖文件,属于「依赖跟程序放一起」的封装方式,不需要安装。但程序真正的状态不在这里,第一次执行后它会在系统盘的数据目录下另建一个工作目录,两处职责完全不同:
| 位置 | 内容 | 能不能删 |
|---|---|---|
解压目录(如C:\tools\win-acme) | wacs.exe、依赖 dll、可选的settings.json | 升级时整个替换,别在里面写自己的东西 |
数据目录(C:\ProgramData\win-acme\<ACME 目录>\) | 账户密钥、Renewals\*.renewal.json、Log\ | 删了等于清空所有续期记录,慎动 |
常见翻车是:为了「重装干净」把解压目录干掉重下,结果新版本读到的还是旧的续期配置;或者反过来,把Renewals目录当缓存清掉,续期任务还在,但任务里引用的续期 ID 已经不存在,跑起来一片红。
settings.json是全局默认值,可以自己写一份放在解压目录覆盖默认;而每个证书的独立配置在数据目录的Renewals\<id>.renewal.json里,记着这个证书用哪个验证插件、哪个安装插件、参数是什么。排错时先看这个文件,比你翻日志快得多。
2.2 域名验证的三条路,选错了后面全是坑
ACME 要求证明域名归你控制,常规做法是三种挑战:
- HTTP-01:客户端在站点根目录下写一个随机文件,CA 从公网
http://域名/.well-known/acme-challenge/xxx拉取比对。只能用 80 端口,不支持通配符。 - DNS-01:客户端在 DNS 里建一条
_acme-challenge的 TXT 记录,CA 查询比对。支持通配符,不要求 80 端口可达,代价是要有 DNS 服务商的 API 或手动改记录。 - TLS-ALPN-01:在 443 上做握手层验证,Windows 侧用到的场景不多,一般不用考虑。
选型逻辑很简单:站点在公网、80 端口能通,用 HTTP-01 最省事;要签*.example.com,或者服务器藏在只出不进的网络里、80 被封,只能走 DNS-01。很多人第一次签通配符失败,是把--host *.example.com写成了--host example.com再加个通配符参数——通配符就是个主机名的一部分,直接写在--host里。
对应到工具里,HTTP-01 的三种实现分别是:selfhosting(客户端自己临时起一个监听占住 80 端口)、filesystem(把验证文件写进你指定的站点目录,交给现有 Web 服务器去响应)、ftp(把文件传到远端站点目录)。IIS 站点用selfhosting最稳,因为它会自己处理端口冲突;但如果 80 被 IIS 默认站点占着,就容易出问题,后面避坑那章细说。
2.3 为什么不继续手工导 PFX
手工路径大家都熟:某处生成 CSR、提交、验证、下载、导入、绑定、设个日历提醒。单站点、一年一次,忍了。一旦站点数量到五六个、分散在两三台机器上,这套流程的可靠性就开始看人——值班的人换了一茬,日历提醒没人接,证书就过期了。
自动化之后,这套流程变成「跑一次命令 + 一个计划任务」,密钥和续期记录都在本地,每次续期是重新签发而不是复用旧证书,所以不存在「续了但没生效」之外的新风险。而「续了但没生效」恰恰是这套方案最容易出问题的地方,也是我后面要花一整章讲的部分。
3. 第一次签发:三条路径的完整命令
向导能覆盖 90% 的场景,但向导生成的东西你未必看得懂。我的习惯是先跑一遍向导,把每一步选的参数记下来,然后改成命令行,因为只有命令行才能进脚本、进 CI、进别人接手时能读的文档。
3.1 先跑一次向导,把参数看全
# 进入解压目录,直接运行不带参数就是交互式向导 cd C:\tools\win-acme .\wacs.exe向导会依次问你:从哪儿取域名(IIS 站点 / 手工输入 / 从其他来源)、用什么方式验证、存到哪儿(PEM 文件 / PFX 文件 / Windows 证书存储)、要不要顺便装到 IIS 或执行部署脚本。走完一遍之后,去数据目录看新生成的.renewal.json,里面每一个字段都对应你刚才的一次选择——这份文件就是你的参数清单,照着它写命令行基本不会错。
需要提醒的是,向导第一次运行会让你同意服务条款并留一个联系邮箱。邮箱一定要填能收到信的:证书快到期而自动续期反复失败时,部分 CA 会往这个地址发提醒,这是我见过唯一能在半夜救命的通知渠道。
3.2 IIS 站点:一条命令签发并绑定
# 给 IIS 里 siteid=1 的站点签发证书,并直接绑定回该站点的 https .\wacs.exe ` --target iis ` --siteid 1 ` --host "www.example.com" --host "example.com" ` --validation selfhosting ` --installation iis ` --accepttos ` --emailaddress "ops@example.com"逐段解释:--target iis --siteid 1表示域名从 IIS 站点绑定里读,站点 ID 用Get-IISSite | Select-Object Name, Id查;--host可以显式覆盖,把根域和 www 一起签进同一张证书,省得后面还要再签一次;--validation selfhosting让客户端自己占住 80 端口完成 HTTP-01;--installation iis会在签发成功后自动把证书绑定到站点并写好 SNI 设置——这一步是关键,跳过它你拿到证书但站点还是旧证书。
注意--siteid和--host同时给的时候,以--host为准。站点里有几个绑定、每个绑定是什么主机名,最好先看清楚,不然会出现「证书签下来了,但访问另一个域名还是报错」的情况——那张证书里根本没有那个名字。
3.3 非 IIS:写文件验证 + 只导出 PEM
跑 Nginx、Tomcat、或者容器挂载的场景,工具只负责签和落盘,部署交给自己的钩子。
# 手工指定域名,验证文件写到站点根目录,证书以 PEM 形式落到 D:\certs\app .\wacs.exe ` --target manual ` --host "app.example.com" ` --validation filesystem ` --webroot "D:\sites\app" ` --store pemfiles ` --pemfilespath "D:\certs\app" ` --installation none ` --accepttos ` --emailaddress "ops@example.com"--webroot指的是「能被公网以http://app.example.com/访问到的那个物理目录」,验证文件会被写到它下面的.well-known\acme-challenge\里。这里最常见的错误是把 webroot 写成上层目录,验证文件落到了访问不到的层级,CA 拉取 404,报错信息只会说「验证失败」,不会告诉你路径错了。判断方法很土但有效:签发过程中命令行会打印出完整 URL,复制到浏览器里能不能打开,一眼就知道。
--store pemfiles产出的是一组文件,通常包括证书链、私钥、以及去掉链的纯证书,文件名以域名开头。Nginx 需要的是「证书链 + 私钥」两份,私钥那份权限要收紧——Windows 上右键属性 → 安全,只留运行 Web 服务的账户和 Administrators 组。
3.4 通配符:DNS-01 必须配一个删记录的钩子
# 签 *.example.com 和根域,验证走 DNS 脚本插件 .\wacs.exe ` --target manual ` --host "*.example.com" --host "example.com" ` --validation dnsscript ` --dnsscript "C:\tools\hooks\dns-hook.ps1" ` --dnsscriptparameters "{Identifier}" ` --store pemfiles ` --pemfilespath "D:\certs\wildcard" ` --installation none ` --accepttos ` --emailaddress "ops@example.com"DNS-01 的核心是「建 TXT 记录 → 等 CA 查到 → 删 TXT 记录」,客户端能自动做的是调用你给的脚本,脚本要自己实现建和删两个动作。下面是个能改改就用的骨架:
# dns-hook.ps1 —— 由 ACME 客户端在创建/清理 TXT 记录时调用 param( [Parameter(Mandatory=$true)][string]$Identifier, # 正在签的域名,如 *.example.com [Parameter(Mandatory=$true)][string]$RecordName, # 要建的主机记录,如 _acme-challenge [Parameter(Mandatory=$true)][string]$Token, # 记录值,一串随机字符 [string]$Action = "create" # create 建,delete 删 ) $hdr = @{ Authorization = "Bearer $env:DNS_API_TOKEN" } # 令牌走环境变量,别写死在脚本里 $api = "https://dns.example.internal/api/v1/records" $body = @{ name = $RecordName; type = "TXT"; value = $Token; ttl = 60 } | ConvertTo-Json if ($Action -eq "delete") { Invoke-RestMethod -Method Delete -Uri "$api`?name=$RecordName" -Headers $hdr } else { Invoke-RestMethod -Method Post -Uri $api -Body $body -ContentType "application/json" -Headers $hdr } Write-Output "ok"参数说明:$Identifier是域名,用来判断记录建在哪个区域;$RecordName和$Token是这条 TXT 的记录名和值,直接透传给 DNS 服务商 API;$Action决定建还是删。有些场景下客户端会复用同一套脚本参数、靠不同的占位符组合来区分动作,具体占位符列表以向导生成的那个.renewal.json里记录的参数为准,别凭记忆写。
最后一行输出ok不是装饰:客户端会看脚本的退出码和输出,非零退出会被当成验证失败。TTL 也不要贪大,60 到 120 秒足够,设成 3600 会让「记录已改但缓存没刷」成为你排查半天都找不到原因的问题。
4. 续期与部署:让它在没人管的时候也正确
签第一张证书只是热身,这套方案的价值全在续期上。续期由两段组成:一段是「谁在什么时候触发」,一段是「签发成功后怎么让服务用上新证书」。前者靠计划任务,后者靠安装插件或部署脚本,任一段断了,结果都是线上证书悄悄过期。
4.1 计划任务是谁建的,怎么看
默认情况下,安装插件执行完之后客户端会注册一个计划任务,名字里带win-acme和自己用的 ACME 目录标识。查一下就能确认:
# 确认续期任务存在、状态正常,并看它什么时候跑过 Get-ScheduledTask -TaskName "win-acme*" | Select-Object TaskName, State Get-ScheduledTaskInfo -TaskName "win-acme renew *" | Select-Object LastRunTime, LastTaskResult, NextRunTimeLastTaskResult是 0 只说明「任务跑完了」,不代表「证书续上了」——这两件事的差别是这套方案最需要警惕的部分。任务默认以 SYSTEM 身份运行,这意味着脚本里所有依赖当前用户环境的东西都会失效:用户级的环境变量、映射的网络驱动器、需要交互的凭据弹窗。我自己踩过的最隐蔽的一次,是部署脚本里读了一个「用户级环境变量」里的 API 令牌,手工跑一切正常,任务跑就静默失败,日志里只有一句无意义的错误码。
想让续期真正跑起来做验证,可以手工触发一次:
# 立即执行一次续期,把详细日志打到控制台,用来验证钩子和权限 & "C:\tools\win-acme\wacs.exe" --renew --force --verbose--force的作用是忽略「还没到续期时间」的判断强行跑一遍,参数名和可用性以你手上这份--help输出为准,个别版本行为略有差异。--verbose会打出每个插件的调用过程和脚本的原始输出,手工验证阶段务必带上,正式任务里可以去掉。
4.2 部署钩子:从「文件更新了」到「服务真的用了」
签发成功只意味着磁盘上的文件变了。Nginx 需要 reload,Tomcat 需要重启或热部署,IIS 靠--installation iis自动处理,而容器化的服务要么挂载目录后 reload,要么走配置中心推送。这些差异造成了同一个现象:证书文件的时间戳是新的,浏览器里看到的还是旧的。
一个可用的部署脚本大致长这样:
# deploy.ps1 —— 由安装脚本插件在签发成功后调用 param( [Parameter(Mandatory=$true)][string]$CertPath, # 新证书落地路径 [Parameter(Mandatory=$true)][string]$Identifier # 对应域名,用于多站点分流 ) $log = "C:\tools\hooks\deploy.log" $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" try { # 一、先备份旧文件,出问题能回滚(这就是我的后悔药) $bak = "D:\certs\backup\$Identifier-$(Get-Date -f yyyyMMddHHmmss)" New-Item -ItemType Directory -Path $bak -Force | Out-Null Copy-Item "D:\nginx\conf\certs\$Identifier*" $bak -ErrorAction SilentlyContinue # 二、换文件,注意私钥权限收紧 Copy-Item "$CertPath\$Identifier-chain.pem" "D:\nginx\conf\certs\$Identifier-chain.pem" -Force Copy-Item "$CertPath\$Identifier-key.pem" "D:\nginx\conf\certs\$Identifier-key.pem" -Force # 三、校验配置再 reload,顺序反了就是一次线上中断 & "D:\nginx\nginx.exe" -t if ($LASTEXITCODE -ne 0) { throw "nginx 配置校验未通过,已中止 reload" } & "D:\nginx\nginx.exe" -s reload "$stamp OK $Identifier" | Out-File $log -Append -Encoding utf8 } catch { "$stamp FAIL $Identifier $($_.Exception.Message)" | Out-File $log -Append -Encoding utf8 exit 1 # 非零退出会让客户端把这次续期标记为失败,下次还会重试 }三段逻辑值得单独说:备份那一段是我被坑出来的固定动作,新证书本身有问题(比如只签了根域没签 www)时,能在一分钟内换回去;nginx -t必须在 reload 之前,配置校验失败还强行 reload 的结果是整个站点起不来;exit 1是给客户端看的信号,吞掉异常返回 0,客户端会认为部署成功,然后一年后你才发现从来没成功过。
脚本参数里{CertPath}、{Identifier}这类占位符的具体写法,以向导生成.renewal.json里记录的为准;不同版本的占位符集合不完全一样,照抄别人的命令最容易在这上面出问题。
4.3 几个真正影响行为的参数
| 参数/配置项 | 常用取值 | 影响 |
|---|---|---|
| 续期提前天数 | 30~55 天 | 提前太多会造成无意义的重复签发,太少则失败重试的窗口不够 |
| 密钥类型 | RSA 2048 / RSA 4096 / ECDSA | ECDSA 握手更快,老客户端兼容性略差;RSA 2048 是最稳的默认 |
| 存储方式 | pemfiles/pfxfile/certificatestore | 决定下游是读文件还是读证书存储,选错等于白签 |
| 安装方式 | iis/none/script | none时你必须自己接管部署,别指望它自动生效 |
| 证书密码 | PFX 才需要 | 密码写进配置文件等于明文,注意目录权限 |
这些值大部分在settings.json里做全局默认,个别可以在命令行覆盖。改完全局默认之后,已经存在的.renewal.json不会自动跟着变——这是很多人「改了参数但行为没变」的原因,要么去改单个续期文件,要么删掉重建。
5. 避坑与排查:签不下来时按这五处看
这套工具的错误提示不算友好,同一个「验证失败」背后可能是四五种完全不同的原因。下面五条是我这几年的血泪记录,按发生频率排的。
5.1 签发阶段:验证过不去的三种现场
现象一:报验证失败,但浏览器手工访问那个验证 URL 是通的。原因通常是 80 端口被别的进程抢先占了——IIS 默认站点、另一个 Web 服务、或者之前没退干净的客户端进程。selfhosting模式下客户端要自己监听 80,占不到就只能失败,而它未必会把「端口被占」直接说出来。解决:netstat -ano | findstr :80找到占用进程;如果 80 必须留给 IIS,就改用filesystem模式,让验证文件写进站点目录由 IIS 去响应,别跟端口较劲。
现象二:filesystem模式下验证文件确实生成了,但 CA 拉不到。原因是--webroot指的不是公网能访问的那个目录。常见于站点配了 URL 重写、或者应用本身对.well-known路径做了拦截。解决:签发时盯住命令行打印的完整 URL,用浏览器或无痕窗口访问一次;返回 404 就去核对物理路径,返回 301/302 就去检查重写规则里有没有把.well-known放行。
现象三:通配符签下来了,但访问根域还是证书错误。原因很直白:*.example.com不覆盖example.com,通配符只匹配一级子域。解决:把两个名字写进同一张证书,--host "*.example.com" --host "example.com",一次签发两个都覆盖。这个坑每年都会有人重新踩一遍,包括我自己。
5.2 续期阶段:任务成功但证书没换
现象四:计划任务LastTaskResult是 0,日志里也没有明显报错,但服务用的还是旧证书。原因是部署脚本在 SYSTEM 身份下跑不通:读不到用户级环境变量、访问不了映射盘、或者调用的 CLI 不在 SYSTEM 的 PATH 里。这类失败最阴,因为脚本自己 try/catch 吞掉了异常还返回了 0。解决:脚本里所有外部依赖都写绝对路径,凭据走机器级环境变量或凭据管理器,并且在 catch 里exit 1;改完之后手工用 SYSTEM 身份跑一次验证(psexec -s -i powershell之类的手段),别只在当前用户下测。
现象五:日志显示续期成功,reload 也执行了,但浏览器里证书指纹没变。原因是多进程/多实例:前面有反向代理或者负载均衡,你只换了一台;或者进程 reload 时旧 worker 还在处理存量连接。解决:换完证书后用指纹比对确认(下一章给方法),多实例场景把部署脚本做成「逐台执行 + 每台校验」,任何一台失败就整体报错,不要静默跳过。
6. 进阶:把「续期成功」变成可以验证的事实
日志说成功、任务返回 0、文件时间戳是新的,这三件事加起来仍然不等于「线上用的是新证书」。想真正闭环,得从证书本身反查。
最直接的办法是读本地证书存储,按域名筛出未过期的那张,看剩余天数:
# 按域名找本地证书,按到期时间倒序取最新一张 $c = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*app.example.com*" } | Sort-Object NotAfter -Descending | Select-Object -First 1 if (-not $c) { "未找到证书"; exit 1 } $days = ($c.NotAfter - (Get-Date)).Days "{0} 到期 {1:yyyy-MM-dd} 剩余 {2} 天" -f $c.Thumbprint, $c.NotAfter, $days if ($days -lt 21) { exit 1 } # 剩三周还没续上,就该叫人了这段可以直接塞进现有的监控脚本:退出码非零就是告警,比盯日志可靠得多。NotAfter是证书自己的字段,绕不过去,任何「续期成功但没生效」的假象在这里都会露馅。
再补一层外部视角:从公网侧取一次证书链,比对指纹。内网签发、外网没生效这种「一半成功」的情况,只有从外面看才看得见。命令行里openssl s_client -connect app.example.com:443 -servername app.example.com加一段管道就能拿到指纹,把它和本地那张的Thumbprint对一遍,一致才算真的换上了。
还有个小技巧值得单独提:正式续期任务上线前,先用测试目录跑一遍全流程。命令里加测试开关(--test之类,具体以--help为准)会走非生产目录签发,签出来的证书浏览器不认,但订单、验证、部署、钩子这整条链路会被完整走一遍。我现在的规矩是:任何一次改动续期配置(换验证方式、换部署脚本、换密钥类型),都强制先用测试目录跑通一遍,再切正式。这条规矩是三年前一次「改了部署脚本,忘了测通配符路径,结果三个月后才发现自动续期一直失败」换来的,从那以后我每次动续期配置都强制走一遍测试目录,再点正式的那下。希望帮到你。
本文还有配套的精品资源,点击获取