MENU

Certimate 部署 SSL 证书到 aaPanel (宝塔面板) 指南

December 21, 2025 • Read: 98 • Linux

前置准备

  • Certimate :已安装并配置好,能够成功申请/续期证书。
  • aaPanel

    • 面板已安装并正在运行。
    • 已在面板中创建了需要部署证书的 网站
    • Python 3 环境(aaPanel 通常自带 Python 3)。

第一步:配置 aaPanel API

为了让脚本能够通过接口操作面板,需要在宝塔面板中开启 API 功能。

  1. 登录 aaPanel/宝塔面板。
  2. 点击左侧菜单的 面板设置 (Settings)
  3. 找到 API 接口 (API Interface) 选项,点击开启。
  4. 记录关键信息

    • 接口密钥 (API Secret Key) :复制并保存,稍后会用到。
    • 面板地址 (Panel URL) :通常是 http://IP:端口
  5. 配置 IP 白名单

    • 在 API 设置中的“IP 白名单”栏,填入部署 Certimate 的服务器 IP 地址。

20251221105717.png

第二步:准备部署脚本

我们将使用一个 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)

20251221105752.png

这是最关键的一步,你需要将第一步获取的宝塔信息填入环境变量中。

变量名必填示例值 / 说明
PANEL_URL宝塔面板的完整地址,例如 http://192.168.1.100:8888
API_KEY在第一步中获取的宝塔 API 密钥
SITES要部署证书的网站域名(宝塔中创建的网站名)。多个域名用逗号分隔,例如 example.com,www.example.com
KEY_PATHCertimate 指定 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(),所以即使用户自签名的面板证书也是可以正常连接的。