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边缘证书页面

打开阿里云 ESA 控制台的「边缘证书」页面,你会看到这样的配额信息:

  • 自定义证书:免费版套餐只能传 2 条
  • Let's Encrypt 免费证书:套餐额度是 5 条

关键问题在于:每部署一次新证书,ESA 就会新增一条自定义证书记录,旧的证书不会自动删除。

也就是说:

  • 第一次部署:1 条自定义证书
  • 第二次部署:2 条自定义证书(旧的还在)
  • 第三次部署:超过 2 条配额,直接报错

这就是为什么你每次续期证书,过几次就会突然失败的原因。


三、解决方案:在部署前自动清理旧证书

思路很简单:在把新证书上传到 ESA 之前,先把旧的过期证书和多余的有效证书删掉,保证配额够用。

我们用 Certimate 的工作流来实现这个自动化:

定时触发 → 申请新证书 → SSH执行清理脚本 → 部署到ESA → 失败时发邮件通知

四、整体效果

Certimate仪表盘

Certimate 的仪表盘可以看到所有证书、工作流状态和最近的运行历史。我们这套方案跑通之后,工作流会每天自动运行,不需要人工干预。


五、搭建步骤

步骤 1:准备授权凭据

授权凭据列表页面

首先在 Certimate 的「授权凭据」页面,我们需要准备两个授权:

  1. 阿里云授权:用于 DNS 验证和 ESA 部署
  2. SSH 远程主机授权:用于执行清理脚本(需要一台能 SSH 登录的服务器,有 python3 和 base64 命令)
    • *

1.1 新建授权:选择提供商

新建授权提供商选择

点击右上角「新建授权」,会看到所有支持的提供商列表。我们需要的两个都在第一行:

  • 远程主机(SSH):用来跑清理脚本
  • 阿里云:用来做 DNS 验证和 ESA 部署
    • *

1.2 配置阿里云授权

阿里云授权配置

选择「阿里云」之后,填写这几项:

  • 名称:随便起个好认的名字,比如「我的阿里云」
  • 阿里云 AccessKeyID:你的阿里云 AccessKey ID
  • 阿里云 AccessKeySecret:你的阿里云 AccessKey Secret
安全提醒:AccessKey 相当于你的账号密码,不要泄露出去。

1.3 配置 SSH 远程主机授权

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 编码后解码执行,彻底避免这个问题。

脚本的清理逻辑:

  1. 列出所有 upload 类型的证书
  2. 先删除所有状态为「已过期」的证书
  3. 剩下的有效证书如果超过阈值(比如只保留 1 张),按创建时间排序删除最旧的
  4. 整个过程用 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)
  • 主机提供商授权:选择你之前配置好的 SSH 授权(比如「听闻NAS」)
  • 使用 SCP 传输文件:关闭
  • 文件格式:PEM 格式
  • 私钥文件输出路径:/tmp/cert.key
  • 证书文件输出路径:/tmp/cert.crt
  • 前置命令(可选):选择你预设模板里的清理脚本
踩坑提醒:SSH 节点默认会把证书文件上传到远程主机,如果路径权限不对会报错。改成 /tmp/ 目录就不会有权限问题了。虽然我们不需要这些文件,但 Certimate 的 SSH 部署节点一定会上传,不如给它一个能写的路径。

3.4 「部署 - 阿里云边缘安全加速 ESA」节点:部署新证书

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/ 下的临时文件就行。我们本来就不需要这些文件,只是为了让节点能跑完。


七、方案总结

这套方案的核心思路:

  1. 问题本质:ESA 的自定义证书配额有限,每次部署都会堆积旧证书
  2. 解决思路:部署前先清理,保证配额够用
  3. 实现方式:用 Certimate 的 SSH 部署节点当执行入口,跑一个清理脚本
  4. 稳定性保证:脚本加了错误保护,即使清理失败也不会卡住整个工作流

如果你也遇到了 ESA 证书配额超限的问题,按照这个思路搭一遍,就能实现完全自动化的证书轮换 + 旧证书清理了。

最后修改:2026 年 10 月 09 日
如果觉得我的文章对你有用,请随意赞赏