资讯动态

win-acme实战:Windows证书自动签发、续期与部署

发布时间:2026/10/27 2:06:50 来源:尧图企业网站定制
简介win-acme.v2.1.18-x64 是一套面向 Windows 网站管理员、个人站长与运维学习者的免费 SSL 证书申请与管理工具主要用于自动化获取、安装和续订 Lets 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-acmewacs.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 里 siteid1 的站点签发证书并直接绑定回该站点的 https .\wacs.exe --target iis --siteid 1 --host www.example.com --host example.com --validation selfhosting --installation iis --accepttos --emailaddress opsexample.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 opsexample.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 opsexample.comDNS-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 需要 reloadTomcat 需要重启或热部署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 几个真正影响行为的参数参数/配置项常用取值影响续期提前天数3055 天提前太多会造成无意义的重复签发太少则失败重试的窗口不够密钥类型RSA 2048 / RSA 4096 / ECDSAECDSA 握手更快老客户端兼容性略差RSA 2048 是最稳的默认存储方式pemfiles/pfxfile/certificatestore决定下游是读文件还是读证书存储选错等于白签安装方式iis/none/scriptnone时你必须自己接管部署别指望它自动生效证书密码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为准会走非生产目录签发签出来的证书浏览器不认但订单、验证、部署、钩子这整条链路会被完整走一遍。我现在的规矩是任何一次改动续期配置换验证方式、换部署脚本、换密钥类型都强制先用测试目录跑通一遍再切正式。这条规矩是三年前一次「改了部署脚本忘了测通配符路径结果三个月后才发现自动续期一直失败」换来的从那以后我每次动续期配置都强制走一遍测试目录再点正式的那下。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑