前置准备
- Certimate :已安装并配置好,能够成功申请/续期证书。
aaPanel :
- 面板已安装并正在运行。
- 已在面板中创建了需要部署证书的 网站 。
- Python 3 环境(aaPanel 通常自带 Python 3)。
第一步:配置 aaPanel API
为了让脚本能够通过接口操作面板,需要在宝塔面板中开启 API 功能。
- 登录 aaPanel/宝塔面板。
- 点击左侧菜单的 面板设置 (Settings) 。
- 找到 API 接口 (API Interface) 选项,点击开启。
记录关键信息 :
- 接口密钥 (API Secret Key) :复制并保存,稍后会用到。
- 面板地址 (Panel URL) :通常是
http://IP:端口。
配置 IP 白名单 :
- 在 API 设置中的“IP 白名单”栏,填入部署 Certimate 的服务器 IP 地址。

第二步:准备部署脚本
我们将使用一个 Python 脚本来完成“上传证书”和“部署到站点”两个动作。该脚本会自动处理宝塔 API 需要的签名验证。
脚本文件
在 Certimate 的部署目标服务器上(或 Certimate 容器内),创建一个文件,例如 deploy_aapanel.py,并将以下代码粘贴进去:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import os
import ssl
import sys
import time
import hashlib
import urllib.parse
import urllib.request
import urllib.error
# --- 工具函数 ---
def is_true(value: str | None) -> bool:
if value is None:
return False
return value.strip().lower() in {"1", "true", "yes", "y", "on"}
DEBUG = is_true(os.getenv("DEBUG"))
def debug(msg: str) -> None:
if DEBUG:
print(f"[DEBUG] {msg}", file=sys.stderr)
def require_env(name: str) -> str:
value = os.getenv(name)
if not value:
print(f"❌ 错误: 环境变量 [{name}] 未设置。", file=sys.stderr)
raise SystemExit(1)
return value
def md5_hex(text: str) -> str:
return hashlib.md5(text.encode("utf-8")).hexdigest()
def add_auth_query(url: str, api_secret_key: str) -> tuple[str, int, str]:
# 生成宝塔 API 认证所需的 request_token
now = int(time.time() * 1000)
request_token = md5_hex(str(now) + md5_hex(api_secret_key))
parts = urllib.parse.urlsplit(url)
existing = urllib.parse.parse_qsl(parts.query, keep_blank_values=True)
existing.extend([("request_time", str(now)), ("request_token", request_token)])
new_query = urllib.parse.urlencode(existing)
return urllib.parse.urlunsplit((parts.scheme, parts.netloc, parts.path, new_query, parts.fragment)), now, request_token
def http_post_form(url: str, form: dict[str, str], headers: dict) -> tuple[int | None, str, dict | None]:
data = urllib.parse.urlencode(form).encode("utf-8")
ctx = ssl._create_unverified_context() # 忽略面板自签证书错误
try:
req = urllib.request.Request(url=url, data=data, method="POST")
for k, v in headers.items(): req.add_header(k, v)
with urllib.request.urlopen(req, context=ctx, timeout=60) as resp:
body = resp.read().decode("utf-8", errors="replace")
return getattr(resp, "status", 200), body, json.loads(body)
except Exception as e:
return None, str(e), None
# --- 主逻辑 ---
def main() -> int:
# 1. 获取环境变量
api_secret_key = os.getenv("API_KEY") or os.getenv("API_SECRET_KEY")
panel_url = os.getenv("PANEL_URL", "").rstrip("/")
sites = os.getenv("SITES")
key_path = os.getenv("KEY_PATH")
crt_path = os.getenv("CRT_PATH")
if not all([api_secret_key, panel_url, sites, key_path, crt_path]):
print("❌ 错误: 请确保 API_KEY, PANEL_URL, SITES, KEY_PATH, CRT_PATH 均已设置。", file=sys.stderr)
return 1
# 2. 读取证书文件
try:
with open(key_path, "r") as f: ssl_key_content = f.read()
with open(crt_path, "r") as f: ssl_crt_content = f.read()
except Exception as e:
print(f"❌ 读取证书文件失败: {e}", file=sys.stderr)
return 1
domains = [s.strip() for s in sites.split(",") if s.strip()]
domains_json = json.dumps(domains, ensure_ascii=False)
headers = {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "User-Agent": "Certimate-Deployer"}
print(f"-> 目标面板: {panel_url}")
print(f"-> 目标站点: {domains_json}")
# 3. 上传证书
print("1. 上传证书中...")
upload_url, _, _ = add_auth_query(f"{panel_url}/v2/ssl_domain?action=upload_cert", api_secret_key)
_, _, upload_json = http_post_form(upload_url, {"key": ssl_key_content, "cert": ssl_crt_content}, headers)
if not upload_json or not upload_json.get("status"):
print(f"❌ 上传失败: {upload_json}", file=sys.stderr)
return 1
# 获取证书 Hash(宝塔 V2 接口逻辑)
cert_hash = upload_json.get("message", {}).get("hash") if isinstance(upload_json.get("message"), dict) else None
if not cert_hash:
print(f"❌ 未能获取证书 Hash: {upload_json}", file=sys.stderr)
return 1
print(f"✅ 证书上传成功 (Hash: {cert_hash})")
# 4. 部署到站点
print("2. 应用证书到站点...")
deploy_url, _, _ = add_auth_query(f"{panel_url}/v2/ssl_domain?action=cert_deploy_sites", api_secret_key)
_, _, deploy_json = http_post_form(deploy_url, {"hash": cert_hash, "append": "1", "domains": domains_json}, headers)
if deploy_json and deploy_json.get("status"):
print(f"✅ 部署成功: {deploy_json.get('msg') or deploy_json.get('message')}")
return 0
else:
print(f"❌ 部署失败: {deploy_json}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())第三步:在 Certimate 中配置部署
在 Certimate 后台,添加一个新的部署配置或编辑现有配置。
1. 选择部署方式
选择 Shell 脚本 (或 SSH,取决于你的 Certimate 是运行在本地容器还是通过 SSH 连接远程服务器)。
- 如果 Certimate 运行在 Docker 中 :推荐使用
本地 (Local)执行模式。 - 如果需要远程连接 :选择
SSH并填写服务器连接信息。
2. 设置环境变量 (Environment Variables)

这是最关键的一步,你需要将第一步获取的宝塔信息填入环境变量中。
| 变量名 | 必填 | 示例值 / 说明 |
|---|---|---|
PANEL_URL | 是 | 宝塔面板的完整地址,例如 http://192.168.1.100:8888 |
API_KEY | 是 | 在第一步中获取的宝塔 API 密钥 |
SITES | 是 | 要部署证书的网站域名(宝塔中创建的网站名)。多个域名用逗号分隔,例如 example.com,www.example.com |
KEY_PATH | 是 | Certimate 指定 path后,指向生成的 .key 文件路径 |
CRT_PATH | 自动 | Certimate 指定 path后,指向生成的 .crt 文件路径 |
3. 设置执行命令
在 Certimate 的脚本输入框中,输入调用 Python 脚本的命令。
假设你将脚本保存为 /data/scripts/deploy_aapanel.py,命令如下:
# 确保脚本有执行权限
chmod +x /data/scripts/deploy_aapanel.py
# 执行脚本
# Certimate 会将证书路径通过环境变量传入,脚本直接读取即可
python3 /data/scripts/deploy_aapanel.py❓常见问题 (FAQ)
Q: 为什么上传成功了,但是部署失败?
A: 请确保 SITES 变量中填写的域名 已经在宝塔面板中创建 。宝塔无法向不存在的站点部署证书。
Q: 脚本报错 urllib.error.HTTPError: HTTP Error 401: Unauthorized?
A: 这通常意味着 API Key 错误,或者服务器时间与宝塔面板服务器时间相差太大(导致签名验证失败)。请同步两端服务器的时间。
Q: 我的面板开启了 SSL (HTTPS),PANEL_URL 怎么填?
A: 如果面板地址是 HTTPS 的,请确保 URL 以 https:// 开头。脚本中包含了 ssl._create_unverified_context(),所以即使用户自签名的面板证书也是可以正常连接的。