ma-cert基于 ACME v2 (RFC 8555) 的 Let's Encrypt 免费证书自动申请脚本,使用 阿里云 DNS (dns-01) 完成域名所有权验证。
两种运行方式方式一:命令行(Node 脚本)
详见下文「配置说明」。 方式二:VS Code 插件(Ma-Cert)本项目同时是一个 VS Code 扩展,在编辑器内一键签发/续期,并查看证书状态。 安装 / 调试:
命令:
| 命令 | 说明 |
|------|------|
| 配置(VS Code 设置,替代
打包发布:
目录结构
证书输出规则每次签发会:
这样既保留了完整历史版本(便于回滚、排查),又给部署脚本提供了一个固定不变的引用路径 模块职责
快速开始1. 环境要求
2. 创建
|
| 变量 | 必填 | 默认值 | 说明 |
|---|---|---|---|
ALI_AK |
是 | — | 阿里云 AccessKeyId |
ALI_SK |
是 | — | 阿里云 AccessKeySecret |
DOMAINS |
是 | — | 逗号分隔的域名列表,支持 *. 泛域名 |
EMAIL |
否 | admin@example.com |
ACME 账户联系邮箱 |
OUT_DIR |
否 | ./certs |
证书输出根目录,实际写入其下的时间戳子目录 + latest/ |
STAGING |
否 | true |
true 用测试环境,默认是测试环境 |
DRY_RUN |
否 | false |
true 时不真正签发,仅验证协议链路 |
SKIP_IF_VALID |
否 | true |
现有证书仍有效时跳过签发 |
RENEW_BEFORE_DAYS |
否 | 15 |
到期前多少天开始续期 |
FORCE_RENEW |
否 | false |
true 时忽略有效期检查,强制重新签发 |
续期检查(幂等运行)
脚本在任何网络请求之前会先检查 OUT_DIR/latest/fullchain.pem,满足条件就直接退出。
这让脚本可以安全地被定时任务反复调用,不会浪费 Let's Encrypt 限额。
判定顺序:
| 条件 | 结果 |
|---|---|
FORCE_RENEW=true |
签发(忽略后续所有检查) |
SKIP_IF_VALID=false |
签发 |
latest/fullchain.pem 不存在或损坏 |
签发 |
现有证书未覆盖全部 DOMAINS |
签发 |
剩余天数 <= RENEW_BEFORE_DAYS |
签发 |
剩余天数 > RENEW_BEFORE_DAYS |
跳过 |
跳过时的输出示例:
现有证书: CN=R3
覆盖域名: example.com, *.example.com
到期时间: 2026-10-06T12:00:00.000Z(剩余 60 天)
✔ 证书仍然有效(剩余 60 天 > 阈值 15 天),跳过本次签发
如需强制重新签发,使用 FORCE_RENEW=true 运行
临时强制签发(不改 .env):
# PowerShell
$env:FORCE_RENEW='true'; node certd_acme.js
# Linux / macOS
FORCE_RENEW=true node certd_acme.js
域名覆盖检查是逐字匹配的:若
DOMAINS从example.com改成example.com,*.example.com,即使旧证书还剩很久,也会自动触发重新签发。
工作流程
0. 检查现有证书有效期,充足则直接退出(见上节)
1. 读取/生成账户密钥 → account_{prod|staging}.key
2. 获取 ACME directory
3. 注册(或复用)Let's Encrypt 账户
4. 生成域名 RSA 2048 密钥 + 构造 CSR(含 SAN 扩展)
5. 创建订单 newOrder
6. 【阶段一】为每个域名计算 dns-01 值,写入阿里云 TXT 记录
7. 【阶段二】通过公共 DNS(223.5.5.5 / 8.8.8.8 / 1.1.1.1) 轮询确认记录已传播
8. 【阶段三】通知 CA 校验 → 轮询 authorization 直到 valid
9. finalize 提交 CSR → 轮询订单直到 valid 拿到 certificate URL
10. POST-as-GET 下载证书 → 拆分写出 4 个 pem 文件
11. 清理本次添加的 TXT 记录
部署示例(Nginx)
server {
listen 443 ssl http2;
server_name example.com *.example.com;
# 引用 latest 目录,续期后无需改配置,reload 即可
ssl_certificate /path/to/certs/local/latest/fullchain.pem;
ssl_certificate_key /path/to/certs/local/latest/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
}
更新证书后需 reload:nginx -s reload
⚠️ 注意事项
一、务必先跑测试环境
STAGING=true 时使用 acme-staging-v02.api.letsencrypt.org:
- 签出的证书浏览器不信任,只能用于验证流程
- 限额极宽松(每周 30000 张重复证书),可随意重试
- 任何脚本改动、新域名首次申请,都应先在 staging 验证通过再切正式环境
二、正式环境限流(Let's Encrypt Rate Limits)
正式环境限流很严格,触发后需等待才能恢复:
| 限制项 | 额度 |
|---|---|
| 同一组域名重复证书 | 5 张 / 周 |
| 每个注册域名签发证书 | 50 张 / 周 |
| 每个 IP 新建账户 | 10 个 / 3 小时 |
| 每个账户新建订单 | 300 个 / 3 小时 |
| 每个账户失败验证 | 5 次 / 小时 / 域名 |
最容易踩的坑:反复调试正式环境,一周内跑满 5 次就被锁到下周。所以调试一律用 staging。
三、账户密钥必须妥善保存
account_prod.key/account_staging.key由脚本自动生成并复用- 删除后会重新注册新账户,可能触发「每 IP 每 3 小时 10 个账户」限制
- 文件权限已设为
0600,绝不能提交到 Git
四、.gitignore 必须包含
.env
account_*.key
certs/
*.pem
*.key
私钥一旦泄漏,证书即失去意义,必须立即吊销并重签。
五、DNS 相关
- 脚本会先清空
_acme-challenge下的历史 TXT 记录(每个根域只清一次),若你手工加过同名 TXT 记录会被删除 example.com与*.example.com的挑战记录同名(都是_acme-challenge.example.com),必须共存两条 TXT,脚本已处理,不要手工干预- DNS 传播等待上限 180 秒,超时会继续尝试通知 CA;若你的 DNS TTL 很长,可能需要调大
- 签发完成或失败时脚本会自动清理本次添加的 TXT 记录
六、证书有效期与续期
- Let's Encrypt 证书有效期 90 天
- 建议到期前 30 天续期(即每 60 天跑一次)
- 每次运行都会生成新的域名私钥,且写入新的时间戳目录;
latest/会同步更新,Nginx 引用latest/时只需 reload - 历史目录会不断累积,可定期清理旧的(保留最近 2~3 次即可)
由于开启 SKIP_IF_VALID 后脚本是幂等的(有效期充足会自动跳过),
可以放心地每天跑一次,到期前自动续期,无需精确计算周期。
Linux 下用 crontab(每天凌晨 3 点检查):
0 3 * * * cd /path/to/ma-cert && /usr/bin/node certd_acme.js >> /var/log/ma-cert.log 2>&1
续期成功后需 reload Nginx,可追加:
0 3 * * * cd /path/to/ma-cert && /usr/bin/node certd_acme.js >> /var/log/ma-cert.log 2>&1 && nginx -t && nginx -s reload
Windows 可用「任务计划程序」创建每日任务。
七、其他
- Let's Encrypt 没有网页控制台,账户由密钥标识,无用户名密码
- 想查看已签发的证书,可到 crt.sh 搜索你的域名
- 泛域名
*.example.com不覆盖裸域名example.com,两者都需要时必须同时写进DOMAINS - 泛域名只能用 dns-01 验证,不支持 http-01
常见错误排查
| 报错 | 原因 | 处理 |
|---|---|---|
缺少 DOMAINS 环境变量 |
.env 未配置或未被读取 |
确认 .env 与脚本同目录 |
阿里云 DNS 调用 ... 失败 |
AK/SK 错误或权限不足 | 检查 RAM 用户的 alidns 权限 |
DNS 传播等待超时 |
TTL 过长 / 解析未生效 | 手工 nslookup -type=TXT _acme-challenge.example.com 确认 |
Incorrect TXT record |
TXT 值不匹配 | 确认没有残留旧记录干扰 |
校验失败: ... 5 failed authorizations |
触发失败验证限流 | 等 1 小时后重试,或先用 staging 调试 |
too many certificates already issued |
触发重复证书限流 | 等到下周,或改用 staging |
finalize 失败: 400 ... malformed |
CSR 结构问题 | 脚本已修复;如自行改过 CSR 代码需用 OpenSSL 校验 |
许可
内部自用脚本,按需自取。