docs: 域名、证书配置文档

This commit is contained in:
R524809
2026-09-30 17:48:03 +08:00
parent 45871584b3
commit 696112e74d
2 changed files with 939 additions and 3 deletions
+4 -3
View File
@@ -38,6 +38,7 @@
16. [`guides/browser-extension-background-service-worker.md`](./guides/browser-extension-background-service-worker.md):浏览器扩展后台 Service Worker 的运行模型、消息调用链与调试方法。 16. [`guides/browser-extension-background-service-worker.md`](./guides/browser-extension-background-service-worker.md):浏览器扩展后台 Service Worker 的运行模型、消息调用链与调试方法。
17. [`guides/minio-local-deployment.md`](./guides/minio-local-deployment.md):MinIO 简介、本地 Docker 部署与应用侧使用方法。 17. [`guides/minio-local-deployment.md`](./guides/minio-local-deployment.md):MinIO 简介、本地 Docker 部署与应用侧使用方法。
18. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。 18. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。
19. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。 19. [`guides/qiniu-https-auto-renewal-tencent-cloud.md`](./guides/qiniu-https-auto-renewal-tencent-cloud.md):Atlas 四域名共用证书、DNS 验证,以及腾讯云 Nginx / 七牛 CDN 的 HTTPS 自动续期与部署操作手册。
20. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。 20. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。
21. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。 21. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。
22. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。
@@ -0,0 +1,935 @@
# 腾讯云服务器运行七牛图片域名 HTTPS 证书自动续期
> 文档日期:2026-09-30。操作示例:腾讯云 Linux 服务器、Ubuntu 24.04、DNSPod 中国站、七牛 Kodo 私有空间与 CDN 加速域名、Let's Encrypt、acme.sh 3.1.6。
>
> 状态:操作手册;文档中的服务器安装、DNS 修改、证书申请和七牛部署尚未在你的账号执行。操作系统、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 保持正确。
如果有 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。
## 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. 等待控制台显示部署完成,检查两个域名的证书;临时下载到本机的私钥按秘密文件管理,传递结束后清理临时副本。
如果两个域名原本就已开启 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 上传可能留下两条证书记录,这是正常的;验收后按引用情况清理旧条目。
参考:[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)
维护账号不获得通用 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 部署脚本并完成首次接入
本节默认服务器已经安装并运行 Nginx。如果使用 Docker、宝塔、其他 Web 服务或开发网站位于另一台机器,应按实际部署方式替换证书分发与重载步骤;不要直接运行这份面向本机 systemd 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 配置定时任务
以下由管理账号执行,创建 systemd 单元:
```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
ExecStartPost=/usr/local/sbin/atlas-cert-install-nginx
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;只有申请/续期及七牛部署返回成功,才执行 root 的 `ExecStartPost` 部署 Nginx。维护账号仍没有 sudo 权限。Nginx 部署失败会使整个 service 失败;acme.sh 的邮件通知只覆盖其自身的申请及七牛步骤,因此还须监控 service 失败和四个域名实际证书。
每天北京时间 03:20 后随机延迟最多 30 分钟执行。每天检查不等于每天重新申请;acme.sh 按证书续期窗口决定是否申请,并更新保存的证书导出路径、调用七牛部署 hook。随后 Nginx 脚本核验证书,指纹变化才重载;若七牛失败则本轮不执行 Nginx 部署,排错后重新运行 service。服务器关机时不会运行,`Persistent=true` 会在恢复后补跑错过的定时检查。
刚申请的证书显示跳过续期是正常的。`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 从网络检查四个域名实际提供的证书:
```bash
set -o pipefail
for cert_domain in atlas.joeyone.cn dev.atlas.joeyone.cn 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 2>/dev/null |
openssl x509 -noout -issuer -dates -fingerprint -sha256
done
```
四个域名的网络证书指纹应在部署传播完成后与正式 `fullchain.cer` 的叶证书指纹一致。私有图片返回 401/403 不能据此判断 TLS 失败;TLS 握手验证可以独立于图片授权完成。
### 10.3 给四个域名分别加独立监控
在现有外部监控系统或支持证书检查的云监控产品中,把四个完整域名分别纳入 TLS 到期监控。同一张证书也可能在某个部署目标更新失败:
- 证书剩余不足 21 天告警,不足 7 天升级处理。
- TLS 握手、证书链或域名匹配失败立即告警。
- 日常任务长期没有运行,单独按任务执行时间监控。
- 私有资源 HTTP 探测需使用受控授权;普通无签名 403 不是资源故障。
首次手工检查到期阈值可使用以下命令;它只检查当前网络节点,不能代替多地区监控:
```bash
set -o pipefail
for cert_domain in atlas.joeyone.cn dev.atlas.joeyone.cn 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_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`。数据库素材身份继续记录 `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. `apps/api/src/media/media.service.ts` 和 `apps/worker/src/main.ts` 的 S3 region 为硬编码 `us-east-1`。需要后续代码任务添加配置,并与七牛 S3 区域匹配;目前没有已生效的 `OBJECT_STORAGE_REGION` 配置项,不能只在 `.env` 填一个新变量就认为完成。
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 已生效。
- [ ] 两个域名均启用私有空间回源鉴权,未添加与现有签名协议冲突的限制。
- [ ] DNS-01 staging 验证成功,临时 TXT 清理正常。
- [ ] 正式 SAN 含 `atlas.joeyone.cn` 与 `*.atlas.joeyone.cn`,覆盖四个域名;未误用更深一层域名。
- [ ] 两个网站 A 记录正确,Nginx 分别路由到对应环境,证书导出、配置检查和重载成功。
- [ ] 七牛 hook 首次部署成功,两域名实际证书均已更新。
- [ ] systemd timer 启用,手工触发 service 成功,七牛及 Nginx 均完成部署;没有另一个 cron/服务器并发维护。
- [ ] 故障/实际续期通知已收到测试消息,四个域名已纳入独立到期监控。
- [ ] 两空间测试图片均通过有效签名、无签名、缓存后无签名、过期签名测试。
- [ ] 权限受控的加密备份和旧有效证书回滚方式已准备。
- [ ] 应用 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/)
云平台界面和接口可能调整;操作时以实际控制台和上列官方文档为准。本文脚本需在你的真实账号做首次验收,不能仅靠复制成功就认为完成续期闭环。