Certimate + 阿里云 ESA免费版 自动证书部署与清理实战指南
解决自定义证书配额超限导致部署失败的问题,用工作流实现全自动证书轮换与旧证书清理。
一、你可能遇到的问题
如果你用 Certimate 自动签发证书,然后部署到阿里云边缘安全加速(ESA),可能会遇到这样的报错:
CertQuotaCheckFailed: The custom certificate quota verification failed.
The number of uploaded certificates exceeds the upper limit allowed for the plan.
翻译过来就是:你上传的自定义证书数量超过了套餐配额,不能再传了。
很多人第一次遇到这个问题会懵:我明明只是续期一张证书,为什么会提示证书太多?
二、问题根源:ESA 的证书配额限制

打开阿里云 ESA 控制台的「边缘证书」页面,你会看到这样的配额信息:
- 自定义证书:免费版套餐只能传 2 条
- Let's Encrypt 免费证书:套餐额度是 5 条
关键问题在于:每部署一次新证书,ESA 就会新增一条自定义证书记录,旧的证书不会自动删除。
也就是说:
- 第一次部署:1 条自定义证书
- 第二次部署:2 条自定义证书(旧的还在)
- 第三次部署:超过 2 条配额,直接报错
这就是为什么你每次续期证书,过几次就会突然失败的原因。
三、解决方案:在部署前自动清理旧证书
思路很简单:在把新证书上传到 ESA 之前,先把旧的过期证书和多余的有效证书删掉,保证配额够用。
我们用 Certimate 的工作流来实现这个自动化:
定时触发 → 申请新证书 → SSH执行清理脚本 → 部署到ESA → 失败时发邮件通知
四、整体效果

Certimate 的仪表盘可以看到所有证书、工作流状态和最近的运行历史。我们这套方案跑通之后,工作流会每天自动运行,不需要人工干预。
五、搭建步骤
步骤 1:准备授权凭据

首先在 Certimate 的「授权凭据」页面,我们需要准备两个授权:
- 阿里云授权:用于 DNS 验证和 ESA 部署
- SSH 远程主机授权:用于执行清理脚本(需要一台能 SSH 登录的服务器,有 python3 和 base64 命令)
- *
1.1 新建授权:选择提供商

点击右上角「新建授权」,会看到所有支持的提供商列表。我们需要的两个都在第一行:
- 远程主机(SSH):用来跑清理脚本
- 阿里云:用来做 DNS 验证和 ESA 部署
- *
1.2 配置阿里云授权

选择「阿里云」之后,填写这几项:
- 名称:随便起个好认的名字,比如「我的阿里云」
- 阿里云 AccessKeyID:你的阿里云 AccessKey ID
- 阿里云 AccessKeySecret:你的阿里云 AccessKey Secret
安全提醒:AccessKey 相当于你的账号密码,不要泄露出去。
1.3 配置 SSH 远程主机授权

选择「远程主机(SSH)」之后,填写:
- 名称:比如「我的NAS服务器」
- 服务器地址:你的服务器 IP,比如 192.168.2.232
- 服务器端口:默认 22
- 认证方式:密码(简单方便)或 SSH 密钥(更安全)
- 用户名:你的 SSH 登录用户名
- 密码:你的登录密码
踩坑提醒:SSH 用户名和密码要确认能正常登录,很多人配置错了用户名(比如填了 root 但实际是 admin)导致认证失败。
1.4 配置通知渠道(可选,推荐)

在授权凭据页面,切换到「通知渠道」标签,点击新建授权,选择你想要的通知方式:
- 邮件(SMTP):最常用的,填邮箱和SMTP服务器就行
- 企业微信 / 钉钉 / 飞书:用机器人Webhook
- Webhook:自定义接口
配置好之后,工作流里的通知节点才能选到这个渠道发消息。
步骤 2:创建预设脚本模板

进入「预设模板 → 脚本模板」,新建一个模板,命名为「阿里云ESA-清理过期及多余自定义证书」。
脚本内容用 base64 一行命令的形式,避免格式问题。完整脚本如下,直接复制使用,只需要改最上面的 4 个参数:
# ===== 配置参数(只需要改这里)=====
SITE_ID="替换成你的ESA站点ID"
MAX_KEEP=1
AK="替换成你的阿里云AccessKey ID"
SK="替换成你的阿里云AccessKey Secret"
# ==================================
echo 'aW1wb3J0IG9zLCBzeXMsIGhtYWMsIGhhc2hsaWIsIGJhc2U2NCwgdXJsbGliLnBhcnNlLCB1cmxsaWIucmVxdWVzdCwganNvbgpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZSwgdGltZXpvbmUKCiMg5LuO546v5aKD5Y+Y6YeP6K+75Y+W5Y+C5pWw77yM6buY6K6k5YC85YWc5bqVClNJVEVfSUQgPSBvcy5lbnZpcm9uLmdldCgiU0lURV9JRCIsICIxMDM2MjM4NTE0MzQyNDMyIikKTUFYX0tFRVAgPSBpbnQob3MuZW52aXJvbi5nZXQoIk1BWF9LRUVQIiwgIjEiKSkKQUsgPSBvcy5lbnZpcm9uLmdldCgiQUsiLCAiTFRBSTV0N0QxVWtxTW1NZXNBNXptNmZuIikKU0sgPSBvcy5lbnZpcm9uLmdldCgiU0siLCAiSUFBdGwwWWt3eTU4d1FaZmtnRTdzNXc2S3Zvc3NIIikKRU5EUE9JTlQgPSAiZXNhLmNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbSIKQVBJX1ZFUiA9ICIyMDI0LTA5LTEwIgoKZGVmIHJwYyhhY3Rpb24sIHBhcmFtcz1Ob25lKToKICAgIHAgPSB7CiAgICAgICAgIkZvcm1hdCI6ICJKU09OIiwgIlZlcnNpb24iOiBBUElfVkVSLCAiQWNjZXNzS2V5SWQiOiBBSywKICAgICAgICAiU2lnbmF0dXJlTWV0aG9kIjogIkhNQUMtU0hBMSIsCiAgICAgICAgIlRpbWVzdGFtcCI6IGRhdGV0aW1lLm5vdyh0aW1lem9uZS51dGMpLnN0cmZ0aW1lKCIlWS0lbS0lZFQlSDolTTolU1oiKSwKICAgICAgICAiU2lnbmF0dXJlVmVyc2lvbiI6ICIxLjAiLCAiU2lnbmF0dXJlTm9uY2UiOiBvcy51cmFuZG9tKDgpLmhleCgpLAogICAgICAgICJBY3Rpb24iOiBhY3Rpb24sCiAgICB9CiAgICBpZiBwYXJhbXM6CiAgICAgICAgcC51cGRhdGUocGFyYW1zKQogICAgaXRlbXMgPSBzb3J0ZWQocC5pdGVtcygpKQogICAgY2Fub24gPSAiJiIuam9pbih1cmxsaWIucGFyc2UucXVvdGUoaywgc2FmZT0iIikrIj0iK3VybGxpYi5wYXJzZS5xdW90ZShzdHIodiksIHNhZmU9IiIpIGZvciBrLHYgaW4gaXRlbXMpCiAgICBzMnMgPSAiR0VUJiIrdXJsbGliLnBhcnNlLnF1b3RlKCIvIiwgc2FmZT0iIikrIiYiK3VybGxpYi5wYXJzZS5xdW90ZShjYW5vbiwgc2FmZT0iIikKICAgIHNpZyA9IGJhc2U2NC5iNjRlbmNvZGUoaG1hYy5uZXcoKFNLKyImIikuZW5jb2RlKCksIHMycy5lbmNvZGUoKSwgaGFzaGxpYi5zaGExKS5kaWdlc3QoKSkuZGVjb2RlKCkKICAgIHBbIlNpZ25hdHVyZSJdID0gc2lnCiAgICB1cmwgPSAiaHR0cHM6Ly8iK0VORFBPSU5UKyIvPyIrdXJsbGliLnBhcnNlLnVybGVuY29kZShwKQogICAgd2l0aCB1cmxsaWIucmVxdWVzdC51cmxvcGVuKHVybCwgdGltZW91dD0zMCkgYXMgcjoKICAgICAgICByZXR1cm4ganNvbi5sb2FkcyhyLnJlYWQoKSkKCmRlZiBsaXN0X3VwbG9hZF9jZXJ0cyhzaXRlX2lkKToKICAgIGFsbF9jZXJ0cywgcGFnZSA9IFtdLCAxCiAgICB3aGlsZSBUcnVlOgogICAgICAgIGQgPSBycGMoIkxpc3RDZXJ0aWZpY2F0ZXMiLCB7IlNpdGVJZCI6IHNpdGVfaWQsICJUeXBlIjogInVwbG9hZCIsICJQYWdlTnVtYmVyIjogcGFnZSwgIlBhZ2VTaXplIjogNTB9KQogICAgICAgIGNzID0gZC5nZXQoIlJlc3VsdCIsIFtdKQogICAgICAgIGFsbF9jZXJ0cy5leHRlbmQoY3MpCiAgICAgICAgaWYgbGVuKGFsbF9jZXJ0cykgPj0gZC5nZXQoIlRvdGFsQ291bnQiLCAwKToKICAgICAgICAgICAgYnJlYWsKICAgICAgICBwYWdlICs9IDEKICAgIHJldHVybiBhbGxfY2VydHMKCmRlZiBkZWxfY2VydChzaXRlX2lkLCBjaWQpOgogICAgcmV0dXJuIHJwYygiRGVsZXRlQ2VydGlmaWNhdGUiLCB7IlNpdGVJZCI6IHNpdGVfaWQsICJJZCI6IGNpZH0pCgp0cnk6CiAgICBwcmludChmIj09PSDlvIDlp4vmuIXnkIYgRVNBIOermeeCuSB7U0lURV9JRH0g55qE6Ieq5a6a5LmJ6K+B5LmmID09PSIpCgogICAgY2VydHMgPSBsaXN0X3VwbG9hZF9jZXJ0cyhTSVRFX0lEKQogICAgcHJpbnQoZiLlhbEge2xlbihjZXJ0cyl9IOW8oCB1cGxvYWQg57G75Z6L6K+B5LmmIikKCiAgICBleHBpcmVkID0gW2MgZm9yIGMgaW4gY2VydHMgaWYgYy5nZXQoIlN0YXR1cyIpID09ICJFeHBpcmVkIl0KICAgIHByaW50KGYi5YW25Lit5bey6L+H5pyfOiB7bGVuKGV4cGlyZWQpfSDlvKAiKQogICAgZm9yIGMgaW4gZXhwaXJlZDoKICAgICAgICBjbiA9IGMuZ2V0KCJDb21tb25OYW1lIiwgIiIpCiAgICAgICAgY2lkID0gY1siSWQiXQogICAgICAgIHRyeToKICAgICAgICAgICAgZGVsX2NlcnQoU0lURV9JRCwgY2lkKQogICAgICAgICAgICBwcmludChmIiAgW09LXSDliKDpmaTov4fmnJ86IHtjbn0gKHtjaWRbOjE2XX0uLi4pIikKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIHByaW50KGYiICBbRkFJTF0g5Yig6Zmk5aSx6LSlIHtjbn06IHtlfSIpCgogICAgdmFsaWQgPSBbYyBmb3IgYyBpbiBjZXJ0cyBpZiBjLmdldCgiU3RhdHVzIikgIT0gIkV4cGlyZWQiXQogICAgcHJpbnQoZiLliankvZnmnInmlYjor4HkuaY6IHtsZW4odmFsaWQpfSDlvKDvvIzpmIjlgLwge01BWF9LRUVQfSDlvKAiKQogICAgaWYgbGVuKHZhbGlkKSA+IE1BWF9LRUVQOgogICAgICAgIHZhbGlkLnNvcnQoa2V5PWxhbWJkYSB4OiB4LmdldCgiQ3JlYXRlVGltZSIsICIiKSkKICAgICAgICBkcm9wID0gdmFsaWRbOmxlbih2YWxpZCktTUFYX0tFRVBdCiAgICAgICAgZm9yIGMgaW4gZHJvcDoKICAgICAgICAgICAgY24gPSBjLmdldCgiQ29tbW9uTmFtZSIsICIiKQogICAgICAgICAgICBjdCA9IGMuZ2V0KCJDcmVhdGVUaW1lIiwgIiIpWzoxMF0KICAgICAgICAgICAgY2lkID0gY1siSWQiXQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBkZWxfY2VydChTSVRFX0lELCBjaWQpCiAgICAgICAgICAgICAgICBwcmludChmIiAgW09LXSDliKDpmaTml6for4E6IHtjbn0gKOWIm+W7uuS6jiB7Y3R9KSIpCiAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgICAgIHByaW50KGYiICBbRkFJTF0g5Yig6Zmk5aSx6LSlIHtjbn06IHtlfSIpCiAgICBlbHNlOgogICAgICAgIHByaW50KGYiICDmlbDph4/mnKrotoXov4fpmIjlgLzvvIzml6DpnIDliKDpmaQiKQoKICAgIHByaW50KCI9PT0g5riF55CG5a6M5oiQID09PSIpCmV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgIHByaW50KGYiPT09IOa4heeQhui/h+eoi+WHuumUme+8jOS9huS4jeW9seWTjeW3peS9nOa1gToge2V9ID09PSIpCgpzeXMuZXhpdCgwKQo=' | base64 -d | SITE_ID=$SITE_ID MAX_KEEP=$MAX_KEEP AK=$AK SK=$SK python3
怎么获取站点ID? 在阿里云 ESA 控制台的站点概览页,URL 里那串数字就是 SiteId。
为什么用 base64?因为 Certimate 的代码编辑器在保存时会自动调整缩进,导致 heredoc 格式和 Python 代码缩进被破坏。用 base64 编码后解码执行,彻底避免这个问题。
脚本的清理逻辑:
- 列出所有 upload 类型的证书
- 先删除所有状态为「已过期」的证书
- 剩下的有效证书如果超过阈值(比如只保留 1 张),按创建时间排序删除最旧的
- 整个过程用 try-except 包裹,出错也不会影响工作流继续往下走
- *
步骤 3:配置工作流(每个节点详细设置)

新建一个工作流,按这个顺序排布 5 个节点:开始 → 申请 → SSH部署 → ESA部署 → 尝试执行(失败通知)。下面逐个节点说明怎么配置。
3.1 「开始」节点:定时触发

- 触发方式:定时触发
- CRON 表达式:
25 21 * * *(每天晚上 21:25 运行一次)
建议把时间设成随机的,比如几点几分,不要都在零点,避免 Let's Encrypt 流量高峰。
3.2 「申请」节点:自动签发证书

关键配置项:
- 域名:你的域名,比如
sangxuesheng.com - 联系邮箱:你的邮箱,证书到期会通知
- 质询方式:DNS-01(用 DNS 验证,不需要开 80 端口)
- DNS 提供商:阿里云 - 云解析 DNS
- DNS 提供商授权:选择你之前配置好的阿里云授权
- 私钥来源:随机生成
- 私钥算法:RSA2048
- 证书颁发机构:Let's Encrypt
- *
3.3 「部署 - 远程主机(SSH)」节点:执行清理脚本

这是最关键的节点,清理脚本就在这里跑。配置如下:
- 待部署证书:申请(来自上一个申请节点)
- 主机提供商:远程主机(SSH)
- 主机提供商授权:选择你之前配置好的 SSH 授权(比如「听闻NAS」)
- 使用 SCP 传输文件:关闭
- 文件格式:PEM 格式
- 私钥文件输出路径:
/tmp/cert.key - 证书文件输出路径:
/tmp/cert.crt - 前置命令(可选):选择你预设模板里的清理脚本
踩坑提醒:SSH 节点默认会把证书文件上传到远程主机,如果路径权限不对会报错。改成 /tmp/ 目录就不会有权限问题了。虽然我们不需要这些文件,但 Certimate 的 SSH 部署节点一定会上传,不如给它一个能写的路径。3.4 「部署 - 阿里云边缘安全加速 ESA」节点:部署新证书

- 待部署证书:申请
- 主机提供商:阿里云 - 边缘安全加速 ESA
- 主机提供商授权:阿里云
- 阿里云服务地域:
cn-hangzhou(杭州) - 阿里云 ESA 站点 ID:你的站点 ID,比如
1036238514342432 - 重复部署:上次部署相同证书成功后跳过
- *
3.5 「尝试执行...」节点:失败通知
最后加一个「尝试执行...」节点,把后面的 ESA 部署包起来。如果部署失败,就走 Catch 分支,发邮件通知你。这样不会因为失败就卡住整个流程。
3.6 通知节点建议:只发失败提醒,成功不打扰

强烈建议:只在失败的时候发通知,成功了不用发。
证书自动部署本来就是每天跑的日常任务,每次成功都发邮件/微信,不出一周你就嫌吵了。只有失败的时候才需要提醒你去处理。
工作流里的「尝试执行...」节点就是干这个的:
- 成功走完 → 直接结束,不发通知
- 失败了 → 走 Catch 分支 → 发通知给你
证书部署失败模板(推荐):
[证书部署失败] {{ $workflow.name }} - 节点 {{ $node.name }} 执行出错
您好,您的证书部署任务出现失败,详情如下:
失败节点:{{ $node.name }}
错误信息:{{ $error.message }}
发生时间:{{ $time }}
请尽快登录 Certimate 查看日志并处理。
通知方式可以在「授权凭据 → 通知渠道」里配置,支持邮件、Webhook、企业微信、钉钉、飞书等。如果你实在想看成功通知,也可以在工作流结束节点后面再加一个通知节点,看个人喜好。
步骤 4:运行验证

发布更改之后,手动运行一次工作流,看看运行历史里的日志:
=== 开始清理 ESA 站点 1036238514342432 的自定义证书 ===
共 1 张 upload 类型证书
其中已过期: 0 张
剩余有效证书: 1 张,阈值 1 张
数量未超过阈值,无需删除
=== 清理完成 ===
看到这个输出就说明清理脚本执行成功了。然后 ESA 部署节点会继续执行,把新证书上传上去。
六、常见坑与注意事项
坑 1:Certimate 容器里没有 shell
如果你用「本地主机」部署节点执行脚本,会报错:
exec: "sh": executable file not found in $PATH
因为 Certimate 的 Docker 镜像极度精简,连 /bin/sh 都没有。所以必须用「SSH 远程主机」节点来执行脚本。
坑 2:脚本格式被破坏
直接在编辑器里写 Python heredoc,保存后会变成这样:
here-document at line 1 delimited by end-of-file (wanted 'PYEOF')IndentationError: unexpected indent
这是因为 CodeMirror 编辑器自动调整了缩进和换行。解决方案就是用 base64 编码的一行命令。
坑 3:SSH 认证失败
报错 handshake failed: unable to authenticate 时,先检查:
- 用户名是不是正确(很多人填了 root,但实际正确的是 admin)
- 密码是不是对的
- 服务器是不是禁止密码登录
坑 4:上传私钥文件权限不足
报错 failed to create remote directory: permission denied 时,把输出路径改成 /tmp/ 下的临时文件就行。我们本来就不需要这些文件,只是为了让节点能跑完。
七、方案总结
这套方案的核心思路:
- 问题本质:ESA 的自定义证书配额有限,每次部署都会堆积旧证书
- 解决思路:部署前先清理,保证配额够用
- 实现方式:用 Certimate 的 SSH 部署节点当执行入口,跑一个清理脚本
- 稳定性保证:脚本加了错误保护,即使清理失败也不会卡住整个工作流
如果你也遇到了 ESA 证书配额超限的问题,按照这个思路搭一遍,就能实现完全自动化的证书轮换 + 旧证书清理了。