Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/SKILL-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 测试

Expand Down
229 changes: 229 additions & 0 deletions docs/addon-mysql-credential-hygiene-no-argv-guide.md
Original file line number Diff line number Diff line change
@@ -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/<pid>/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" <<EOF
[client]
host=${HOST}
port=${PORT}
user=root
password=${OB_ROOT_PASSWD}
EOF

# 命令行不再出现密码字面值
timeout -k 1 30 mysql --defaults-file="$cnf" -e "$SQL"
```

**关键点**:

- 必须 `mktemp -d` 创建独立目录并 `chmod 700`,避免 `/tmp` 中其它进程读到。
- 写文件前 `umask 077`,确保创建的 `client.cnf` 模式只允许 owner 读写。
- 必须 `trap 'rm -rf "${tmp_dir}"' EXIT INT TERM` 而不是只写 `trap ... EXIT`。`INT` 和 `TERM` 单独列,否则 kubelet 给 Pod 发 SIGTERM 时清理逻辑不触发,临时文件留在节点上。
- 客户端必须用 `--defaults-file=$cnf`(**不**用 `--defaults-extra-file`,后者会叠加用户配置 `~/.my.cnf` 影响行为;`--defaults-file` 直接锁定唯一来源)。

### 规则 C — 外层加 bounded timeout

任何 `mysql` / `obclient` 调用都包 `timeout`:

```bash
timeout -k 1 30 mysql --defaults-file="$cnf" -e "$SQL"
```

- `30` 是超时上限,按场景调(角色探针 2-5s;行集读 30s;DDL 类 60-120s)。
- `-k 1` 表示软超时到达后再过 1 秒发 `KILL`,防止僵进程占住 Pod。
- 不写 timeout 的脚本一旦碰到 vcluster API / DB 后端慢,整条 lifecycle action 会无限挂;postReady / role probe 路径出过多次这类 hang。

### 规则 D — 调用结束后立刻验证 argv 不含密码

每条修改后或新增的脚本必须做一次本地 / live 双 sha 验证 + argv 静态扫描:

```bash
# 1. 本地脚本静态扫描
grep -cE '(mysql|obclient)[^|]*-p[^"]*\$\{?[A-Z_]+\}?|--password=\$\{' /path/to/script
# 期望输出 0

# 2. 若 script 已经下发为 live ActionSet / ConfigMap,再从 live 资源里抓出实际内容做相同扫描
kubectl get actionset <NAME> -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=<RUN_ID>
live_postready_sha=<sha256 of live script content>
local_patch_sha=<sha256 of local source>
sha_match=PASS|FAIL
unsafe_mysql_argv_matches=<integer; must be 0>
has_timeout=<integer; must be ≥1>
has_trap_cleanup=<integer; must be ≥1, regex `^[[:space:]]*trap `>
```

- `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" <<EOF
[client]
host=${HOST}
port=${PORT:-2881}
user=root
password=${OB_ROOT_PASSWD}
EOF

timeout -k 1 30 mysql --defaults-file="$cnf" -e "SELECT @@version"
```

`ps aux | grep mysql` 看到的是 `mysql --defaults-file=/tmp/ob-client-XXXXXX/client.cnf -e SELECT @@version`,密码不在 argv。

### 反例 2(trap 不全)

```bash
trap "rm -rf $tmp_dir" EXIT # 只挂 EXIT
```

容器收到 `SIGTERM` 时不触发 trap,临时密码文件留在 Pod 节点上直到 Pod 真的死掉为止。修法:

```bash
trap 'rm -rf "${tmp_dir}"' EXIT INT TERM
```

### 反例 3(用 MYSQL_PWD 环境变量代替 -p)

```bash
MYSQL_PWD="$OB_ROOT_PASSWD" mysql -h "$HOST" -uroot -e "$SQL"
```

部分 mysql 版本会把 `MYSQL_PWD` 在 `ps -E` / `/proc/<pid>/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,**不**回写到上方方法论正文。