证书
GET/api/certs
返回你名下的证书列表。
| 字段 | 说明 |
|---|---|
id | 证书 ID,用于站点的 cert_id |
name | 证书名 |
domain | 覆盖的域名 |
type | lets 表示系统自动签发并自动续期 |
上传证书(自动化续期)
POST/api/certs/upload
把你自己签的证书推上来。按域名幂等:同一个域名已经有证书时原地替换内容(证书 ID 不变), 所以已经绑好这张证书的站点不需要任何改动就会立刻用上新证书。跑一百次和跑一次结果相同 —— 这正是 certbot / acme.sh 的 deploy-hook 需要的语义。
| 参数 | 必填 | 说明 |
|---|---|---|
domain | 是 | 证书对应的域名,必须是你名下的站点域名 |
cert_pem | 是 | 证书 PEM。建议传 fullchain(叶子证书 + 中间证书) |
key_pem | 是 | 私钥 PEM。不能带口令加密 |
name | 否 | 证书名,默认用域名 |
入口会先做这几项校验,任何一项不过都返回 400 且不写入任何东西:
| 校验 | 不过时的含义 |
|---|---|
| PEM 能解析、取得到有效期 | 文件损坏或根本不是证书 |
| 证书未过期 | 传上去只会让浏览器报红 |
证书的 SAN 覆盖 domain | 传错文件了(最常见)。只看证书里真实的 SAN,不看你声明的域名 |
| 证书与私钥配对 | 证书和私钥不是一对 |
curl -X POST https://cdn.treeidc.cn/api/certs/upload \
-H "Authorization: Bearer $HSCDN_KEY" \
-H "Content-Type: application/json" \
-d "{\"domain\":\"www.example.com\",
\"cert_pem\":\"$(awk '{printf "%s\\n", $0}' fullchain.pem)\",
\"key_pem\":\"$(awk '{printf "%s\\n", $0}' privkey.pem)\"}"
返回:
{
"ok": true,
"id": "12",
"created": false, // false = 替换了已有的那张(站点绑定不变)
"domain": "www.example.com",
"not_after": "2026-12-08T09:00:00.000Z",
"key_match": "ok", // "unverified" = 非 RSA 证书,本进程验不了配对(不是错误)
"bound_sites": [] // 新建时会把还没绑证书、且域名被覆盖的站点自动绑上
}
接进 certbot / acme.sh
用 deploy-hook,每次续期成功后自动推上来:
# /etc/letsencrypt/renewal-hooks/deploy/hscdn.sh
#!/bin/bash
set -e
D="$RENEWED_DOMAINS"
python3 - "$RENEWED_LINEAGE" "$D" <<'PY'
import json, os, sys, urllib.request
lineage, domain = sys.argv[1], sys.argv[2].split()[0]
body = json.dumps({
"domain": domain,
"cert_pem": open(f"{lineage}/fullchain.pem").read(),
"key_pem": open(f"{lineage}/privkey.pem").read(),
}).encode()
req = urllib.request.Request(
"https://cdn.treeidc.cn/api/certs/upload", data=body, method="POST",
headers={"Authorization": "Bearer " + os.environ["HSCDN_KEY"],
"Content-Type": "application/json"})
print(urllib.request.urlopen(req, timeout=30).read().decode())
PY
为什么不用
POST /api/certs 做续期 ——
那条是「新建一张证书」。拿它做自动续期会每次多出一张同域名的新记录,
而你的站点仍然绑在旧的那张上(自动补绑只管「还没绑证书」的站点)。
于是每一步都返回 200、看起来续期成功了,而边缘会继续递旧证书直到它过期。
自动化请一律用本条 /api/certs/upload。下载证书
GET/api/certs/{id}/download
取回证书的完整材料,用于装到你自己的其它服务器上。
curl -H "Authorization: Bearer $HSCDN_KEY" \
https://cdn.treeidc.cn/api/certs/7/download
{
"ok": true,
"id": 7,
"domain": "www.example.com",
"name": "www.example.com",
"cert_pem": "-----BEGIN CERTIFICATE-----\n…(证书链)…",
"key_pem": "-----BEGIN PRIVATE KEY-----\n…(私钥)…"
}
| 返回 | 含义 |
|---|---|
200 | 成功。key_pem 可能为 null(只上传了证书链、没有私钥的情形) |
404 证书不存在 | ID 不存在或不属于你 —— 两者返回同一句话 |
404 还没有签发成功 | 证书还在签发中或签发失败,暂时没有可下载的内容 |
这个接口会返回私钥明文。每次调用都会在你的操作日志里留下一条
cert_download 记录(见账户信息的操作记录)。请不要把响应写进日志文件或 CI 输出。控制台的「证书管理」页面每行也有下载按钮:
| 情况 | 下载到什么 |
|---|---|
| 证书有私钥(绝大多数) | 一个 <域名>.zip,里面是 <域名>.pem 与 <域名>.key |
| 只有证书链、没有私钥 | 直接下 <域名>.pem |
为什么打包成 zip 而不是分别下两个文件:浏览器会把同一次点击里的第二个下载当作「多文件下载」拦下来(Chrome 会弹权限提示,不点就静默丢弃),结果只拿到证书、拿不到私钥。打成一个包就只有一次下载动作,不会被拦。
中文域名的文件名会用 punycode 形式(如
中文.com → xn--fiq228c.com.zip)—— 直接用中文会被文件系统与浏览器的字符限制压成下划线,不同域名还会撞成同一个文件名。绑定到站点
curl -X POST https://cdn.treeidc.cn/api/sites/123/update \
-H "Authorization: Bearer $HSCDN_KEY" \
-H "Content-Type: application/json" \
-d '{"cert_id": 7, "https_enabled": true}'
只能绑定属于你自己的证书。引用别人的
cert_id 会返回 403。自动签发与手工上传的区别
自动签发(type: "lets") | 手工上传 | |
|---|---|---|
| 续期 | 系统自动续 | 需要你自己换 |
| 到期风险 | 低 | 过期会导致 HTTPS 不可用 |
如果你用的是手工上传的证书,建议把「到期前告警」做进自己的监控:定期拉
/api/certs,对临近到期的证书提前提醒。