Sunny
0

用 acme.sh + 腾讯云 DNSPod,为个人多子域名站点配置免费通配符 HTTPS 自动续期

#系统

最近把服务器上的 HTTPS 证书管理顺手重构了一遍。

起因并不复杂:原来的方案不是不能用,而是越来越碎。机器上已经挂了多个长期服务,包括博客、OpenClaw 和工具站,每个子域名单独配一套证书,短期没问题,长期维护就开始变得重复、分散,而且容易遗忘。

最终,我把它收敛成了一套更省心的结构:

  • 使用 acme.sh 管理证书
  • 使用腾讯云 DNSPod API 做 DNS 验证
  • 一次申请 example.com + *.example.com
  • nginx 多个站点统一引用同一套证书
  • 后续自动续期,续期完成后自动 reload nginx

这篇文章就把这次落地过程完整记录下来,同时把中间几个容易踩的坑单独拎出来讲清楚。

一、为什么要从“每个域名单独配证书”切到通配符自动续期

我原来的机器上,大致是这种结构:

  • www.example.com 使用一套证书
  • test1.example.com 使用一套证书
  • test2.example.com 使用一套证书
  • .....

这种方式的优点很直接:

  • 容易理解
  • 刚开始搭站时上手简单
  • 出问题时定位也比较直观

但缺点也很明显:

  • 证书文件越来越多
  • nginx 配置分散在多个位置
  • 免费证书 90 天一轮,续期完全靠手工
  • 后面每新增一个子域名,基本都要重复同一套流程

真正让人烦的不是“不会做”,而是:

这件事明明不复杂,但总要周期性再做一遍。

对于这种重复劳动,最好的处理方式通常不是继续忍受,而是把它自动化。

如果服务器上只有一个站点,手工维护未必不能接受;但只要开始挂多个子域名服务,比如博客、工具站、面板、AI 助手入口、内部接口等,证书管理就很容易成为一块零碎但持续消耗注意力的运维负担。

所以这次我的目标很明确:

用一套通配符证书覆盖主要子域名,并把续期与部署流程自动化。


二、免费证书和收费证书,差别到底在哪里

这次顺手也把一个常见误区想明白了。

很多人看到云厂商官网里几百、几千元的证书套餐,会下意识以为 HTTPS 本身很贵。其实对个人站点来说,绝大多数场景真正需要的只是:

  • 浏览器信任
  • 传输加密
  • 正常的 HTTPS 访问能力

对于这类需求,免费 DV 证书通常已经足够

收费证书更常卖的是:

  • 企业身份验证
  • 商业支持
  • 合规要求
  • 品牌背书
  • 托管式多域名/通配符管理能力

所以免费DV证书适配个人场景:

  • 个人博客
  • 自建工具站
  • 几个自维护子域名服务

三、通配符证书是不是一定要花钱

不一定。

这是很多人第一次接触通配符证书时最容易混淆的地方。

云厂商控制台里展示的“通配符证书”通常是商业套餐,所以价格看起来不低。但这并不意味着技术上通配符证书必须付费。

如果你走的是下面这条路:

  • acme.sh
  • Let’s Encrypt
  • DNS 验证

那么完全可以免费申请通配符证书。

例如:

*.example.com

不过有一个细节必须注意:
*.example.com 并不包含 example.com 裸域名。

所以更稳妥的申请方式一般是:

-d example.com -d '*.example.com'

这样既能覆盖裸域名,也能覆盖一级子域名。这也是我这次实际采用的方式。


四、为什么通配符证书必须使用 DNS 验证

证书验证通常有两种常见思路:

1. HTTP 验证

让证书机构访问站点下某个固定路径,例如:

http://example.com/.well-known/...

这种方式适合普通单域名证书,思路直观,实现也简单。

2. DNS 验证

通过在域名解析中增加一条 TXT 记录,证明你对该域名拥有控制权。

如果要申请通配符证书,比如:

*.example.com

那就必须走 DNS 验证。

这也意味着自动化工具需要具备操作 DNS 解析的能力。所以我这次的结构自然变成了:

  • 腾讯云 DNSPod 管解析
  • acme.sh 通过 API 自动写入 _acme-challenge TXT 记录
  • 验证通过后向 Let’s Encrypt 申请证书

五、我的实际环境

为了方便你判断这套方案能否直接套用,先简单交代一下环境:

  • 域名:example.com
  • DNS:腾讯云 DNSPod
  • Web 服务:nginx
  • 服务形态:多个子域名反向代理多个本地服务

当前实际用到的站点包括:

  • www.example.com
  • test1.example.com
  • test2.example.com

本质上,这是一套很典型的:

一台 Linux 服务器 + nginx + 多子域名自建服务

这类环境非常适合统一使用通配符证书。


六、最终采用的方案

我最后确定下来的方案是:

  • 使用 acme.sh 作为 ACME 客户端
  • 使用腾讯云 DNSPod API 做 DNS 验证
  • 一次申请:
    • example.com
    • *.example.com
  • 将证书统一部署到固定路径:
xxx/example.com_bundle.crt
xxx/example.com.key

然后让 nginx 里所有需要的站点统一引用这两份文件。

这样做有两个明显好处:

  1. 证书来源统一:以后不用再记每个站点各自用哪套证书。
  2. 续期流程固定acme.sh 续期后直接覆盖固定路径,再自动 reload nginx。

七、实际落地步骤

1)安装 acme.sh

先安装 acme.sh

curl https://get.acme.sh | sh -s email=你的邮箱

安装完成后,它通常会完成以下动作:

  • 把脚本装到 ~/.acme.sh/
  • 给 shell 加别名
  • 自动注册定时任务

后续的自动续期就是靠它完成的。


2)准备腾讯云 API 密钥

这里我一开始还踩了一个认知坑。

最初我以为需要找 DNSPod 老式 Token,也就是类似:

  • DP_Id
  • DP_Key

但后来核对 acme.sh 官方适配脚本后确认,现在这套环境完全可以直接走腾讯云新版 DNS 插件:

--dns dns_tencent

对应的变量是:

Tencent_SecretId
Tencent_SecretKey

也就是说,这里要准备的是腾讯云 API 密钥中的:

  • SecretId
  • SecretKey
    获取腾讯云API密钥管理界面,如图所示

这两个值本质上属于敏感凭据,务必做到:

  • 不通过聊天明文发送
  • 不提交到 git
  • 不放进公开文章截图
  • 最好只在服务器本机填写,如果后续没使用到,建议删除,避免留下风险。

3)申请通配符证书

核心申请命令如下:

/root/.acme.sh/acme.sh --issue --dns dns_tencent -d example.com -d '*.example.com'

这条命令会完成几件事:

  • 调用腾讯云 DNSPod API
  • 自动写入 _acme-challenge TXT 记录
  • 向 Let’s Encrypt 发起域名验证
  • 验证通过后签发证书

如果执行过程中报错,建议第一时间加 debug:

/root/.acme.sh/acme.sh --issue --dns dns_tencent -d example.com -d '*.example.com' --debug 2

这一点非常重要,因为后续排查问题时,debug 输出基本就是最直接的依据。


4)把证书部署到 nginx 固定路径

签发成功后,不建议到处直接引用 acme.sh 默认目录下的原始输出路径。更稳妥的做法,是把最终使用路径固定下来。

我这次统一部署到:

xxx/nginx/zepeng/example.com_bundle.crt
xxx/nginx/zepeng/example.com.key

对应命令类似:

/root/.acme.sh/acme.sh --install-cert -d example.com \
  --key-file xxx/nginx/zepeng/example.com.key \
  --fullchain-file xxx/nginx/zepeng/example.com_bundle.crt \
  --reloadcmd "nginx -t && systemctl reload nginx"

这样做的意义很实际:

  • nginx 永远引用固定路径
  • acme.sh 每次续期都会把新证书写到同一位置
  • 写完以后自动执行配置检查和 reload

换句话说,后续就不再需要手工替换证书文件。


5)统一 nginx 中的证书引用

这是这次真正让结构变“清爽”的关键一步。

原来不同站点分别引用各自的证书文件,例如:

  • www.example.com_bundle.crt
  • test1.example.com_bundle.crt
  • test2.example.com_bundle.crt

后来全部统一改成:

ssl_certificate xxx/nginx/zepeng/example.com_bundle.crt;
ssl_certificate_key xxx/nginx/zepeng/example.com.key;

我涉及的nginx配置文件包括:

  • xxx/nginx/conf.d/halo.conf
  • xxx/nginx/conf.d/test1.conf
  • xxx/nginx/conf.d/test2.conf

统一后,这几个站点就都使用同一套通配符证书。


6)检查并重载 nginx

修改完成后,照例执行:

nginx -t
systemctl reload nginx

这一步不要省略。

我自己的最终结果是:

  • nginx -t 正常
  • reload 成功
  • 正式生效配置已经统一到通配符证书

到这里,这套方案才算真正闭环。


八、这次过程中踩过的几个坑

相比“安装命令”,我觉得下面几个坑其实更值得写,因为它们更接近真实运维场景。

坑 1:看起来只是加一个 80 端口域名,结果打开后进了博客主页

一开始给 test1.example.com 只配了 HTTP 反代,理论上看起来并没有问题:

  • test1.example.com -> nginx 80
  • nginx 再转发到 127.0.0.1:8021

但浏览器实际访问时,却打开了博客主页。

后来回头看才意识到,问题本质不是 80 端口冲突,而是:

实际访问链路很可能已经走到了 HTTPS 443。

如果某个子域名只有 80 配置、没有对应的 443 配置,它就可能被现有其他 443 站点接住。表面上看像是“反代错了”,实际上是 HTTPS 命中逻辑出了偏差。

这也是为什么后面还是老老实实给它补了 HTTPS。


坑 2:一开始误以为必须找 DNSPod 老 Token

前面最初还在找 DNSPod 老式的:

  • DP_Id
  • DP_Key

后面查了 acme.sh 官方适配脚本才确认,现在这套环境直接走:

--dns dns_tencent

对应:

  • Tencent_SecretId
  • Tencent_SecretKey

这个坑的本质在于:

不同年代的文档混在一起看,很容易把老插件和新插件搞混。

稳妥的方式,还是以当前 acme.sh 官方支持为准。


坑 3:AuthFailure.SecretIdNotFound

这个错误也非常值得单独记录。

当时 debug 输出里明确报了:

AuthFailure.SecretIdNotFound
The SecretId is not found

一旦看到这个错误,其实就可以立刻排除很多方向:

  • 不是 nginx 配置问题
  • 不是证书验证机制问题
  • 不是 DNS 线路问题

而是更前面一层:

提交给腾讯云 API 的 SecretId 本身没有被识别到。

最后实际就是通过重新核对/处理密钥解决的。

这个经验很实用:

遇到 API 报错时,不要一上来先怀疑 nginx,要先判断错误是不是已经明确指向凭据本身。


九、最终效果

这次改完之后,整套结构比之前清爽很多。

证书层面

一张证书覆盖:

  • example.com
  • *.example.com

nginx 层面

多个站点统一引用:

xxx/nginx/zepeng/example.com_bundle.crt
xxx/nginx/zepeng/example.com.key

运维层面

后续续期由 acme.sh 自动完成:

  • 自动检查到期时间
  • 自动申请续期
  • 自动部署到固定路径
  • 自动 reload nginx

相比手工每 90 天续一次,这种方式的维护体验要好很多。


十、安全建议

虽然这套方案很好用,但有几件事最好顺手做好。

1. 不要把 API 密钥到处发

像下面这些值:

  • Tencent_SecretId
  • Tencent_SecretKey

本质上都属于比较敏感的云 API 凭据。

不要:

  • 明文发聊天
  • 截图不打码
  • 提交进 git 仓库
  • 写进公开文章

2. 脚本里的真实密钥用完最好清掉

如果为了方便,先把真实值填进脚本里跑通了,后面最好改回占位符。

不然时间久了,很容易遗忘在服务器文件里。


3. 旧证书不要第一时间删除

原来按子域名单独签发的那些旧证书,建议先保留:

  • 先确认所有站点都访问正常
  • 再考虑是否清理旧文件

这样回滚更稳。


4. 能最小权限就尽量最小权限

如果腾讯云 API 密钥权限还能进一步收敛,尽量按最小权限原则处理。

自动化很方便,但自动化凭据也应该按照正式生产凭据来管理。


十一、总结

这次折腾完之后,我对这件事的结论很简单。

第一,个人站点没有必要一上来就为证书支付高价。免费 DV 证书对绝大多数自建服务已经够用。

第二,真正影响维护体验的,通常不是证书贵不贵,而是:

续期和部署是不是自动化。

第三,对于多子域名场景,下面这套组合非常实用:

  • acme.sh
  • 腾讯云 DNSPod API
  • example.com + *.example.com 通配符证书
  • nginx 统一引用固定证书路径

这条路走通之后,后面再新增子域名服务,心态会轻松很多。

因为证书这件事,终于从“周期性手工活”,变成了“基础设施的一部分”。

常用Nginx的配置文件

server {
    listen 80;
    listen [::]:80;
    server_name your-domain.com www.your-domain.com;

    # HTTP 跳转 HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your-domain.com www.your-domain.com;

    # 网站根目录
    root /var/www/blog;
    index index.html;

    # SSL 证书
    ssl_certificate     /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    # TLS 基础配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    # 日志
    access_log /var/log/nginx/blog.access.log;
    error_log  /var/log/nginx/blog.error.log warn;

    # 安全响应头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # favicon / robots
    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        log_not_found off;
        access_log off;
    }

    # 静态资源缓存
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, max-age=2592000, immutable";
        try_files $uri =404;
    }

    # 博客主站
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 禁止访问隐藏文件
    location ~ /\. {
        deny all;
    }
}

参考链接

系列导航

按系列顺序继续阅读当前专题内容

自动化教程