2637 字
13 分钟
ssl证书认证与安装

前言#

前两天突然发现服务器的SSL证书过期了,acme的自动更新也没成功运行,费了不少劲也踩了不少坑,以下简记let’s encrypt + win-acme的认证与更新。

windows server 2012 + nginx==1.28.0 + win-acme==2.2.9.1701

准备工作#

win-acme#

win-acme应该算windows上比较好用的,官网及说明文档见https://www.win-acme.com,下载页见https://github.com/win-acme/win-acme/releases/,版本根据自己需求选择,trimmed是精简版(无法安装插件),一般pluggable就行,其他形如 plugin.validation.dns.*.v.*.*.*.zip的是对应dns的插件(需配合pluggable版本使用),有需要的可以下载对应压缩包,并把压缩包内容解压到win-acme的根目录即可。

以下默认解压后程序根目录为C:/win-acme。

两种认证模式示例#

ssl证书认证的核心即将域名与服务器进行绑定以防止被劫持。以下简介两种不同的认证模式。

文件认证#

文件认证的基本模式为,win-acme在本地创建一个文件,然后让认证服务器从指定域名访问该文件,比对文件内容是否一致。 基于此逻辑,需要完成的有以下几步

配置服务器#

以nginx为例,要让服务器从相应域名能够访问到文件,首先要配置好nginx.conf

snippets/acme-challenge.conf
location ^~ /.well-known/acme-challenge/ { # 请将该block置于最前,以避免被其他规则覆盖
default_type "text/plain";
try_files $uri =404; # 查找请求的文件,找不到返回404
expires -1; # 禁止缓存验证文件(避免验证失败)
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
nginx.conf
server {
listen 80; # 服务器认证默认走80端口
server_name abc.com; # 域名
root C:/certificates/abc.com/; # 临时访问文件的本地位置
include snippets/acme_challenge.conf; # 引入认证模块
location / { # 自动跳转https
return 302 https://abc.com$request_uri;
}
... # 其他配置,如日志、流量等
}

当然,把认证模块抽象出来再通过include进行复用并非必要,如果只有一个域名,直接在server block里写location规则就行

外部访问 http://abc.com/.well-known/acme-challenge/xxx 时会指向 C:\certificates\abc.com\.well-known\acme-challenge\xxx

确认相应权限#

这个坑比较容易踩,主要是让win-acme和nginx拥有相应必要权限。

目录权限
证书存放目录写入权限 + nginx 读取权限
验证目录修改权限 + nginx/everyone读取权限
win-acme程序目录执行权限 + 运行账户写入权限
  • 手动运行时当前登录用户有相应权限即可。
  • 如果要作为计划任务运行,可以考虑用SYSTEM用户或专门的服务账户,并确保该账户拥有相应权限。

运行WACS(Windows ACME Simple)#

  1. TUI <span id="wacs-tui">

    • 选择认证模式 wacs-filesystem-1 [M] Create Certificate (full options)

      手动选择配置

    • 如何输入域名 wacs-filesystem-2 [2] Manual Input

      手动输入模式

    • 输入域名 wacs-filesystem-3 abc.com

      此处替换为你的实际域名

    • 选择证书条目数 wacs-filesystem-4 [4] Single certificate

      单证书(可选按域名/按主机)

    • 如何确认域名所有权 wacs-filesystem-5 [1] [http] Save verification files on (network) path

      将验证文件保存至(网络)路径

    • 输入验证文件存放位置 wacs-filesystem-6 C:\certificates\abc.com

      此处替换为你的实际临时存放文件夹,即 配置服务器中的root路径

    • 是否复制web.config到认证目录下 wacs-filesystem-7 n

      这个一般是IIS配置才需要的模块,非IIS的直接选否即可。因为WACS认证时let’s encrypt服务器会访问 http://abc.com/.well-known/acme-challenge/{token},而IIS可能会阻止未知扩展名的文件访问,或者对静态文件设置MIME类型导致未定义类型请求被过滤,web.config 就是为了确保 .well-known/acme-challenge 下的文件可以被外部访问,且返回正确的MIME type,以避免被IIS拦截请求。

    • 确认加密类型 wacs-filesystem-8 [2] RSA key

      使用默认RSA即可

    • 如何存储证书 wacs-filesystem-9 [2] PEM encoded files (Apache, nginx, etc.)

      前面用的nginx,所以这里也使用nginx支持的PEM类型

    • 存储证书位置 wacs-filesystem-10 D:\certificated\abc.com

      此处替换为你的本地存储位置

    • 是否对PEM文件加密 wacs-filesystem-11 [1] None

    • 其他证书存储位置 wacs-filesystem-12 [5] No (additional) storage steps

      无需其他存储

    • 是否更新服务器配置 wacs-filesystem-13 [3] No (additional) installation steps

      无需其他安装步骤,如果有需要,比如重启nginx服务器可以自己编写脚本,然后选择 [2] Start external script or program

    • 其他细节 wacs-filesystem-14 打开并确认协议条款,并输入邮箱

    • 认证成功

      认证成功后可以确认nginx.conf的配置

      server {
      listen 443 ssl;
      server_name abc.com;
      ssl_certificate D:/certificates/abc.com/abc.com-chain.pem; # 证书路径
      ssl_certificate_key D:/certificates/abc.com/abc.com-key.pem; # 私钥路径
      root ... # 其他
      ...
      }

      然后重启nginx服务

      Terminal window
      nginx -t
      nginx -s reload

      另外wacs一般会把自动更新写进任务计划程序,可以通过 taskschd.msc找到。也可在主界面选择 [R] Run Renewals - [S] Run *all* renewals (force)来强制更新

    • 认证失败

      根据提示信息确认失败原因,修正后重试。

      注意, 未经备案的域名可能导致let’s encrypt在访问时被部分dns服务商拦截,此类报错相对隐蔽,可能需要用 --verbose的详细日志才能发现真正原因

  2. CLI

    命令行参数用法详参https://www.win-acme.com/reference/cli

    以下给出上述步骤的命令行参数写法

    Terminal window
    wacs.exe --source manual --host "abc.com" ^
    --validation filesystem --webroot "C:\certificates\abc.com" ^
    --store pemfiles --pemfilespath "D:\certificated\abc.com" ^
    --verbose >> res.log

    linux请使用反斜杠 \换行(不会有人用linux还来看wacs的流程吧,不会吧,不会吧)

    最后一行 --verbose启用详细日志输出,>> 将结果静默追加输出至当前文件夹的res.log文件,方便检查。如无需追加直接使用 >覆盖输出。

DNS认证#

DNS认证的基本模式为,在DNS记录中添加一条TXT记录,let’s encrypt检查该记录是否存在且正确。 部分常见的DNS服务商(如Cloudflare, Aliyun等)可以通过下载对应的插件来进行认证(见win-acme) 以下简介通用做法,需要dns服务商提供api接口

编写脚本#

wacs需要两个接口,一为创建一条dns记录,二为删除该条dns记录,具体可用参数占位符详见https://www.win-acme.com/reference/plugins/validation/dns/script

参数名示例释义
{Identifier}sub.abc.com待验证的主机名
{RecordName}_acme-challenge.sub.abc.com完整TXT记录名
{ZoneName}abc.com可注册域名
{NodeName}_acme-challenge.sub记录节点名
{Token}DGyRejmCefe7v4NfDGDKfAACME 要求添加到 TXT 记录的内容
{vault://json/mysecret}-Vault中存储的内容(比如 API Key 或密码)

wacs在调用脚本时,根据状态码确认脚本是否成功运行,以下仅示例。

C:/win-acme/CustomScript/dns.bat
C:\Python311\python.exe "C:\win-acme\CustomizeScripts\dns.py" %*

如使用虚拟环境,以anaconda为例

Terminal window
CALL "D:\Anaconda3\condabin\conda.bat" activate myenv
python "C:\win-acme\CustomizeScripts\dns.py" %*
CALL conda deactivate
echo Done

python脚本,具体api响应结构和请求参数,请参考dns服务商提供的文档

C:/win-acme/CustomScript/dns.py
# requirements: requests, dnspython
# python >= 3.6 (typehint, f-string)
# python >= 3.11 (StrEnum)
import requests
import sys
from enum import IntEnum, StrEnum
import time
import logging
import dns.resolver
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s",datefmt="%Y-%m-%d %H:%M:%S")
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(fmt)
logger.addHandler(stream_handler)
API_KEY = "YOUR_API_KEY"
BASE = "https://www.sample.com/api"
TIMEOUT_MIN = 10
class VERSION(IntEnum):
DEFAULT = 1
class TYPE(StrEnum):
XML = "xml"
JSON = "json"
class ACTION(StrEnum):
ADD_DNS_RECORD = "dnsAddRecord"
LIST_DNS_RECORDS = "dnsListRecords"
DELETE_DNS_RECORD = "dnsDeleteRecord"
class RRTYPE(StrEnum):
A = "A"
AAAA = "AAAA"
CNAME = "CNAME"
MX = "MX"
TXT = "TXT"
SRV = "SRV"
CAA = "CAA"
def wait_for_dns_propagation(txt_record_name: str, expected_value: str,timeout: int = 60*10, interval: int = 30):
start = time.time()
while True:
try:
answers = dns.resolver.resolve(txt_record_name, 'TXT')
for rdata in answers:
for txt_string in rdata.strings:
try:
decoded_value = txt_string.decode()
if decoded_value == expected_value:
logger.info(f"TXT记录生效: {txt_record_name} = {expected_value}")
return True
except Exception as e:
logger.error(f"无法解码字符串:{e}")
continue
except Exception as e:
logger.warning(f"查询DNS失败: {e}, 等待{interval}s后重试")
time.sleep(interval)
elapsed = time.time() - start
if elapsed > timeout:
logger.error(f"等待 TXT 记录超时: {txt_record_name}")
return False
def add(domain: str, txt_value: str, rrhost: str):
url = f"{BASE}/{ACTION.ADD_DNS_RECORD}?version={VERSION.DEFAULT}&type={TYPE.JSON}&key={API_KEY}&domain={domain}&rrtype={RRTYPE.TXT}&rrhost={rrhost}&rrvalue={txt_value}&rrttl=3600"
r = requests.get(url)
if r.status_code != 200:
logger.error(f"添加DNS记录失败: {r.text}")
sys.exit(1)
logger.info(r.text)
logger.info(f"等待DNS记录生效...{TIMEOUT_MIN}分钟")
res = wait_for_dns_propagation(rrhost, txt_value, timeout=60*TIMEOUT_MIN, interval=30)
if res:
sys.exit(0)
else:
logger.error("DNS记录超时未生效, 请检查是否成功添加,如有必要可上调最大等待时长")
sys.exit(1)
def delete(domain: str, rec_id: str):
url = f"{BASE}/{ACTION.DELETE_DNS_RECORD}?version={VERSION.DEFAULT}&type={TYPE.JSON}&key={API_KEY}&domain={domain}&rrid={rec_id}"
r = requests.get(url)
logger.info(r.text)
sys.exit(0 if r.status_code == 200 else 1)
def get_txt_record_id(domain: str, txt_value: str):
url = f"{BASE}/{ACTION.LIST_DNS_RECORDS}?version={VERSION.DEFAULT}&type={TYPE.JSON}&key={API_KEY}&domain={domain}"
r = requests.get(url)
logger.info(r.text)
data = r.json()
reply = data.get("reply", None)
if reply:
resource_records = reply.get("resource_record", None)
if resource_records:
for rec in resource_records:
if rec.get("type") == RRTYPE.TXT and rec.get("value") == txt_value:
return rec.get("record_id")
logger.error("未找到一致的TXT记录")
else:
logger.error("未找到记录")
else:
logger.error("请求失败")
return None
def main():
action = sys.argv[1]
domain = sys.argv[2]
txt_value = sys.argv[3]
rrhost = sys.argv[4] if len(sys.argv) > 4 else "_acme-challenge"
if action == "add":
add(domain, txt_value, rrhost)
elif action == "delete":
rec_id = get_txt_record_id(domain, txt_value)
if rec_id:
delete(domain, rec_id)
else:
logger.warning("没有需要删除的记录")
sys.exit(0)
else:
raise ValueError("未知操作")
if __name__ == "__main__":
main()

运行WACS(Windows ACME Simple)#

  1. TUI

    • 前置准备 前四步同前,详见文件认证部分

    • 如何确认域名所有权 wacs-dns01-1 [8] [dns] Create verification records with your own script

      用脚本创建认证记录

    • 脚本路径 wacs-dns01-2 C:/win-acme/CustomScript/dns.bat

      此处更改为你的脚本文件路径

    • 删除认证记录 wacs-dns01-3 [1] Using the same script

      按需求选择

    • 脚本参数(创建记录) wacs-dns01-4 add {ZoneName} {Token} {RecordName}

      此处按照你的脚本设置传递参数

    • 脚本参数(删除记录) wacs-dns01-5 delete {ZoneName} {Token}

      此处按照你的脚本设置传递参数

    • 并发设置 wacs-dns01-6 [1] Run everything one by one

      一般选串行即可,避免dns服务商出现http429等问题

    • 后续设置

      后续同前,详见文件认证部分

  2. CLI

    命令行参数用法详参https://www.win-acme.com/reference/cli

    以下给出上一个步骤的命令行参数写法

    Terminal window
    wacs.exe --source manual --host "abc.com" ^
    --validationmode dns-01 --validation script --dnsscript "C:\win-acme\CustomizeScripts\dns.bat" ^
    --dnscreatescriptarguments "add {ZoneName} {Token} {RecordName}" ^
    --dnsdeletescriptarguments "delete {ZoneName} {Token}" ^
    --store pemfiles --pemfilespath "D:\certificated\abc.com" ^
    --verbose >> res.log
  3. Vault使用

    出于安全性考虑,以及避免脚本硬编码API_KEY,可以使用Vault,如https://github.com/hashicorp/vault,同样注意权限问题。

    • 存储key

    vault kv put secret/winacme_dns api_key="YOUR_REAL_DNS_API_KEY"

    注意:vault中token有过期时间,合理设计有效期或自动更新。

    • 修改脚本接收的参数(sys.argv)

    • 修改wacs使用

      • TUI中修改参数,注意参数顺序与脚本对应 add {ZoneName} {Token} {vault://json/secret/winacme_dns:api_key} {RecordName}

        delete {ZoneName} {Token} {vault://json/secret/winacme_dns:api_key}

      • 或CLI使用中修改相应传参

        Terminal window
        ...
        --dnscreatescriptarguments "add {ZoneName} {Token} {vault://json/secret/winacme_dns:api_key} {RecordName}" ^
        --dnsdeletescriptarguments "delete {ZoneName} {Token} {vault://json/secret/winacme_dns:api_key}" ^
        ...

其他#

限额提醒#

Let’s encrypt的申请有rate limit,如果仅在脚本或设置调试阶段,可以考虑使用 --baseuri https://acme-staging-v02.api.letsencrypt.org/参数,待调试成功后再进入生产环境。

类型限额
同主域名证书申请限额50/周/主域名
同域名集证书申请份数限额5/周/域名集
同账户申请证书份数限额300/3小时/账户
同账户同域名集验证失败次数限额5/小时/域名集/账户

主域名指 sub1.example.comsub2.example.comexample.com

域名集指申请的证书所生效的域名集合,如 {example.com, sub.example.com}

ssl证书认证与安装
https://3cqscbr.club/posts/sslcertificate/sslcertificate/
作者
漫天花雨
发布于
2026-05-09
许可协议
CC BY-NC-SA 4.0