diff --git a/docs/README.md b/docs/README.md index a8ad817..0cc6832 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,6 +38,7 @@ 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 部署与应用侧使用方法。 18. [`guides/postgresql-local-development.md`](./guides/postgresql-local-development.md):原生 PostgreSQL / Docker Compose 本地数据库、角色、迁移与建表。 -19. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。 -20. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。 -21. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。 +19. [`guides/qiniu-https-auto-renewal-tencent-cloud.md`](./guides/qiniu-https-auto-renewal-tencent-cloud.md):Atlas 四域名共用证书、DNS 验证,以及腾讯云 Nginx / 七牛 CDN 的 HTTPS 自动续期与部署操作手册。 +20. [`api/README.md`](./api/README.md):HTTP API 统一约定及第一批账户、门店、水印接口。 +21. [`reviews/implementation-readiness-open-items.md`](./reviews/implementation-readiness-open-items.md):实现前必须闭环的数据库/API 事项与已确认约束。 +22. 后续按模块补全定价、采集、商品编辑、发布和在线商品接口。 diff --git a/docs/guides/qiniu-https-auto-renewal-tencent-cloud.md b/docs/guides/qiniu-https-auto-renewal-tencent-cloud.md new file mode 100644 index 0000000..db2cb05 --- /dev/null +++ b/docs/guides/qiniu-https-auto-renewal-tencent-cloud.md @@ -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 | + 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 | + 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/) + +云平台界面和接口可能调整;操作时以实际控制台和上列官方文档为准。本文脚本需在你的真实账号做首次验收,不能仅靠复制成功就认为完成续期闭环。