最近把服务器上的 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-challengeTXT 记录- 验证通过后向 Let’s Encrypt 申请证书
五、我的实际环境
为了方便你判断这套方案能否直接套用,先简单交代一下环境:
- 域名:
example.com - DNS:腾讯云 DNSPod
- Web 服务:nginx
- 服务形态:多个子域名反向代理多个本地服务
当前实际用到的站点包括:
www.example.comtest1.example.comtest2.example.com
本质上,这是一套很典型的:
一台 Linux 服务器 + nginx + 多子域名自建服务
这类环境非常适合统一使用通配符证书。
六、最终采用的方案
我最后确定下来的方案是:
- 使用
acme.sh作为 ACME 客户端 - 使用腾讯云 DNSPod API 做 DNS 验证
- 一次申请:
example.com*.example.com
- 将证书统一部署到固定路径:
xxx/example.com_bundle.crt
xxx/example.com.key
然后让 nginx 里所有需要的站点统一引用这两份文件。
这样做有两个明显好处:
- 证书来源统一:以后不用再记每个站点各自用哪套证书。
- 续期流程固定:
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_IdDP_Key
但后来核对 acme.sh 官方适配脚本后确认,现在这套环境完全可以直接走腾讯云新版 DNS 插件:
--dns dns_tencent
对应的变量是:
Tencent_SecretId
Tencent_SecretKey
也就是说,这里要准备的是腾讯云 API 密钥中的:
SecretIdSecretKey
获取腾讯云API密钥管理界面,如图所示

这两个值本质上属于敏感凭据,务必做到:
- 不通过聊天明文发送
- 不提交到 git
- 不放进公开文章截图
- 最好只在服务器本机填写,如果后续没使用到,建议删除,避免留下风险。
3)申请通配符证书
核心申请命令如下:
/root/.acme.sh/acme.sh --issue --dns dns_tencent -d example.com -d '*.example.com'
这条命令会完成几件事:
- 调用腾讯云 DNSPod API
- 自动写入
_acme-challengeTXT 记录 - 向 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.crttest1.example.com_bundle.crttest2.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.confxxx/nginx/conf.d/test1.confxxx/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_IdDP_Key
后面查了 acme.sh 官方适配脚本才确认,现在这套环境直接走:
--dns dns_tencent
对应:
Tencent_SecretIdTencent_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_SecretIdTencent_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;
}
}
参考链接
- acme.sh 官方仓库:https://github.com/acmesh-official/acme.sh
- acme.sh 官方 Wiki:https://github.com/acmesh-official/acme.sh/wiki
- acme.sh DNS API 说明:https://github.com/acmesh-official/acme.sh/wiki/dnsapi
- Let’s Encrypt 官网:https://letsencrypt.org/
- 腾讯云 DNSPod 产品页:https://cloud.tencent.com/product/dnspod