diff --git a/docs/SKILL-INDEX.md b/docs/SKILL-INDEX.md index 9576711..cb1db1c 100644 --- a/docs/SKILL-INDEX.md +++ b/docs/SKILL-INDEX.md @@ -30,6 +30,7 @@ - [`addon-pvc-rebind-via-workload-intent-guide.md`](addon-pvc-rebind-via-workload-intent-guide.md) — 当一条 OpsRequest 需要把同名 PVC 从一块 PV 改绑到另一块(rebuild / restore-into-place / PV migration),用 Workload CR annotation 把意图交给 Workload 控制器(唯一写者),并按 intent-before-release + helper PV `claimRef` prebind 顺序交接所有权,避免 OpsRequest 控制器、Workload 控制器、动态 provisioner 三方抢同名 PVC 所有权造成 `PersistentVolume "" not found`、helper PV 消失或绑错 PV - [`addon-fake-server-protocol-validation-guide.md`](addon-fake-server-protocol-validation-guide.md) — FakeSentinel / FakeMySQL / fake mongos 等 fake protocol server 的 golden real-server 对照验收方法:raw protocol request、协议感知 reader、命令矩阵分类、known delta 记录、client-consumed fields 覆盖证明;附 Redis Sentinel RESP 案例 - [`addon-engine-image-entrypoint-env-name-collision-guide.md`](addon-engine-image-entrypoint-env-name-collision-guide.md) — chart cmpd 定义的 env name 与 engine docker image 内 docker-entrypoint.sh reserved env name 撞名时, image entrypoint 会静默执行副作用 (典型: CREATE USER 无 IF NOT EXISTS 写 binlog → secondary replay 撞 1396)。识别信号 / grep entrypoint script 排查 / rename chart env 修法 (KB_ prefix 推荐) / 验证收敛 6-gate / N=1 GREEN ≠ release-ready。附 MariaDB Addon 2026-05-13 案例 (mariadb:11.4 entrypoint grep + chart diff + 6-gate verify) 进 Case Appendix +- [`addon-mysql-credential-hygiene-no-argv-guide.md`](addon-mysql-credential-hygiene-no-argv-guide.md) — addon lifecycle action / ActionSet hook / 测试脚本调用 `mysql` `obclient` 等 MySQL-protocol client 时,密码不能进 argv(`-p$PASS` / `--password=$PASS` 全部禁),必须 stdin 或 `mktemp -d` + `chmod 700` + `umask 077` + `trap 'rm -rf "$tmp_dir"' EXIT INT TERM` + `--defaults-file` + 外层 `timeout`;patch-gate 三指标 `unsafe_mysql_argv_matches=0 / has_timeout=1 / has_trap_cleanup=1` 仅证修复在位、**不**证 PITR / 功能 PASS / acceptance / release-ready;附 OceanBase enterprise addon `oceanbase-physical-backup` postReady 修复后 6 样本运行时观测案例(不外推) ### 2. 写新 smoke / chaos 测试 diff --git a/docs/addon-mysql-credential-hygiene-no-argv-guide.md b/docs/addon-mysql-credential-hygiene-no-argv-guide.md new file mode 100644 index 0000000..5822527 --- /dev/null +++ b/docs/addon-mysql-credential-hygiene-no-argv-guide.md @@ -0,0 +1,229 @@ +# Addon `mysql` / `obclient` Credential Hygiene — Passwords Must NOT Appear in Process argv + +> **Audience**: addon dev / test / TL +> **Status**: stable methodology +> **Applies to**: any KB addon that invokes `mysql`, `obclient`, or any MySQL-protocol client from a Pod-side script (lifecycle action, ActionSet `postReady`, backup/restore hook, role probe, smoke test, configuration manager). +> **Applies to KB version**: any (methodology, version-agnostic). + +属于:方法论主题文档(不绑定单一引擎)。 + +## 1. 这篇要解决的问题 + +addon 在 lifecycle action / ActionSet hook / 测试脚本里经常需要调用 `mysql` 或 `obclient` 去执行 SQL。最常见、也最危险的写法是: + +```bash +# 反例 +mysql -h "$HOST" -P "$PORT" -uroot -p"$OB_ROOT_PASSWD" -e "$SQL" +mysql --password="$OB_ROOT_PASSWD" -h "$HOST" -e "$SQL" +``` + +这两种写法都把明文密码塞进 **进程参数(argv)**。带来的硬问题: + +1. **任何 `ps` / `/proc//cmdline` / `kubectl exec ... ps` 看一眼就能拿到密码。** Pod 内只要有任意一个有 read 权限的旁路(sidecar、metrics scraper、kbagent、debug shell、kubectl exec、节点上 root),密码就泄。 +2. **Pod 出问题做 evidence 收集时会被打印出来。** `describe pod`、controller log 里的 `Started container ... command [...]`、coredump、stack trace 都可能落盘。 +3. **日志聚合 / metrics / tracing 把命令行当作 process_command 字段采集。** 一旦进 ELK / Datadog / Loki 类系统,密码就外溢出 Pod 边界。 +4. **CI / E2E 测试归档证据时把 `ps` 输出整段打包。** 评审、Bug 复盘、客户支持现场都可能拿到。 + +> **边界 framing**:这一篇只讲 "addon 调用 MySQL-protocol client 时密码进 argv" 这一个问题。同样需要"密码不进进程参数"的其他情况(`psql` / `redis-cli` / `mongosh` / 自定义 binary)思路一致,但 binary 行为与可用替代参数不同,应另开主题。 + +## 2. 硬规则 + +### 规则 A — 任何客户端调用都不把密码作为 argv 的字面值 + +凡是涉及 MySQL-protocol client 的脚本,**禁止**写出下面任何形式: + +| 反例形式 | 为什么不行 | +|---|---| +| `mysql -p"$PASS"` / `mysql -p$PASS` | argv[i] = `-pVALUE`,明文落 `/proc/.../cmdline` | +| `mysql --password="$PASS"` | argv[i] = `--password=VALUE`,明文落 `/proc/.../cmdline` | +| `obclient -p"$PASS"` / `obclient --password=$PASS` | 同上 | +| `mysql ... <<< "SET password='$PASS'"` 但客户端用 `-p$PASS` 连 | 仍然落 argv | +| `bash -c "mysql -p${PASS} -e ..."` | shell 解析后 argv 仍含明文 | +| `kubectl exec POD -- mysql -p$PASS ...` | argv 落在 pod 进程列表,且 `kubectl exec` 命令本身的 argv 也包含密码(在调用方机器上同样泄) | + +> 这条规则覆盖所有 binary:`mysql`、`obclient`、`mariadb`、任何 MySQL-protocol 客户端都同样适用。 + +### 规则 B — 密码必须通过 stdin 或临时 client config 文件传入,client config 必须 trap cleanup + +合规的两种基本写法: + +**写法 1 — stdin(最简单,无文件落盘)** + +```bash +# OK:密码通过 stdin 喂 MYSQL_PWD 不行(MYSQL_PWD 环境变量在某些 mysql 版本也被 ps -e 暴露) +# 用 client 自带的 prompt-from-stdin 行为: +printf '%s\n' "$OB_ROOT_PASSWD" | mysql -h "$HOST" -uroot --skip-password -e "$SQL" +# (注:上面写法只在客户端支持从 tty/stdin 读密码时有效;多数 mysql/obclient 不支持。最实用的还是写法 2。) +``` + +**写法 2 — 临时 client config 文件(最通用)** + +```bash +set -euo pipefail + +tmp_dir="$(mktemp -d /tmp/db-client-XXXXXX)" +chmod 700 "$tmp_dir" + +# 关键:trap 必须覆盖 EXIT / INT / TERM 三种退出路径;不要只写 EXIT +trap 'rm -rf "${tmp_dir}"' EXIT INT TERM + +cnf="${tmp_dir}/client.cnf" +umask 077 +cat > "$cnf" < -o jsonpath='{.spec.restore.postReady[0].job.command[2]}' \ + | grep -cE '(mysql|obclient)[^|]*-p[^"]*\$\{?[A-Z_]+\}?|--password=\$\{' +# 期望输出 0 +``` + +把这个数字记成 `unsafe_mysql_argv_matches=0`,连同 `bash -n` 结果、文件 sha 一起写进 evidence pre-launch gate。如果是 0 才允许跑下一步。 + +## 3. 测试侧验收(patch-gate) + +新写 / 改过的 addon lifecycle action / ActionSet 脚本,进入测试前在 evidence 目录里维护一个 `patch-gate-summary.env`: + +``` +run_id= +live_postready_sha= +local_patch_sha= +sha_match=PASS|FAIL +unsafe_mysql_argv_matches= +has_timeout= +has_trap_cleanup= +``` + +- `live_postready_sha` 与 `local_patch_sha` 必须一致,否则说明 live cluster 上跑的是另一份代码,patch 没生效。 +- 这个 gate **只能证明两条修复(凭据卫生 + bounded timeout + trap cleanup)在 live 资源里**,**不能**作为"功能 PASS"、"产品已被证明稳定"、"release-ready" 的证据。任何 closeout 都要把这条边界单列。 + +## 4. 评审时的快速 checklist + +PR 评审时按这 5 条扫一遍: + +1. **`grep -nE '(mysql|obclient)[^|]*-p[^"]*\$|--password=\$' patch_files` 是否 0 命中?** +2. **是否有 `timeout` 包住每个客户端调用?** +3. **临时文件方案是否同时具备**:`mktemp -d` + `chmod 700` + `umask 077` + `trap 'rm -rf "$tmp_dir"' EXIT INT TERM`? +4. **使用的是 `--defaults-file` 不是 `--defaults-extra-file`?** +5. **closeout / 报告里有没有把 `unsafe_mysql_argv_matches=0` 当 PITR / 功能 PASS 的证据?(不该当)** + +任一条不过,PR 不合并。 + +## 5. 反例与正例对照 + +### 反例 1(密码进 argv) + +```bash +mysql -h "$HOST" -uroot -p"$OB_ROOT_PASSWD" -e "SELECT @@version" +``` + +`ps aux | grep mysql` 立刻看见 `-pSomePass!23`。 + +### 正例(stdin 传不可用时,用 client config 文件 + trap cleanup) + +```bash +set -euo pipefail + +tmp_dir="$(mktemp -d /tmp/ob-client-XXXXXX)" +chmod 700 "$tmp_dir" +trap 'rm -rf "${tmp_dir}"' EXIT INT TERM + +cnf="${tmp_dir}/client.cnf" +umask 077 +cat > "$cnf" </environ` 显示。比 `-p$PASS` 稍好但仍不彻底安全。**不推荐**作为主方案,仅在没有 client config 文件能力的极端 sidecar / minimal image 环境作为 fallback。 + +## 6. 与其他 skill 的关系 + +- `addon-evidence-discipline-guide.md` — patch-gate 的 `unsafe_mysql_argv_matches=0` 只能匹配"修复在位"这个结论,不能写成"功能 PASS"。本 doc §3 与 §4 第 5 条复用该规则。 +- `addon-bounded-eventual-convergence-guide.md` — §2 规则 C 的 `timeout` 不是 product-side timeout(postReady 业务等待用 `wait_postready_restore` 的 budget),是单条 client 调用上限,避免 hang 阻塞 lifecycle 上层逻辑。 +- `addon-test-acceptance-and-first-blocker-guide.md` — patch-gate 通过 ≠ 测试 PASS;当 closeout 时 readback / target / postReady 出问题,仍按 first-blocker 5 层分类。 + +## Appendix A — OceanBase enterprise addon 案例(仅记录证据,不外推) + +> 本附录只记录在 OceanBase enterprise addon 上的具体观测,**不**作为方法论结论本身,**不**作为 release / acceptance 证据。 +> 边界严格:N 个样本仅证明在该 N 个样本中 `unsafe_mysql_argv_matches=0` 是 live、可读、可计数;并不证明产品稳定 / PITR full coverage / release-ready。 + +### Case A.1 — `oceanbase-physical-backup` ActionSet `spec.restore.postReady[0].job.command[2]` + +在 OceanBase enterprise addon 上把 postReady 中的 `mysql` 调用按 §2 改造后,进入测试 patch-gate 的数字: + +- live postReady script sha256: `b74912857084451adbf707166a82bd8f55efce5ca74f3b2ce964c65451fc9925` +- `unsafe_mysql_argv_matches = 0` +- `has_timeout = 1` +- `has_trap_cleanup = 1`(`trap 'rm -rf "${tmp_dir}"' EXIT INT TERM` 在脚本第 333 行) + +观测样本边界(截至本附录写作时):6 个 PITR 端到端 runtime 样本中均观察到上述三项指标稳定为 `0 / 1 / 1`;其中两个独立运行路径下分别累计 N=3 clean candidates,**仅**用于证明 §2 三条规则的修复在测试期内有效落地,**不**作为 PITR 功能 PASS 或 addon 验收证据。 + +### Case A.2 — 反例对比 + +OceanBase enterprise addon 的修复前版本曾出现 `mysql -p${OB_ROOT_PASSWD}` 风格调用,在测试 evidence 归档时进程列表带出明文密码字段,被首次 patch-gate 自动扫描捕获,相应 closeout 标注为 runner-hygiene first-blocker(不是 product),按 §2 改造后才允许进入下一轮样本。 + +附录到此为止;后续若在其它引擎或路径上的实测数据请另起 appendix,**不**回写到上方方法论正文。