Files
atlas-seller-kit/docs/guides/qiniu-https-auto-renewal-tencent-cloud.md
T

1034 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 腾讯云服务器运行七牛图片域名 HTTPS 证书自动续期
> 文档日期:2026-09-30。操作示例:腾讯云 Linux 服务器、Ubuntu 24.04、DNSPod 中国站、七牛 Kodo 私有空间与 CDN 加速域名、Let's Encrypt、acme.sh 3.1.6。
>
> 状态:2026-10-03 已完成 Let's Encrypt staging 与正式证书签发,正式证书的颁发者、有效期和 SAN 已由维护者提供的 OpenSSL 输出确认;七牛 hook 对两个图片域名返回部署成功,两个图片域名的网络检查均返回本次正式证书的颁发者、有效期,且指纹一致。服务器使用 1Panel/OpenResty,Atlas 生产、开发应用尚未部署。定时自动续期仍待验收;网站 HTTPS 接入留待应用部署时完成。现场进度见第 6.2 节、第 7 节、第 8.2 节、第 9.2 节和第 10.2 节。操作系统、DNS 托管商和实际域名需要按现场情况核对。
## 1. 要完成的事情
Atlas 的生产、开发网站及两个环境的图片域名共用一张证书。七牛空间仍按环境隔离,由腾讯云服务器集中执行证书申请、续期和部署。
| 环境 | 七牛空间示例 | 下载域名示例 | 空间访问控制 |
|---|---|---|---|
| 开发联调 | `atlas-dev-assets` | `img-dev.atlas.joeyone.cn` | 私有 |
| 生产 | `atlas-prod-assets` | `img.atlas.joeyone.cn` | 私有 |
### 1.1 四个域名与一张证书
统一把 Atlas 的域名放在 `atlas.joeyone.cn` 下:
| 用途 | 完整域名 | DNSPod 主机记录(区域为 `joeyone.cn`) | 类型 | 目标 |
|---|---|---|---|---|
| 生产网站 | `atlas.joeyone.cn` | `atlas` | A | 生产腾讯云服务器 IP |
| 开发网站 | `dev.atlas.joeyone.cn` | `dev.atlas` | A | 开发服务器 IP;同机部署时可与生产相同 |
| 生产图片 | `img.atlas.joeyone.cn` | `img.atlas` | CNAME | 七牛生产 CDN 分配的目标 |
| 开发图片 | `img-dev.atlas.joeyone.cn` | `img-dev.atlas` | CNAME | 七牛开发 CDN 分配的目标 |
申请一张 SAN 包含以下两项的证书:
```text
atlas.joeyone.cn
*.atlas.joeyone.cn
```
`atlas.joeyone.cn` 覆盖生产网站;`*.atlas.joeyone.cn` 覆盖 `dev`、`img`、`img-dev` 这一级子域名。避免使用 `img.dev.atlas.joeyone.cn`,因为它多了一层,不能被这张通配符证书覆盖。`*.joeyone.cn` 也不能覆盖 `img.atlas.joeyone.cn`。
这张证书只申请、续期一次,但有三个部署目标:腾讯云 Nginx、七牛生产图片域名、七牛开发图片域名。多个域名写入同一张证书不表示它们需要指向同一个服务器或空间。
```text
腾讯云服务器上的证书维护任务
→ 通过 DNSPod API 创建临时 TXT 记录
→ Let's Encrypt 签发 atlas.joeyone.cn + *.atlas.joeyone.cn
→ acme.sh 上传到七牛并更新两个图片域名的证书绑定
→ 导出证书,验证本机 Nginx 配置后重载
生产 / 开发网站 → 腾讯云 Nginx → 各环境应用
Ozon / 图片访问 → 七牛 CDN HTTPS 域名 → 各环境私有空间
```
图片下载流量直接经过七牛,不经过证书维护服务器。网站 A 记录指向腾讯云,图片 CNAME 指向七牛。本手册的 Nginx 自动部署示例假设两个网站均由维护服务器上的 Nginx 提供 HTTPS;如果开发网站在另一台机器,需要为那台机器另加安全分发和配置检查/重载步骤,集中签发不会自动部署到远端服务器。
### 1.2 通配符 DNS 与其他系统
申请通配符证书不要求创建通配符 A 记录。主流程只创建上表四条明确记录,更容易检查各环境指向。
如果希望新建 Atlas 网站子域名默认指向同一服务器,可以另设主机记录 `*.atlas`、类型 A、值为服务器 IP。已有明确的 `img.atlas`、`img-dev.atlas` CNAME 仍优先匹配,不要将图片 CNAME 改成服务器 A 记录。`*.atlas` 也不能替代 `atlas` 本身的记录。[DNSPod 泛解析说明](https://cloud.tencent.com/document/product/302/9073)
其他系统继续使用各自的域名和证书,例如 `git.joeyone.cn`、`img.other-system.joeyone.cn`。新增 Atlas 证书不会修改它们的现有绑定;不需要为了 Atlas 更换全站 `*.joeyone.cn` 证书,也不需要修改主域名的 `*` 解析。
### 两种有效期要分开理解
| 对象 | 用途 | 谁负责更新 |
|---|---|---|
| HTTPS 证书 | 证明网站/图片域名身份并加密连接 | acme.sh 自动续期并部署 |
| 图片签名 URL 的 `e` / `token` | 授权下载某一张私有图片 | Atlas Worker 在发布时生成 |
七牛的“上传自有证书”只上传当前证书,不会自动向 Let's Encrypt 续期或替换域名绑定;自动化依赖本手册的签发、部署 hook 和定时任务。
更换 HTTPS 证书通常不改变图片 URL。自动换证书也不会延长已经到期的图片签名链接。
## 2. 操作前准备
准备以下资源,不需要将真实密钥填进本文或 Git 仓库:
- 一台长期运行、有公网出站能力的腾讯云 Linux 服务器;已有业务服务器也可运行这个小任务。
- 可以 SSH 登录,且当前管理用户具有 `sudo` 权限。
- 你拥有的 `joeyone.cn` 域名,以及上表四个未被其他用途占用的 Atlas 域名。
- 域名实际 DNS 托管商的 API 权限。域名在哪里买、服务器在哪家云、DNS 在哪家托管是三个独立问题。
- 两个七牛私有空间,以及开通七牛 CDN 的账号。
- 能接收故障通知的运维邮箱,以及可用的 SMTP 账号或其他现有告警渠道。
示例替换表:
| 本文示例 | 替换为 |
|---|---|
| `joeyone.cn` | 你的主域名 |
| `atlas.joeyone.cn` | 生产网站及证书主域名 |
| `dev.atlas.joeyone.cn` | 开发网站 |
| `*.atlas.joeyone.cn` | Atlas 一级子域名的通配符 SAN |
| `img.atlas.joeyone.cn` | 生产图片域名 |
| `img-dev.atlas.joeyone.cn` | 开发图片域名 |
| `ops@joeyone.cn` | 能接收通知的邮箱 |
| `atlas-prod-assets` / `atlas-dev-assets` | 你实际创建的空间 |
| `OWNER_UIN` / `DNSPOD_DOMAIN_ID` | 腾讯云主账号 UIN / DNSPod 域名 ID |
确认实际 DNS 服务商:
```bash
dig +short NS joeyone.cn
```
主流程使用腾讯云 DNSPod 新版 API,即 `dns_tencent` 插件及 `Tencent_SecretId`、`Tencent_SecretKey`。如果 NS 和管理控制台不是 DNSPod,先阅读第 15 节,不要申请无关的腾讯云 DNS 密钥。
## 3. 在七牛创建空间和 CDN 下载域名
### 3.1 创建两个私有空间
在七牛控制台的对象存储中创建开发、生产空间:
1. 访问控制选 **私有**。
2. 为两环境分别记录空间名、区域、S3 空间名和 S3 endpoint。
3. 区域按应用访问和目标客户地区选择;Ozon 抓图需要额外验证境外可访问性。
4. 应用使用的存储凭据按环境授权,不在本地开发配置生产空间的读写凭据。
本文的证书自动化任务是集中运维任务,可以维护两个域名。它使用的七牛管理凭据不应下发给浏览器,也不应与本地开发的素材上传权限混为一谈。
### 3.2 绑定「自定义 CDN 加速域名」
分别为两个空间添加域名:
| 控制台项目 | 开发域名 | 生产域名 |
|---|---|---|
| 域名 | `img-dev.atlas.joeyone.cn` | `img.atlas.joeyone.cn` |
| 域名类型 | 自定义 CDN 加速域名 | 自定义 CDN 加速域名 |
| 源站 | 对应开发私有空间 | 对应生产私有空间 |
| 使用场景 | 图片 / 小文件 | 图片 / 小文件 |
| 覆盖范围 | 按实际访问地区选择 | 必须包含 Ozon 实际抓图所需地区并验收 |
本流程使用 CDN 域名。七牛官方 FAQ 明确区分:CDN 可以通过 API 更新证书,Kodo 自定义源站域名目前需要在控制台手动绑定证书。[七牛 SSL FAQ,Q15](https://developer.qiniu.com/fusion/kb/3905/ssl-certificate-faq?category=kb)
涉及中国大陆的域名接入,按七牛控制台要求完成 ICP 备案;不将选择境外节点视为本项目的备案替代方案。
首次创建时如果支持先以 HTTP 创建,可以先完成域名和 CNAME,稍后用正式证书开启 HTTPS。如果界面要求证书才能继续,可先完成第 4~7 节申请证书,再回来绑定。若已有可用 HTTPS 证书,保留原绑定,直到新证书准备好后替换。
### 3.3 配置四个域名的 DNS
先按第 1.1 节为网站配置 `atlas`、`dev.atlas` 的 A 记录;如果服务器有 IPv6 并已提供相应服务,可按实际情况另配 AAAA。
为两个图片子域名添加以下解析,记录值分别从七牛域名配置页复制,不自行猜测:
| 主机记录 | 类型 | 记录值 | 线路 |
|---|---|---|---|
| `img-dev.atlas` | CNAME | 七牛给开发域名提供的目标 | 默认 |
| `img.atlas` | CNAME | 七牛给生产域名提供的目标 | 默认 |
同一个主机记录不要同时保留指向应用服务器的 A / AAAA 记录。若 DNS 服务商具有代理/CDN 开关,本手册路径使用 DNS 解析模式,避免引入第二层代理。
验证:
```bash
dig +short A atlas.joeyone.cn
dig +short A dev.atlas.joeyone.cn
dig +short CNAME img-dev.atlas.joeyone.cn
dig +short CNAME img.atlas.joeyone.cn
```
本证书的两个 SAN 都使用 `_acme-challenge.atlas.joeyone.cn` 验证,在 `joeyone.cn` 区域内的主机记录是 `_acme-challenge.atlas`。同一记录下可能同时出现两个 TXT 值,这是正常的;由 acme.sh 自动创建、保留到验证完成后清理,不手工覆盖其中一个。它和网站 A、图片 CNAME 不冲突。不要提前给挑战记录添加会干扰验证的其他 CNAME。
### 3.4 开启私有空间回源鉴权
在七牛 **CDN → 域名管理 → 对应域名 → 配置 → 访问控制 → 回源鉴权** 中开启。源站为七牛私有 bucket 时,按七牛内置私有空间模式配置,不需要搭建自己的鉴权服务器。[七牛回源鉴权说明](https://developer.qiniu.com/fusion/3930/back-to-the-source-authentication)
两个域名都要配置,并等待控制台显示配置生效。Ozon 发布链路使用七牛私有下载凭证 `e` / `token`:
- 不额外开启一套要求 `sign` / `t` 的 CDN 时间戳防盗链;那是不同签名协议,现有 Worker 不会生成该凭证。
- 不设置仅允许 Atlas 网站 Referer 的白名单;平台抓图可能没有 Referer。
- 不用登录 Cookie、固定浏览器 UA 或未经验证的 Ozon IP 白名单作为抓图前提。
- CDN 缓存可以启用,但缓存命中也必须执行私有访问鉴权。第 11 节会验证这一点。
两种签名协议和互斥限制参见[七牛访问控制 API](https://developer.qiniu.com/fusion/13364/fusion-api-domain-config)。
## 4. 腾讯云服务器准备
### 4.1 确认系统与网络
SSH 登录后先检查:
```bash
cat /etc/os-release
timedatectl status
```
主流程假设 Ubuntu 24.04 + systemd。若为 TencentOS Server、Rocky Linux 等,使用相应包管理器安装 Bash、Git、curl、OpenSSL、CA 根证书、Python 3、DNS 查询工具及 util-linux;其余 systemd 流程相同。不要为执行本手册重装正在运行的服务器。
DNS-01 验证本身不需要开放入站 80 / 443。网站仍需要 Nginx 的 443 入站及现有 HTTP 跳转规则;保留业务服务原有规则。证书任务需要以下出站能力:
| 流量 | 目的 |
|---|---|
| TCP 443 | GitHub、Let's Encrypt、DNSPod API、七牛 API、两个图片域名 |
| UDP / TCP 53 | DNS 查询和验证 |
| 实际 SMTP 端口,例如 TCP 465 / 587 | 第 10 节的通知 |
| 系统原有时间同步流量 | 正确验证签名和证书时间 |
腾讯云 CVM 检查实例关联的安全组及本机防火墙;轻量应用服务器检查其防火墙与实际出站限制。不覆盖现有规则、不放通全部端口。无公网出站的私网实例还需要 NAT 等出口。[腾讯云安全组概述](https://cloud.tencent.com/document/product/213/112610)
### 4.2 安装工具并创建维护账号
以下命令由具有 `sudo` 权限的管理用户执行:
```bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl git openssl dnsutils python3 util-linux nano
sudo useradd --system --create-home \
--home-dir /var/lib/atlas-cert \
--shell /bin/bash atlas-cert
sudo chmod 700 /var/lib/atlas-cert
sudo install -d -m 700 -o atlas-cert -g atlas-cert /var/lib/atlas-cert/src
sudo -iu atlas-cert
```
`useradd` 只在首次执行;如果账号已经存在,检查其 home 和用途后复用,不重复创建。维护账号不需要 sudo 权限。接下来第 5~8 节在这个账号的 Bash 会话里执行。
## 5. 创建 DNSPod 和七牛凭据
### 5.1 腾讯云 DNSPod API 凭据
在腾讯云 **访问管理 CAM → 用户列表** 创建仅用于证书 DNS 验证的子用户,再为该子用户创建 API 密钥。保存 `SecretId` 和 `SecretKey`,不要使用主账号全权限密钥。[子账号 API 密钥管理](https://cloud.tencent.com/document/product/598/37140)
acme.sh 3.1.6 的 `dns_tencent` 脚本调用以下四个 DNSPod 操作:
| 操作 | 用途 |
|---|---|
| `DescribeRecordList` | 探测域名所在 DNS 区域 |
| `DescribeRecordFilterList` | 查找本次验证创建的 TXT |
| `CreateRecord` | 创建验证 TXT |
| `DeleteRecord` | 清理验证 TXT |
参考实现:[dns_tencent 3.1.6](https://github.com/acmesh-official/acme.sh/blob/3.1.6/dnsapi/dns_tencent.sh)。
在 CAM 创建自定义策略并关联子用户。以下 JSON 中的 `OWNER_UIN` 必须换成资源所属主账号 UIN,`DNSPOD_DOMAIN_ID` 换成 DNSPod 控制台/API 获取的域名 ID,不是文字域名:
```json
{
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": [
"dnspod:DescribeRecordList",
"dnspod:DescribeRecordFilterList",
"dnspod:CreateRecord",
"dnspod:DeleteRecord"
],
"resource": ["qcs::dnspod::uin/OWNER_UIN:domain/DNSPOD_DOMAIN_ID"]
}
]
}
```
本例授权 `joeyone.cn` 所在的 domain 资源。DNSPod 当前资源模型按域名授权,上述权限能修改该主域名内的记录,并非仅 `_acme-challenge.atlas` 的 TXT;更严格的隔离可另行采用挑战域委托。资源格式以[腾讯云 DNSPod 授权资源说明](https://cloud.tencent.com/document/product/302/105719)为准,首次验证失败应检查策略和资源归属,不直接改成全账号管理权限。
### 5.2 七牛证书部署凭据
准备可访问目标 CDN 域名的七牛 AK / SK。证书部署需要上传证书及修改这两个 CDN 域名的 HTTPS 配置;单纯的 bucket 上传 token 或 S3 对象写入权限不能代替这些管理权限。
能使用子账号/策略时按当前七牛账号实际支持的权限限制授权范围,并通过首次部署验证。不要假设所有七牛 AK 都是 bucket 级凭据。若开发、生产域名属于不同七牛账号,本手册单组 AK / SK 无法同时更新两者,需分别部署各账号的任务和凭据。
### 5.3 保存维护配置
在 `atlas-cert` 会话执行。先创建文件,再用编辑器填写,真实密钥不作为命令参数或终端历史输入:
```bash
umask 077
test -e /var/lib/atlas-cert/secrets.env || \
install -m 600 /dev/null /var/lib/atlas-cert/secrets.env
nano /var/lib/atlas-cert/secrets.env
```
填写下面的内容,替换所有示例值:
```bash
CERT_EMAIL='ops@joeyone.cn'
CERT_MAIN_DOMAIN='atlas.joeyone.cn'
CERT_WILDCARD_DOMAIN='*.atlas.joeyone.cn'
Tencent_SecretId='替换为证书维护子账号的SecretId'
Tencent_SecretKey='替换为证书维护子账号的SecretKey'
QINIU_AK='替换为七牛证书部署AK'
QINIU_SK='替换为七牛证书部署SK'
QINIU_CDN_DOMAIN='img.atlas.joeyone.cn img-dev.atlas.joeyone.cn'
QINIU_FORCE_HTTPS='true'
```
这是 Bash 配置文件,键名区分大小写。`CERT_WILDCARD_DOMAIN` 必须保留 `*` 并加引号;后面的申请命令也始终给变量加引号,避免 shell 展开。`QINIU_CDN_DOMAIN` 只填两个实际图片域名,用空格分隔,不带 `https://`、路径、通配符或前导点,也不填两个网站域名。
加载配置:
```bash
set -a
. /var/lib/atlas-cert/secrets.env
set +a
```
acme.sh 会在自己的配置目录缓存 DNS/七牛凭据,因此 `secrets.env` 和 `.acme.sh` 整个目录都必须按秘密文件管理;仅删除环境文件并不能撤销缓存凭据。不要执行会完整输出配置的 `--info`,不要启用 `set -x`、不安全调试输出或把原始日志发到公共渠道。
## 6. 安装 acme.sh 并先做 DNS 验证演练
### 6.1 安装固定版本
继续在 `atlas-cert` 会话执行:
```bash
git clone --depth 1 --branch 3.1.6 \
https://github.com/acmesh-official/acme.sh.git \
/var/lib/atlas-cert/src/acme.sh
cd /var/lib/atlas-cert/src/acme.sh
./acme.sh --install \
--home /var/lib/atlas-cert/.acme.sh \
--accountemail "$CERT_EMAIL" \
--no-cron --no-profile
chmod 700 /var/lib/atlas-cert/.acme.sh
/var/lib/atlas-cert/.acme.sh/acme.sh --version
/var/lib/atlas-cert/.acme.sh/acme.sh --set-default-ca --server letsencrypt
```
安装目录与源码目录如果已存在,先检查原部署;不要覆盖其他证书任务。本文明确禁用安装器的默认 cron,第 9 节统一用 systemd timer,避免重复续期。版本升级应在维护窗口检查发行说明后进行,而非每次定时任务自动下载最新代码。
### 6.2 使用 Let's Encrypt 测试环境验证 DNS 权限
测试证书保存到独立目录,不上传七牛,也不用于浏览器/Ozon。它只用于确认 DNS 验证流程可自动运行:
```bash
install -d -m 700 /var/lib/atlas-cert/acme-staging
/var/lib/atlas-cert/.acme.sh/acme.sh \
--home /var/lib/atlas-cert/.acme.sh \
--config-home /var/lib/atlas-cert/acme-staging \
--register-account --server letsencrypt_test -m "$CERT_EMAIL"
/var/lib/atlas-cert/.acme.sh/acme.sh \
--home /var/lib/atlas-cert/.acme.sh \
--config-home /var/lib/atlas-cert/acme-staging \
--issue --server letsencrypt_test --dns dns_tencent \
-d "$CERT_MAIN_DOMAIN" -d "$CERT_WILDCARD_DOMAIN" \
--keylength 2048
```
RSA 2048 用作兼容性示例,避免后续文件目录和 `--ecc` 参数混用。acme.sh 会创建临时 TXT、等待 DNS 验证并清理;首次可能需几分钟。成功后在 DNSPod 检查本次 TXT 已清理,且两个图片 CNAME 保持正确。
### 2026-10-03 现场执行记录
- 服务器上的 `atlas-cert` 账号运行上述 staging `--issue` 命令,于北京时间 2026-10-03 11:02:09 收到证书文件路径及续期窗口输出;这确认测试环境签发已完成,不能视为正式证书或七牛部署验收。
- 测试证书位于 `/var/lib/atlas-cert/acme-staging/atlas.joeyone.cn/`:叶证书 `atlas.joeyone.cn.cer`、私钥 `atlas.joeyone.cn.key`、中间证书 `ca.cer`、完整证书链 `fullchain.cer`。这里只记录路径,不记录证书或私钥内容。
- 本次 staging 输出的 ARI 建议窗口为 `2026-12-01T04:47:36Z`~`2026-12-02T23:58:26Z`,所选下次续期时间为 `2026-12-01T20:38:56Z`;这些是**测试证书**的输出,不能用作正式证书续期日程。
- DNSPod 自动验证已使签发完成;临时 TXT 是否清理、两个图片域名 CNAME 是否保持正确仍需在 DNSPod 复核。随后按第 7 节申请正式证书,并用 `openssl` 检查正式证书的 SAN 和有效期。
- 正式签发和部署前,确认此前截图中暴露的腾讯云及七牛凭据均已停用、更换,并核对 `secrets.env` 与 acme.sh 缓存中的凭据。不要在项目文档或排错日志中保存真实密钥。
如果有 CAA 记录,确保它允许 Let's Encrypt 的 `letsencrypt.org` 签发;不要为排错直接删除业务已有 CAA。DNS-01 验证原理参见[Let's Encrypt 官方说明](https://letsencrypt.org/docs/challenge-types/)。
## 7. 申请正式证书
测试验证通过后,仍在 `atlas-cert` 会话执行。正式流程使用默认配置目录,与测试目录分开:
```bash
/var/lib/atlas-cert/.acme.sh/acme.sh \
--register-account --server letsencrypt -m "$CERT_EMAIL"
/var/lib/atlas-cert/.acme.sh/acme.sh \
--issue --server letsencrypt --dns dns_tencent \
-d "$CERT_MAIN_DOMAIN" -d "$CERT_WILDCARD_DOMAIN" \
--keylength 2048
openssl x509 \
-in "/var/lib/atlas-cert/.acme.sh/$CERT_MAIN_DOMAIN/fullchain.cer" \
-noout -issuer -dates -ext subjectAltName
```
确认 SAN 同时包含 `atlas.joeyone.cn` 与 `*.atlas.joeyone.cn`,证书颁发者属于正式受信任 CA。不要把第 6 节测试证书部署到七牛;不要通过反复 `--force` 申请正式证书来测试定时任务。
证书目录下:
| 文件 | 七牛用途 |
|---|---|
| `fullchain.cer` | 完整证书链,上传为证书内容 |
| `atlas.joeyone.cn.key` | 证书私钥 |
证书私钥仅用于证书部署,不是七牛 SK,也不是 Ozon 店铺 API Key。
### 2026-10-03 正式签发记录
维护者在北京时间 2026-10-03 11:08:13 收到正式证书签发成功输出,证书目录为 `/var/lib/atlas-cert/.acme.sh/atlas.joeyone.cn/`。随后运行本节的 OpenSSL 检查,结果如下;尚未据此确认七牛或网站已使用新证书。
| 项目 | 本次结果 |
|---|---|
| 颁发者 | `C = US, O = Let's Encrypt, CN = YR2` |
| SAN | `*.atlas.joeyone.cn`、`atlas.joeyone.cn` |
| 生效时间 | UTC `2026-10-03 02:09:40`;北京时间 `2026-10-03 10:09:40` |
| 到期时间 | UTC `2027-01-01 02:09:39`;北京时间 `2027-01-01 10:09:39` |
| ARI 建议窗口 | UTC `2026-12-01 04:53:44`~`2026-12-03 00:04:34` |
| acme.sh 所选下次续期时间 | UTC `2026-12-01 20:51:08`;北京时间 `2026-12-02 04:51:08` |
ARI 时间是客户端本次选择的续期时间,实际自动执行仍依赖第 9 节的定时任务。下一步为第 8.1 节首次上传和启用七牛 HTTPS。
## 8. 首次开启七牛 HTTPS 并配置自动部署
### 8.1 首次开启 HTTPS
内置七牛 hook 调用修改 HTTPS 配置接口。因此首次先在控制台将两个 CDN 域名的 HTTPS 都启用,再交给 hook 维护;避免域名尚未开启 HTTPS 时直接调用更新接口失败。
1. 用 SSH/SFTP 客户端从服务器安全取回第 7 节的 `fullchain.cer` 和域名 `.key` 文件。
2. 在七牛 **SSL 证书服务 → 证书管理 → 上传自有证书** 中上传完整链和私钥;使用 PEM/Nginx 格式。
3. 在七牛 **CDN → 域名管理 → 对应域名 → HTTPS 配置**,两个域名都选择刚上传的证书并开启 HTTPS。
4. 如需 HTTP 强制跳转 HTTPS,保持与 `QINIU_FORCE_HTTPS='true'` 一致。
5. 等待控制台显示部署完成,检查两个域名的证书;临时下载到本机的私钥按秘密文件管理,传递结束后清理临时副本。
取回文件时可复用已有管理员 SSH 连接的 SFTP 文件面板。本次服务器使用已有的 `root` 管理连接访问证书目录;终端内的 `sudo -iu atlas-cert` 不会改变文件面板的登录身份。不要为下载文件给维护账号新增 SSH 密码或放宽证书目录权限。
1. 打开客户端的服务器文件面板,将远端路径设为 `/var/lib/atlas-cert/.acme.sh/atlas.joeyone.cn/`。`.acme.sh` 是隐藏目录,优先直接输入完整路径,或启用“显示隐藏文件”。
2. 只下载 `fullchain.cer` 和 `atlas.joeyone.cn.key` 到本机专用临时目录,保留服务器上的原文件。不要下载整个 `.acme.sh`,其中包含账户与 DNS/API 凭据。
3. 在七牛上传这两个文件;若页面要求填写 PEM 文本,在本机文本编辑器打开对应文件,完整复制各自的 BEGIN/END 区块。私钥不发送到聊天、项目仓库或公共日志。
4. 两个域名证书验收完成后,清理本机临时副本;服务器原文件继续供后续自动续期与部署使用。
如果两个域名原本就已开启 HTTPS,先上传并切换到新证书即可。不要先删除旧证书。
### 8.2 执行一次自动部署
在 `atlas-cert` 会话执行:
```bash
set -a
. /var/lib/atlas-cert/secrets.env
set +a
/var/lib/atlas-cert/.acme.sh/acme.sh \
--deploy -d "$CERT_MAIN_DOMAIN" --deploy-hook qiniu
```
该步骤会上传证书,并逐一更新 `QINIU_CDN_DOMAIN` 中的两个域名,同时保存部署 hook 供续期时调用。确认输出包含两个域名的部署成功记录,并在七牛控制台复核。首次手工上传和 hook 上传可能留下两条证书记录,这是正常的;验收后按引用情况清理旧条目。
2026-10-03 现场记录:维护者以 `atlas-cert` 执行上述部署命令,于北京时间 11:26:12 收到 `img.atlas.joeyone.cn` 和 `img-dev.atlas.joeyone.cn` 各自的 `certificate has been deployed successfully`,最终输出 `Success`。随后两个图片域名的网络证书检查结果已记录于第 10.2 节;自动续期执行仍需完成第 9 节的 systemd timer。日志中的 `updating domain atlas.joeyone.cn` 使用证书主域名标识本次证书,不能据此判断网站 Nginx 已更新。
参考:[acme.sh 七牛部署实现](https://github.com/acmesh-official/acme.sh/blob/3.1.6/deploy/qiniu.sh)、[七牛证书管理与部署 FAQ](https://developer.qiniu.com/fusion/3905/ssl-certificate-faq)。
收紧本地文件权限:
```bash
find /var/lib/atlas-cert/.acme.sh -type d -exec chmod 700 {} +
find /var/lib/atlas-cert/.acme.sh -type f -exec chmod go-rwx {} +
find /var/lib/atlas-cert/acme-staging -type d -exec chmod 700 {} +
find /var/lib/atlas-cert/acme-staging -type f -exec chmod go-rwx {} +
```
### 8.3 导出同一张证书给本机 Nginx
继续在 `atlas-cert` 会话执行:
```bash
install -d -m 700 /var/lib/atlas-cert/nginx-export
/var/lib/atlas-cert/.acme.sh/acme.sh \
--install-cert -d "$CERT_MAIN_DOMAIN" \
--key-file /var/lib/atlas-cert/nginx-export/privkey.pem \
--fullchain-file /var/lib/atlas-cert/nginx-export/fullchain.pem
chmod 600 /var/lib/atlas-cert/nginx-export/privkey.pem \
/var/lib/atlas-cert/nginx-export/fullchain.pem
```
`--install-cert` 会保存导出路径,续期时自动更新这两个文件。Nginx 不直接引用 acme.sh 内部文件;本流程由第 9.2 节的 root 管理脚本复制到 `/etc/nginx/tls/atlas/current/`、执行 `nginx -t` 并重载,因此这里没有设置 `--reloadcmd`。必须完成第 9 节,只有导出证书不能让 Nginx 使用新证书。[acme.sh 证书安装说明](https://github.com/acmesh-official/acme.sh#3-install-the-cert-to-apachenginx-etc)
2026-10-03 现场记录:北京时间 11:27:26,维护者收到两项 `Installing` 输出,证书已导出到以下路径;Nginx 接入仍需完成第 9.2 节。
| 文件用途 | 实际导出路径 |
|---|---|
| 证书私钥 | `/var/lib/atlas-cert/nginx-export/privkey.pem` |
| 完整证书链 | `/var/lib/atlas-cert/nginx-export/fullchain.pem` |
维护账号不获得通用 sudo 权限。证书申请和七牛 API 调用以 `atlas-cert` 执行;复制到系统证书目录与 Nginx 重载由 systemd 的 root 步骤完成。root 脚本不加载 `secrets.env`,只读取固定路径的证书文件。
## 9. 配置 systemd 定时续期
### 9.1 创建续期入口
在 `atlas-cert` 会话创建脚本:
```bash
cat > /var/lib/atlas-cert/renew-qiniu.sh <<'SH'
#!/usr/bin/env bash
set -euo pipefail
umask 077
# 统一加载维护凭据,定时任务不依赖人工登录会话。
set -a
. /var/lib/atlas-cert/secrets.env
set +a
# 与手工维护共用锁文件,避免并发操作同一套证书。
exec 9>/var/lib/atlas-cert/renew.lock
flock -n 9 || exit 0
/var/lib/atlas-cert/.acme.sh/acme.sh \
--cron --home /var/lib/atlas-cert/.acme.sh
SH
chmod 700 /var/lib/atlas-cert/renew-qiniu.sh
# 首次由维护账号创建锁,避免 root 部署脚本先创建后导致维护账号无法写入。
test -e /var/lib/atlas-cert/renew.lock || \
install -m 600 /dev/null /var/lib/atlas-cert/renew.lock
bash -n /var/lib/atlas-cert/renew-qiniu.sh
exit
```
`exit` 返回原管理账号。
### 9.2 创建 Nginx 部署脚本并完成首次接入
本节按网站服务选择接入方式:当前现场使用 **9.2.1 的 1Panel 分支**;9.2.2 仅适用于已经安装并运行宿主机 systemd Nginx 的服务器。
> 2026-10-03 现场环境确认:服务器为 1Panel `v2.0.11 stable`,网站服务容器为 `1Panel-openresty-C1B1`,镜像为 `1panel/openresty:1.27.1.2-2-3-focal`。维护者执行本节原脚本时,证书检查与宿主机 `nginx -t` 通过,但 `systemctl reload nginx` 报 `nginx.service is not active, cannot reload`;本节接入未完成。应用部署后应使用 1Panel 配置网站 HTTPS,证书续期后的同步与容器重载需按实际网站配置适配。当前可先启用第 9.3 节不含网站部署步骤的基础定时任务,待适配验收后补上 `ExecStartPost`。
>
> 1Panel v2.0.11 从服务器路径导入证书时会读取并保存内容,不能把导入操作视为持续引用源文件;acme.sh 更新 `nginx-export` 后仍需主动同步到 1Panel 网站使用的证书。依据:[v2.0.11 证书导入实现](https://github.com/1Panel-dev/1Panel/blob/v2.0.11/agent/app/service/website_ssl.go)。
#### 9.2.1 当前现场:1Panel/OpenResty,应用尚未部署
2026-10-03,维护者通过 `docker inspect` 与容器内 `nginx -T` 确认以下挂载关系:
| 宿主机路径 | OpenResty 容器路径 | 用途 |
|---|---|---|
| `/opt/1panel/www` | `/www` | 网站目录及各网站证书文件 |
| `/opt/1panel/www/conf.d` | `/usr/local/openresty/nginx/conf/conf.d` | 网站配置 |
| `/opt/1panel/apps/openresty/openresty/conf/ssl` | `/usr/local/openresty/nginx/conf/ssl` | 默认站点证书 |
现有网站包括 `git.joeyone.cn`、`pipi-assets.joeyone.cn`、`test.joeyone.cn`、`wmd.joeyone.cn`,分别使用 `/www/sites/<网站域名>/ssl/fullchain.pem` 和 `privkey.pem`。输出中没有 `atlas.joeyone.cn` 或 `dev.atlas.joeyone.cn` 的网站配置;维护者确认两套 Atlas 应用尚未部署。
**当前可先完成图片域名验收及第 9.3 节的七牛续期任务。** 网站应用未部署前,无需创建占位反向代理,也不把项目本地开发端口当作云端应用地址。现有网站及默认站点证书继续由各自配置维护。
应用部署时按以下顺序接入:
1. 部署生产、开发应用,确认各自实际访问地址、监听端口及 OpenResty 容器可达性。
2. 在 1Panel 的“网站 → 证书”上传正式证书,使用“本地导入/服务器路径”时填写以下**宿主机路径**;若界面要求文件内容,则在受控终端读取后粘贴,不将私钥发送到聊天或提交到仓库。
- 证书:`/var/lib/atlas-cert/nginx-export/fullchain.pem`
- 私钥:`/var/lib/atlas-cert/nginx-export/privkey.pem`
3. 在 1Panel 新建 `atlas.joeyone.cn` 和 `dev.atlas.joeyone.cn` 网站,配置各自真实应用的反向代理,并在 HTTPS 设置中选择导入的同一张 Atlas 证书。
4. 再次检查网站配置中的实际证书路径,并从网络验证两个网站证书和环境路由。文件目录可能由网站创建方式决定,不预先假设一定为 `/www/sites/atlas.joeyone.cn`。
5. 接入续期后同步:优先通过与 1Panel 版本匹配的证书更新接口更新已导入证书,使面板记录与关联网站一起更新;验收后再接入第 9.3 节的 `ExecStartPost`。当前未确认面板接口配置、证书 ID 和新网站实际路径,自动同步尚未完成。
仅手工导入证书不会完成网站自动续期部署。以后只替换网站目录里的 PEM 文件也可能造成面板保存的证书内容与实际文件不一致,后续面板操作可能重新写入旧内容,应完成上述同步适配。
#### 9.2.2 宿主机 systemd Nginx 分支
以下示例仅用于宿主机 Nginx。当前 1Panel 现场跳过本小节;不要为使旧脚本成功而启动另一个宿主机 Nginx。
以下由管理账号执行。脚本及系统证书目录由 root 管理,维护账号不能修改部署脚本。每次部署使用独立版本目录,只有配置检查和重载成功后才记录成功指纹;失败则恢复原目录链接,供下次重试:
```bash
sudo install -d -m 700 -o root -g root /etc/nginx/tls/atlas
sudo tee /usr/local/sbin/atlas-cert-install-nginx > /dev/null <<'SH'
#!/usr/bin/env bash
set -euo pipefail
umask 077
# 与申请、手工部署共用锁,避免读取尚未导出完的证书。
exec 9>/var/lib/atlas-cert/renew.lock
flock 9
cert_root=/etc/nginx/tls/atlas
export_root=/var/lib/atlas-cert/nginx-export
release=$(mktemp -d "$cert_root/release.XXXXXXXX")
old_target=$(readlink "$cert_root/current" || true)
activated=0
finished=0
# 失败时恢复上一次目录,不把未成功部署的指纹标记为完成。
cleanup() {
if [[ "$finished" != 1 ]]; then
if [[ "$activated" == 1 ]]; then
if [[ -n "$old_target" ]]; then
ln -s "$old_target" "$cert_root/.rollback"
mv -Tf "$cert_root/.rollback" "$cert_root/current"
else
rm -f "$cert_root/current"
fi
fi
rm -rf "$release"
fi
}
trap cleanup EXIT
install -m 600 "$export_root/fullchain.pem" "$release/fullchain.pem"
install -m 600 "$export_root/privkey.pem" "$release/privkey.pem"
openssl x509 -in "$release/fullchain.pem" -noout -checkend 0
for cert_domain in atlas.joeyone.cn dev.atlas.joeyone.cn img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
openssl x509 -in "$release/fullchain.pem" -noout -checkhost "$cert_domain"
done
# 验证公钥一致,不输出私钥或证书原文。
cert_pub=$(openssl x509 -in "$release/fullchain.pem" -pubkey -noout |
openssl pkey -pubin -outform DER | sha256sum)
key_pub=$(openssl pkey -in "$release/privkey.pem" -pubout -outform DER | sha256sum)
[[ "$cert_pub" == "$key_pub" ]]
fingerprint=$(openssl x509 -in "$release/fullchain.pem" -noout -fingerprint -sha256)
if [[ -L "$cert_root/current" && -f "$cert_root/deployed-fingerprint" ]] &&
[[ "$(cat "$cert_root/deployed-fingerprint")" == "$fingerprint" ]]; then
rm -rf "$release"
finished=1
exit 0
fi
ln -s "$(basename "$release")" "$cert_root/.next"
mv -Tf "$cert_root/.next" "$cert_root/current"
activated=1
/usr/sbin/nginx -t
/usr/bin/systemctl reload nginx
printf '%s\n' "$fingerprint" > "$cert_root/deployed-fingerprint"
finished=1
SH
sudo chown root:root /usr/local/sbin/atlas-cert-install-nginx
sudo chmod 700 /usr/local/sbin/atlas-cert-install-nginx
sudo bash -n /usr/local/sbin/atlas-cert-install-nginx
sudo /usr/local/sbin/atlas-cert-install-nginx
```
第一次运行会建立 `current` 目录链接,并检查/重载已有 Nginx。随后在**现有**生产和开发网站 `server` 配置中分别设置如下 TLS 指令,保留各自原有 `location`、反向代理、访问控制和日志配置。以下是待合并的片段,不是完整站点配置:
```nginx
# 生产网站的 TLS 配置,业务路由沿用现有生产配置。
listen 443 ssl;
server_name atlas.joeyone.cn;
ssl_certificate /etc/nginx/tls/atlas/current/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/atlas/current/privkey.pem;
```
```nginx
# 开发网站使用同一张证书,业务路由指向开发环境。
listen 443 ssl;
server_name dev.atlas.joeyone.cn;
ssl_certificate /etc/nginx/tls/atlas/current/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/atlas/current/privkey.pem;
```
同一 IP 可承载两个网站,Nginx 按域名选择对应的 `server`;两个网站的 upstream 和环境配置仍分别维护。不要仅因证书共用就让开发流量进入生产应用。[Nginx HTTPS 配置说明](https://nginx.org/en/docs/http/configuring_https_servers.html)
配置编辑完成后,由管理账号执行:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
旧证书版本目录暂时保留作为回滚材料。新证书在四个域名验收通过后,可由管理账号清理不再需要的旧版本,保留当前版本和仍有效的回滚版本。目录含私钥,权限保持 700/600。
### 9.3 配置定时任务
当前 1Panel 现场尚未部署 Atlas 网站,先创建**证书续期、七牛部署及 PEM 导出**的定时任务。以下基础服务不调用第 9.2.2 节的宿主机 Nginx 脚本,因此不会因 `nginx.service` 未运行而报错;网站自动同步仍须在应用部署后单独接入。
以下由管理账号执行,创建 systemd 单元。如果先前已创建含 `ExecStartPost=/usr/local/sbin/atlas-cert-install-nginx` 的服务,使用此基础单元覆盖后重新加载:
```bash
sudo tee /etc/systemd/system/atlas-qiniu-cert-renew.service > /dev/null <<'UNIT'
[Unit]
Description=Atlas shared certificate renewal and deployment
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/var/lib/atlas-cert
UMask=0077
ExecStart=/usr/sbin/runuser -u atlas-cert -- /var/lib/atlas-cert/renew-qiniu.sh
TimeoutStartSec=30min
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
UNIT
sudo tee /etc/systemd/system/atlas-qiniu-cert-renew.timer > /dev/null <<'UNIT'
[Unit]
Description=Daily Atlas Qiniu certificate check
[Timer]
OnCalendar=*-*-* 03:20:00 Asia/Shanghai
RandomizedDelaySec=30min
Persistent=true
Unit=atlas-qiniu-cert-renew.service
[Install]
WantedBy=timers.target
UNIT
sudo systemd-analyze verify \
/etc/systemd/system/atlas-qiniu-cert-renew.service \
/etc/systemd/system/atlas-qiniu-cert-renew.timer
sudo systemctl daemon-reload
sudo systemctl enable --now atlas-qiniu-cert-renew.timer
sudo systemctl start atlas-qiniu-cert-renew.service
sudo systemctl list-timers atlas-qiniu-cert-renew.timer --all
sudo systemctl show atlas-qiniu-cert-renew.service \
-p Result -p ExecMainStatus
sudo journalctl -u atlas-qiniu-cert-renew.service -n 50 --no-pager
```
该基础服务由 root 启动,`ExecStart` 通过 `runuser` 降权执行 acme.sh,维护账号仍没有 sudo 权限。基础服务成功只证明当前续期检查流程完成,不证明网站证书已同步;还须监控 service 失败和两个图片域名实际证书。
每天北京时间 03:20 后随机延迟最多 30 分钟执行。每天检查不等于每天重新申请;acme.sh 按证书续期窗口决定是否申请,并更新保存的证书导出路径、调用七牛部署 hook。服务器关机时不会运行,`Persistent=true` 会在恢复后补跑错过的定时检查。
**已有宿主机 Nginx 网站的服务器**:先完成第 9.2.2 节验收,再在服务 `[Service]` 中添加 `ExecStartPost=/usr/local/sbin/atlas-cert-install-nginx`,重新执行单元校验、`daemon-reload` 和手工触发 service。**1Panel 现场**:完成第 9.2.1 节的网站及证书同步适配后,才添加对应的部署脚本;不能使用宿主机 Nginx 脚本替代。带 `ExecStartPost` 时,只有前置续期流程成功才执行网站部署,网站部署失败会使整个 service 失败;应同时监控网站证书和任务状态。
刚申请的证书显示跳过续期是正常的。`Type=oneshot` 完成后显示 inactive/dead 也是正常的,检查 timer 为 active、任务 `Result=success`、`ExecMainStatus=0`。日志只在受控终端查看,不导出带凭据的调试日志。
## 10. 配置通知与独立到期检查
### 10.1 配置续期/部署邮件通知
在管理账号执行 `sudo -iu atlas-cert`,编辑 `/var/lib/atlas-cert/secrets.env`,追加以下值。示例采用邮箱服务商的 SMTP 465;主机、端口及授权码按实际邮箱说明填写:
```bash
SMTP_BIN='/usr/bin/curl'
SMTP_HOST='smtp.your-mail-provider.example'
SMTP_PORT='465'
SMTP_SECURE='ssl'
SMTP_USERNAME='cert-bot@example.com'
SMTP_PASSWORD='替换为SMTP授权码'
SMTP_FROM='cert-bot@example.com'
SMTP_TO='ops@joeyone.cn'
```
如使用 587/STARTTLS,改为 `SMTP_PORT='587'`、`SMTP_SECURE='tls'`。在服务器/安全组出站规则中允许对应端口,不使用可能被云平台限制的 25 端口。
```bash
set -a
. /var/lib/atlas-cert/secrets.env
set +a
/var/lib/atlas-cert/.acme.sh/acme.sh \
--set-notify --notify-hook smtp --notify-level 2 --notify-mode 0
exit
```
这条命令会发送配置测试邮件给 `SMTP_TO`,应确认实际收到。级别 2 用于报告错误和实际续期结果,日常跳过无需每次发信。已有可靠通知渠道时可替换为 acme.sh 支持的 hook,但部署失败必须能通知到维护者。参数见[acme.sh 通知说明](https://github.com/acmesh-official/acme.sh/wiki/notify)及[SMTP hook 实现](https://github.com/acmesh-official/acme.sh/blob/master/notify/smtp.sh)。
### 10.2 从网络检查四个域名实际证书
本地证书更新成功不等于所有 CDN 节点完成更新。当前两个网站尚未部署,先使用 OpenSSL 检查两个图片域名。应用上线且网站 HTTPS 配置完成后,将循环域名扩展为 `atlas.joeyone.cn dev.atlas.joeyone.cn img.atlas.joeyone.cn img-dev.atlas.joeyone.cn`,再验收四个域名。
仅复制代码块内的命令,不复制 Markdown 的三个反引号。若终端显示连续的 `>` 续行提示,先按 `Ctrl+C` 返回正常提示符,再重新粘贴命令。
```bash
set -o pipefail
for cert_domain in img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
printf '%s\n' "$cert_domain"
timeout 20 openssl s_client \
-connect "$cert_domain:443" -servername "$cert_domain" \
-verify_hostname "$cert_domain" -verify_return_error </dev/null |
openssl x509 -noout -issuer -dates -fingerprint -sha256
done
```
四个域名的网络证书指纹应在部署传播完成后与正式 `fullchain.cer` 的叶证书指纹一致。私有图片返回 401/403 不能据此判断 TLS 失败;TLS 握手验证可以独立于图片授权完成。
2026-10-03 现场网络检查记录(维护者从服务器执行,检查的具体时刻未提供):
| 项目 | 两个图片域名的结果 |
|---|---|
| 域名 | `img.atlas.joeyone.cn`、`img-dev.atlas.joeyone.cn` |
| 颁发者 | `C = US, O = Let's Encrypt, CN = YR2` |
| 有效期开始 | `2026-10-03 02:09:40 GMT`(北京时间 10:09:40) |
| 有效期结束 | `2027-01-01 02:09:39 GMT`(北京时间 10:09:39) |
| SHA-256 指纹 | `E0:EB:B3:0B:A7:3A:B0:12:10:5F:EC:8D:F0:1E:99:F5:0D:09:9C:CA:29:27:BF:8A:B0:4C:74:48:04:44:D0:00` |
两域名返回的指纹相同,颁发者和有效期与第 7 节本次正式签发结果一致,确认本次探测到的 CDN 节点已提供正式证书;这不代表所有地区节点都已完成传播,也不代表定时续期任务已验收。
同次检查中,`atlas.joeyone.cn` 与 `dev.atlas.joeyone.cn` 返回 `Could not read certificate from <stdin>` / `Unable to load certificate`,表示管道没有获得可解析的证书。由于当时隐藏了 `s_client` 的错误输出,无法仅凭这一结果区分 DNS、连接或 TLS 失败;结合应用尚未部署的现场状态,两个网站暂列待部署后验收,无需重新签发证书。
### 10.3 给四个域名分别加独立监控
在现有外部监控系统或支持证书检查的云监控产品中,当前先把两个图片域名纳入 TLS 到期监控;网站上线后补齐两个网站域名。同一张证书也可能在某个部署目标更新失败:
- 证书剩余不足 21 天告警,不足 7 天升级处理。
- TLS 握手、证书链或域名匹配失败立即告警。
- 日常任务长期没有运行,单独按任务执行时间监控。
- 私有资源 HTTP 探测需使用受控授权;普通无签名 403 不是资源故障。
首次手工检查到期阈值可使用以下命令;它只检查当前网络节点,不能代替多地区监控。当前仅检查图片域名,网站上线后再扩展域名列表:
```bash
set -o pipefail
for cert_domain in img.atlas.joeyone.cn img-dev.atlas.joeyone.cn; do
if timeout 20 openssl s_client \
-connect "$cert_domain:443" -servername "$cert_domain" \
-verify_hostname "$cert_domain" -verify_return_error </dev/null 2>/dev/null |
openssl x509 -noout -checkend 1814400; then
printf '%s:当前证书超过21天有效\n' "$cert_domain"
else
printf '%s:证书不足21天或TLS检查失败,请处理\n' "$cert_domain" >&2
fi
done
```
## 11. 验证私有图片链接和缓存鉴权
为每个空间上传一张没有敏感内容的测试 JPEG,例如对象键 `ops/cert-smoke.jpg`。使用浏览器或七牛工具上传即可,保留该测试对象供验收;不要覆盖业务图片。
先确认第 10.2 节 TLS 验证通过,再执行图片访问测试。签名 URL、AK/SK 都不直接打印到终端。以下脚本在 `atlas-cert` 会话运行,当前配置假设管理 AK/SK 同时具有这两个空间的私有下载签名能力;若使用了仅能管理证书的凭据,请改用目标空间的授权下载凭据执行本步骤,不扩大证书维护凭据权限。
```bash
set -a
. /var/lib/atlas-cert/secrets.env
set +a
python3 <<'PY'
import base64
import hashlib
import hmac
import os
import time
import urllib.error
import urllib.parse
import urllib.request
def status(url):
try:
with urllib.request.urlopen(url, timeout=20) as response:
response.read(1)
return response.status
except urllib.error.HTTPError as error:
return error.code
except Exception as error:
# 网络异常可能含完整签名链接,只输出异常类型。
raise SystemExit(f"网络检查失败:{type(error).__name__}") from None
def signed(base, deadline):
url = f"{base}?e={deadline}"
digest = hmac.new(
os.environ["QINIU_SK"].encode(), url.encode(), hashlib.sha1
).digest()
token = base64.urlsafe_b64encode(digest).decode()
return f"{url}&token={os.environ['QINIU_AK']}:{token}"
for domain in os.environ["QINIU_CDN_DOMAIN"].split():
base = f"https://{domain}/{urllib.parse.quote('ops/cert-smoke.jpg', safe='/')}"
unsigned = status(base)
valid_url = signed(base, int(time.time()) + 30)
valid = status(valid_url)
cached_unsigned = status(base)
# 对同一个已访问链接等待过期,验证缓存命中没有绕过授权。
time.sleep(32)
expired = status(valid_url)
print(f"{domain}: 无签名={unsigned}, 有效签名={valid}, "
f"缓存后无签名={cached_unsigned}, 过期签名={expired}")
if not (unsigned in (401, 403) and valid == 200
and cached_unsigned in (401, 403) and expired in (401, 403)):
raise SystemExit("私有访问验收未通过,请检查对象和回源鉴权")
PY
```
预期:有效签名 200,无签名、缓存后无签名和已过期签名均被拒绝。有效链接得到 404 通常表示测试对象未上传到对应空间或对象键不同;得到 403 应检查签名密钥、域名、时钟和访问控制。
这个测试只验证你的网络到七牛的访问。还需从另一网络/境外环境核验,最终以测试商品提交后的 Ozon 图片抓取与处理结果验收为准。[七牛私有下载凭证](https://developer.qiniu.com/kodo/1656/download-private)
## 12. 与 Atlas 应用配置对接
### 12.1 应用配置与维护配置分开
证书任务使用 `QINIU_AK` / `QINIU_SK`;应用使用以下既有环境项。两者职责和权限不同,不把证书维护文件放到应用仓库或前端构建环境。
以华东浙江区域的生产空间为示例,实际 endpoint 和 S3 空间名从七牛控制台获取:
```dotenv
OBJECT_STORAGE_PROVIDER=qiniu_kodo
OBJECT_STORAGE_ENDPOINT=https://s3.cn-east-1.qiniucs.com
OBJECT_STORAGE_REGION=cn-east-1
OBJECT_STORAGE_BUCKET=atlas-prod-assets
OBJECT_STORAGE_FORCE_PATH_STYLE=true
OBJECT_STORAGE_ACCESS_KEY=填写生产对象存储凭据
OBJECT_STORAGE_SECRET_KEY=填写生产对象存储凭据
OZON_IMAGE_DOWNLOAD_ORIGIN=https://img.atlas.joeyone.cn
OZON_IMAGE_URL_TTL_SECONDS=604800
QINIU_ACCESS_KEY=填写生产私有下载签名凭据
QINIU_SECRET_KEY=填写生产私有下载签名凭据
OZON_PUBLISH_ENABLED=auto
```
开发联调配置替换为开发空间、开发凭据和 `https://img-dev.atlas.joeyone.cn`。`OZON_IMAGE_DOWNLOAD_ORIGIN` 同时用于 API 的图片预览、附件下载和 Worker 向 Ozon 交付图片;七牛私有空间的链接由服务端按用途签发短期下载凭证。上传仍使用 `OBJECT_STORAGE_ENDPOINT`。数据库素材身份继续记录 `storage_provider + bucket + object_key`,不保存签名 URL 作为素材主数据。
`OBJECT_STORAGE_BUCKET` 当前用于 S3 操作,因此应填七牛显示的 **S3 空间名**;如它与普通空间名不同,不可混用。自定义 CDN 域名绑定相应的实际空间。[七牛 S3 服务域名说明](https://developer.qiniu.com/kodo/4088/s3-access-domainname)
### 12.2 接入校验事项
证书配置完成后,还需确认应用与存储空间的联通性:
1. API 和 Worker 的 S3 客户端读取 `OBJECT_STORAGE_REGION`;它必须与七牛空间的区域及 endpoint 匹配。API 的七牛预览与附件下载使用绑定的 HTTPS 图片域名签发私有下载链接。
2. 需验证现有 S3 上传、HEAD/GET、图片域名私有下载和浏览器直传的七牛兼容性;上传 endpoint 的 CORS 要允许实际 Atlas 开发/生产网站 Origin、PUT/GET/HEAD 及预检请求中实际使用的头,不能拿 CDN 配置代替上传 CORS。
3. 发布代码拒绝 `local_minio` 或与当前 bucket 不一致的素材。原 MinIO 图片需要复制、校验并更新素材定位,或重新导入,不能仅切换配置。
4. 原有架构文件仍以普通本地开发使用 MinIO 为基线。本手册描述待实施的七牛联调操作,不自行宣布该基线已被替换;正式切换时应同步更新上游架构和交接文档。
5. 生产 `OZON_PUBLISH_ENABLED=auto` 默认不启用发布。完成七牛延迟抓图及真实店铺其他前置验收后,另行显式启用,不在证书维护步骤中顺便开启生产上架。
图片链接默认 7 天是现有应用设置,不是 Ozon 官方有效期保证。CDN 对 `publishing/` 的对象缓存可以存在,但空间生命周期不能提前删除仍在发布/重试/对账中的对象。
## 13. 日常维护、换密钥与回滚
### 查看任务
管理账号执行:
```bash
sudo systemctl list-timers atlas-qiniu-cert-renew.timer --all
sudo systemctl show atlas-qiniu-cert-renew.service -p Result -p ExecMainStatus
sudo journalctl -u atlas-qiniu-cert-renew.service --since '2 days ago' --no-pager
```
### 重试部署现有证书
如果正式证书已经签发,只有七牛部署失败,不需要再次申请证书:
```bash
sudo -iu atlas-cert
set -a
. /var/lib/atlas-cert/secrets.env
set +a
flock /var/lib/atlas-cert/renew.lock \
/var/lib/atlas-cert/.acme.sh/acme.sh \
--deploy -d "$CERT_MAIN_DOMAIN" --deploy-hook qiniu
exit
```
七牛重试成功后,运行以下命令也检查/部署 Nginx,不重新申请证书:
```bash
sudo systemctl start atlas-qiniu-cert-renew.service
```
如果导出文件丢失,先在 `atlas-cert` 会话重新执行第 8.3 节的 `--install-cert`。部署之后按第 10.2 节再次检查网络实际证书,七牛配置传播期间部分节点可能仍返回旧证书。
### 换 DNS/七牛密钥
1. 保留旧密钥,在控制台创建权限正确的新密钥。
2. 停止 timer,并检查/等待正在运行的 service 完成。
3. 编辑 `secrets.env`;DNS 凭据还可能缓存为 `account.conf` 中的 `SAVED_Tencent_SecretId` / `SAVED_Tencent_SecretKey`,七牛 hook 凭据缓存于域名配置。部分缓存会优先于新的 shell 环境值,不能只改一个文件就宣告完成轮换。
4. 在受控编辑器中同步缓存配置或删除旧的对应缓存键,让下次调用重新保存;不要输出整份配置到日志。备份也必须保留 600 权限。
5. 七牛凭据用上一段的 `--deploy` 验证;DNS 新凭据用独立 staging 配置目录重新做 DNS 验证,正常生产续期时确认保存新值。
6. 再次启动 timer,确认任务/通知和网络证书正常后,禁用并删除旧密钥。
```bash
sudo systemctl stop atlas-qiniu-cert-renew.timer
sudo systemctl is-active atlas-qiniu-cert-renew.service
```
如果 service 正在执行,先等待完成,不同时手工改凭据。轮换结束:
```bash
sudo systemctl start atlas-qiniu-cert-renew.timer
```
### 回滚
保留原有效证书及七牛证书 ID、Nginx 旧版本目录,直到新证书在四个域名、多网络测试均通过。发生新证书部署问题时,可暂停 timer,在七牛控制台将受影响域名重新绑定原有效证书;确认网络实际证书后排查。Nginx 需要回滚时,由管理账号将 `/etc/nginx/tls/atlas/current` 链接恢复到原有效版本,执行 `nginx -t` 后重载,并重新核验网站证书;同步移除 `deployed-fingerprint`,修复后才恢复定时任务,避免记录与实际部署不一致。原证书已经过期时不能回滚到它,应部署新正式证书。
### 备份与迁移维护服务器
加密备份整个 `/var/lib/atlas-cert`(含 ACME 账号、证书、私钥、缓存凭据)、`/etc/nginx/tls/atlas`、root 部署脚本、两个网站的 Nginx 配置和两个 systemd 单元,限制备份读取权限。换机器时先在旧机停 timer,恢复目录与账号权限,再在新机部署单元和做一次续期检查、证书部署/网络检查,避免两台机器同时操作同一证书。
## 14. 常见故障
| 现象 | 优先检查 | 处理 |
|---|---|---|
| DNS 验证失败 / 找不到根域 | 实际 NS、DNSPod 账号归属、四个操作权限 | 确认正确插件和域名级资源;不要扩大到主账号权限 |
| `Tencent_SecretId` 未配置 | 变量大小写、配置文件权限、systemd 用户 | 确认环境文件已加载,不使用 `DP_Id` 代替 |
| TXT 验证超时 | DNS 传播、权威 DNS、DNSSEC、CAA、出站网络 | staging 中排查,再申请正式证书 |
| 七牛 `no such domain` / 612 | 域名是否存在于该 AK 所属 CDN 账号、是否用了源站域名、前导点 | 填准确 CDN 域名,移除前导点,不填写 URL |
| 首次更新 HTTPS 失败 | 两个域名是否均已开启 HTTPS | 先完成控制台首次绑定,再部署 hook |
| 证书已上传但图片域名仍旧证书 | 两域名绑定、配置传播、当前 CDN 节点 | 检查两个域名而非只看证书列表,等待传播并多网络核验 |
| TLS 正常但图片 401/403 | 链接过期、签名、服务器时间、Referer/IP/UA 限制 | 检查图片授权,不重新申请 HTTPS 证书 |
| 已过期链接仍能下载 | 私有回源鉴权和缓存命中路径 | 修正鉴权,重跑第 11 节,不改 bucket 为公开 |
| 网站仍返回旧证书 | Nginx 导出目录、部署脚本、`nginx -t`、service 状态 | 修复导出/配置后重跑 service,核验两个网站证书 |
| 本地证书新、七牛证书旧 | hook 调用失败、AK 权限、目标域名列表 | 重试 `--deploy`,不强制重复签发 |
| systemd 执行后 inactive | oneshot 已正常结束 | 看 timer、Result 和退出码 |
| 没收到邮件 | SMTP 授权码、From、端口出站、收件箱 | 重做通知配置测试,检查邮箱服务商要求 |
| 图片签名链接 404 | 对象是否在正确空间、key 编码、是否被提前清理 | 检查对象身份和保留策略 |
| 应用 S3 访问失败 | region、endpoint、S3 空间名、对象权限 | 按第 12 节核对应用配置和存储空间权限 |
排错时只提供脱敏后的错误码、域名、时间和请求 ID;不提供 SK、证书私钥、完整授权 URL 或完整账号配置文件。
## 15. DNS 不在 DNSPod、或需要其他证书形式
### DNS 托管商不同
腾讯云服务器可以使用任意受支持 DNS 服务商的 API。仅替换第 5 节凭据和第 6~7 节的 `--dns` 插件,七牛部署和 systemd 流程不变:
| DNS 实际托管商 | acme.sh 插件 | 凭据说明 |
|---|---|---|
| 腾讯云 DNSPod 新版 API | `dns_tencent` | `Tencent_SecretId`、`Tencent_SecretKey` |
| DNSPod 旧版 API | `dns_dp` | `DP_Id`、`DP_Key`;不是腾讯云 SecretId/SecretKey |
| 阿里云 DNS | `dns_ali` | 按该插件文档配置对应凭据 |
| Cloudflare DNS | `dns_cf` | 按该插件文档使用限定 DNS 权限的 API token |
不要仅因为服务器在腾讯云就迁移域名 DNS。其他插件的最新变量名和权限参考 [acme.sh DNS API 指南](https://github.com/acmesh-official/acme.sh/wiki/dnsapi)及 [DNS API 指南第二部分](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2)。
### 扩展 Atlas 域名时保持同一层级
主流程已使用 `atlas.joeyone.cn` + `*.atlas.joeyone.cn`。未来增加 `api.atlas.joeyone.cn`、`staging.atlas.joeyone.cn` 或 `img-staging.atlas.joeyone.cn`,证书覆盖范围无需改变,但仍要配置 DNS、对应服务以及证书部署。新增图片域名还要绑定独立环境空间,并加入 `QINIU_CDN_DOMAIN` 和监控列表。
若使用 `img.dev.atlas.joeyone.cn` 等更深层域名,需要额外 SAN 或新的通配符,不适合当前四域名共用一张证书的命名目标。`--deploy -d`、`--install-cert -d` 始终使用证书主域名 `atlas.joeyone.cn`,不能拿某个图片域名当证书身份。
### 开发生产使用不同七牛账号
需要每账号独立配置目录、凭据和部署任务,分别申请/维护证书更容易管理权限。本手册集中任务适用于同一七牛账号下、由同一维护者管理的两个域名;不把共享证书当作共享存储权限的理由。
## 16. 完成验收清单
- [ ] 两个空间都是私有,生产与开发数据/应用凭据隔离。
- [ ] 两个下载域名是七牛 CDN 域名,分别指向正确空间,CNAME 已生效。
- [ ] 两个域名均启用私有空间回源鉴权,未添加与现有签名协议冲突的限制。
- [x] DNS-01 staging 验证及测试证书签发成功(2026-10-03)。
- [ ] 临时 TXT 已清理,两个图片域名 CNAME 保持正确。
- [x] 正式 SAN 含 `atlas.joeyone.cn` 与 `*.atlas.joeyone.cn`,覆盖四个域名;颁发者及有效期已检查(2026-10-03)。
- [ ] 两个网站 A 记录正确,Nginx 分别路由到对应环境,证书导出、配置检查和重载成功。
- [x] 七牛 hook 首次部署对两个图片域名均返回成功(2026-10-03 11:26:12,北京时间)。
- [x] 两个图片域名的实际网络证书已核验更新(2026-10-03,当前探测节点的颁发者及有效期与本次正式证书一致,两个域名指纹相同)。
- [ ] systemd timer 启用,手工触发 service 成功,七牛及 Nginx 均完成部署;没有另一个 cron/服务器并发维护。
- [ ] 当前 1Panel 阶段:先启用仅续期、七牛部署及 PEM 导出的基础 timer,手工触发成功;应用上线后补齐网站同步,再验收上一项完整链路。
- [ ] 故障/实际续期通知已收到测试消息,四个域名已纳入独立到期监控。
- [ ] 两空间测试图片均通过有效签名、无签名、缓存后无签名、过期签名测试。
- [ ] 权限受控的加密备份和旧有效证书回滚方式已准备。
- [ ] 应用 S3 region、上传与图片域名的私有下载均已验证,未把证书部署成功当作 Ozon 上架验收成功。
## 17. 项目与官方参考
- [项目对象存储概念与身份边界](./object-storage-object-key.md)
- [项目技术栈与环境边界](../architecture/technology-and-deployment.md)
- [七牛 SSL FAQ:证书 API 与源站域名限制](https://developer.qiniu.com/fusion/kb/3905/ssl-certificate-faq?category=kb)
- [七牛私有空间 CDN 回源鉴权](https://developer.qiniu.com/fusion/3930/back-to-the-source-authentication)
- [七牛私有下载 URL](https://developer.qiniu.com/kodo/1656/download-private)
- [腾讯云子账号 API 密钥管理](https://cloud.tencent.com/document/product/598/37140)
- [腾讯云 DNSPod 可授权资源类型](https://cloud.tencent.com/document/product/302/105719)
- [acme.sh 3.1.6 发行说明](https://github.com/acmesh-official/acme.sh/releases/tag/3.1.6)
- [Nginx HTTPS 配置](https://nginx.org/en/docs/http/configuring_https_servers.html)
- [DNSPod 泛解析与明确记录匹配](https://cloud.tencent.com/document/product/302/9073)
- [acme.sh 项目与安装说明](https://github.com/acmesh-official/acme.sh)
- [Let's Encrypt DNS-01 验证](https://letsencrypt.org/docs/challenge-types/)
云平台界面和接口可能调整;操作时以实际控制台和上列官方文档为准。本文脚本需在你的真实账号做首次验收,不能仅靠复制成功就认为完成续期闭环。