#!/usr/bin/env python3 # -*- coding: utf-8 -*- """作业18 · 生成「密钥轮换 Runbook」骨架(占位符由你填真实系统名) 用法: python hw18_02_密钥轮换Runbook骨架.py python hw18_02_密钥轮换Runbook骨架.py --org 播城科技 --platform-owner 平台组 输出: docs/md_cursor/作业18_密钥轮换Runbook_cursor.md(若 --write-doc) data/homework18/secret_rotation_runbook_skeleton.md """ from __future__ import annotations import argparse import os from datetime import datetime from pathlib import Path SYSTEMS = [ { "id": "dify_console", "name": "{{DIFY_CONSOLE_ADMIN}}", "secret_type": "管理员密码 / Session", "storage": "{{DIFY_ENV_PATH}} / 207 PostgreSQL", "consumers": "运维、工作室登录", "rotation_days": 90, }, { "id": "dify_api_app", "name": "{{DIFY_APP_API_KEYS}}", "secret_type": "应用 API Key", "storage": "Dify 控制台 · 各应用", "consumers": "业务系统调用 Agent", "rotation_days": 180, }, { "id": "litellm_master", "name": "{{LITELLM_MASTER_KEY}}", "secret_type": "LiteLLM Master Key", "storage": "/opt/litellm/.env → LITELLM_MASTER_KEY", "consumers": "Dify 模型供应商、脚本 curl", "rotation_days": 90, }, { "id": "vllm_upstream", "name": "{{VLLM_API_KEY}}", "secret_type": "vLLM Bearer / api_key", "storage": "LiteLLM config.yaml · Dify OpenAI 供应商", "consumers": "LiteLLM → 116:8000", "rotation_days": 180, }, { "id": "xinference", "name": "{{XINFERENCE_API_KEY}}", "secret_type": "Xinference API Key (xf-...)", "storage": "207 .env / Dify Xinference 供应商", "consumers": "Embedding / Rerank", "rotation_days": 180, }, { "id": "langfuse", "name": "{{LANGFUSE_PK_SK}}", "secret_type": "Langfuse Public/Secret Key", "storage": "Dify 追踪配置 · scripts_cursor/_config.py", "consumers": "worker ops_trace 上报", "rotation_days": 90, }, { "id": "dingtalk_webhook", "name": "{{DINGTALK_ROBOT_WEBHOOK}}", "secret_type": "钉钉机器人 access_token", "storage": "作业6/7 _config · 8780 环境变量", "consumers": "周报、自愈通知", "rotation_days": 90, }, { "id": "gpu_ssh", "name": "{{GPU_SSH_CREDENTIAL}}", "secret_type": "GPU 机 SSH 密码/密钥", "storage": "8779/8780 后端 · _config GPU_PASS", "consumers": "作业5/7 健康检查与 restart", "rotation_days": 90, }, { "id": "higress_admin", "name": "{{HIGRESS_ADMIN_PASS}}", "secret_type": "Higress 控制台", "storage": "patches_cursor/Higress部署", "consumers": "网关配置", "rotation_days": 90, }, { "id": "postgres_redis", "name": "{{DB_REDIS_PASSWORDS}}", "secret_type": "PostgreSQL / Redis 密码", "storage": "docker .env · envs/databases/", "consumers": "Dify 全栈", "rotation_days": 365, }, ] def build_runbook(org: str, owner: str, oncall: str) -> str: ts = datetime.now().strftime("%Y-%m-%d") table_rows = "\n".join( f"| {s['name']} | {s['secret_type']} | {s['storage']} | {s['rotation_days']} | ☐ |" for s in SYSTEMS ) return f"""# 作业18 · 密钥轮换 Runbook(骨架 · 请填 {{{{PLACEHOLDER}}}} 占位符) > **组织**:{org} > **平台负责人**:{owner} > **值班接口**:{oncall} > **版本**:骨架 {ts} — 由 `hw18_02_密钥轮换Runbook骨架.py` 生成 > **原则**:先 **登记** → **双写/并行** → **切流量** → **废旧密钥** → **审计归档** --- ## 0. 轮换分级 | 级别 | 场景 | 窗口 | 审批 | |------|------|------|------| | R1 | 疑似泄露 / 离职交接 | 4 小时内 | 信息安全 + 平台负责人 | | R2 | 计划到期轮换 | 维护窗口 | 平台负责人 | | R3 | 低敏内部 key | 月度批量 | 运维值班 | --- ## 1. 密钥清单(填真实系统名) | 系统占位符 | 类型 | 存放位置 | 建议周期(天) | 上次轮换 | |------------|------|----------|--------------|----------| {table_rows} --- ## 2. 通用轮换步骤(所有系统适用) 1. **登记**:工单 `{{TICKET_ID}}`,记录影响面(哪些 Agent/脚本会断) 2. **生成新密钥**:在 `{{SECRET_STORE}}` 或目标控制台创建 **新** key(勿先删旧) 3. **并行写入**:`.env` / Dify 供应商 / LiteLLM config **同时保留新旧**(若支持双 key) 4. **重启/热加载**:按系统 — LiteLLM `docker compose restart`;Dify worker 需 reload 追踪配置 5. **验收**: - Dify 对话 1 条 - Langfuse 新 Trace 出现 - `curl LiteLLM /v1/models` - 作业5 健康 Agent 探活(若动 GPU SSH) 6. **废旧密钥**:验收 24h 无报错后,控制台吊销旧 key 7. **审计行**:写入 `{{AUDIT_LOG}}`(操作人、时间、系统、是否回滚) --- ## 3. 分系统操作要点(骨架) ### 3.1 LiteLLM Master Key(`{{LITELLM_MASTER_KEY}}`) ```bash # 1. 备份 cp /opt/litellm/.env /opt/litellm/.env.bak.$(date +%Y%m%d) # 2. 改 .env 中 LITELLM_MASTER_KEY=<新 key> # 3. 同步 Dify 中所有使用该网关的模型供应商 API Key # 4. 重启 docker compose -f /opt/litellm/docker-compose.yml restart litellm # 5. 验收 curl -sS http://192.168.110.207:4000/v1/models -H "Authorization: Bearer <新 key>" ``` ### 3.2 Langfuse PK/SK(`{{LANGFUSE_PK_SK}}`) - Dify 控制台 → 设置 → 追踪 → 更新 PK/SK - **必须重启 worker**:`docker compose restart worker`(ops_trace 缓存) - 验收:发一条 Agent 对话 → Langfuse 有新 Trace ### 3.3 vLLM / Xinference 上游 Key - 改 LiteLLM `config.yaml` 中 `api_key` + Dify 供应商 - 116 vLLM 若启用 `--api-key`,需与 LiteLLM 同步 - 验收:命中测试 + 对话 ### 3.4 钉钉 Webhook(`{{DINGTALK_ROBOT_WEBHOOK}}`) - 钉钉群机器人 → 重置 webhook → 更新 `HOMEWORK6_DINGTALK_WEBHOOK` / 8780 env - 验收:`作业6_02_验收周报.py` 或 8780 `/notify/recovery` 测试 ### 3.5 GPU SSH(`{{GPU_SSH_CREDENTIAL}}`) - **高敏**:优先改密钥登录,禁用密码 - 更新 `scripts_cursor/_config.py` 或服务器 env **勿提交 Git** - 验收:8779 `/gpu/info` 返回 H20 ### 3.6 PostgreSQL / Redis(`{{DB_REDIS_PASSWORDS}}`) - 维护窗口;改 `.env` → 滚动重启 db → redis → api → worker - 提前备份(作业15 清单) --- ## 4. 回滚 | 系统 | 回滚动作 | |------|----------| | LiteLLM | 恢复 `.env.bak` + restart | | Langfuse | 恢复 Dify 追踪旧 PK/SK + restart worker | | 钉钉 | 恢复旧 webhook URL | | DB | **禁止**随意回滚密码;从备份 restore | --- ## 5. 轮换后检查清单 - [ ] 所有 Agent 对话 smoke test(行政 / 作业1 / 作业5 各 1 条) - [ ] Langfuse 24h 内 Trace 无断档 - [ ] 旧密钥已在源系统吊销 - [ ] `{{AUDIT_LOG}}` 已填写 - [ ] 相关脚本 `_config.py` 已同步(207 `/opt/workbuddy`,**密钥不进 Git**) --- ## 6. 与出站白名单的关系 - 轮换 **不改变** 域名白名单;若 webhook 域名变更,须重新走 `作业18_出站审核与业务策略_cursor.md` 审批。 --- *请将文中所有 `{{PLACEHOLDER}}` 替换为你们真实系统名、路径与工单号。* """ def main() -> None: parser = argparse.ArgumentParser(description="生成密钥轮换 Runbook 骨架") parser.add_argument("--org", default="{{ORG_NAME}}") parser.add_argument("--platform-owner", default="{{PLATFORM_OWNER}}") parser.add_argument("--oncall", default="{{ONCALL_CONTACT}}") parser.add_argument("--write-doc", action="store_true", help="同时写入 docs/md_cursor/") args = parser.parse_args() body = build_runbook(args.org, args.platform_owner, args.oncall) od = Path(os.environ.get("HW18_OUT", Path(__file__).resolve().parents[2] / "data" / "homework18")) od.mkdir(parents=True, exist_ok=True) out = od / "secret_rotation_runbook_skeleton.md" out.write_text(body, encoding="utf-8") print(f"Wrote {out}") if args.write_doc: doc = Path(__file__).resolve().parents[2] / "docs" / "md_cursor" / "作业18_密钥轮换Runbook_cursor.md" doc.write_text(body, encoding="utf-8") print(f"Wrote {doc}") if __name__ == "__main__": main()