# 威胁模型与信任边界

LLMS 索引： [llms.txt](/llms.txt)

---

安全设计的起点不是“打开 TLS”或“创建一个只读用户”，而是回答：

```text
保护什么
防谁做什么
跨过哪条边界
造成什么后果
由哪一层阻止、发现、限制和恢复
```

没有威胁模型，最小权限就没有“最小”的参照；审计也不知道该记录什么。

本章不要求先写一份几十页的合规文档。对一个数据库服务，先把下面六列填满
就足以发现大部分架构空洞：

| 资产 | 主体 | 入口 | 不允许的动作 | 首要控制 | 验收证据 |
|---|---|---|---|---|---|
| 租户订单 | API runtime | pooled primary | 跨租户读写 | ACL + RLS | 正负 SQL |
| 模式定义 | migration pipeline | direct primary | 未审批 DDL | owner 分离 | role graph + log |
| 备份/WAL | backup agent | repository | 未授权读取/删除 | 专用身份 + 存储策略 | restore/audit |
| 凭据 | application/deployer | secret channel | 泄露、长期有效 | 轮换/撤销 | 双版本演练 |
| 运行日志 | operator/SIEM | log pipeline | 敏感值扩散 | 脱敏 + ACL | config + sample |

## 23.1.1 用户、应用、运维、平台与第三方 {#item-23-1-1}

### 一个请求里有不止一个“用户”

典型 API 请求至少包含五种身份：

```text
human end user
  -> application service identity
      -> PostgreSQL session_user
          -> PostgreSQL current_user
              -> business tenant / subject
```

它们不可互换。

`session_user` 是连接时通过 PostgreSQL/PgBouncer 认证的 login。`current_user`
是当前做权限检查的 effective role；`SET ROLE` 后二者可以不同。终端用户往往
根本没有数据库 login，其身份由应用认证系统维护。tenant 又可能是组织、项目、
账户或数据域，并不一定等于人或数据库角色。

本章实验刻意记录：

```sql
SELECT session_user, current_user;
```

在 runtime 事务中应类似：

```text
session_user = test
current_user = pg36_ch23_runtime
```

这能证明数据库执行权限被收窄，却不能证明终端用户是谁。后者需要应用把
request id、actor id、授权结果与数据库 transaction 关联到受保护的审计链。

### 终端用户

终端用户可以被信任去：

- 提交业务输入；
- 持有自己的认证因子；
- 发起自己被授权的动作。

不能被信任去：

- 声明“我属于 tenant B”后直接控制数据库上下文；
- 选择 effective database role；
- 决定查询是否绕过 RLS；
- 控制审计字段、来源 IP 或 `application_name` 的安全含义。

因此：

```text
HTTP header X-Tenant-ID
  -> 只能作为一个待校验输入
  -> 应用根据已认证 actor 和授权关系求出 authorized tenant
  -> 再用 bind parameter 写入 transaction-local database context
```

若直接做：

```text
SET app.tenant_id = request.headers["X-Tenant-ID"]
```

RLS 只是把越权选择高效地执行了一遍。

### 应用与批处理

“应用”也不是一个主体。至少拆成：

| workload | 需要 | 不需要 |
|---|---|---|
| API runtime | 短事务、必要 DML | owner、DDL、TRUNCATE |
| async worker | 特定队列对应的 DML | 全库后台权限 |
| read API | SELECT | 写入 |
| report/ETL | 受控只读、资源预算 | 主写高权 |
| CDC | replication/slot 的精确能力 | SUPERUSER |
| migration | object owner 或受控 DDL | 常驻 serving credential |

若它们共用一个 login：

- 一处泄露扩大到所有能力；
- 无法按 workload 撤销；
- 日志难以归因；
- 连接预算和 timeout 无法分开；
- 临时授予会悄悄变成永久默认。

本章角色模型先按“能力”拆 NOLOGIN role，再让独立 login 以明确 membership
获得其中一项。实验为了复用已交付的 PgBouncer `test` 身份，在同一个沙箱
login 上挂 runtime 和 readonly；这是有标签的实验例外，不是生产模板。

### 运维人员

运维需要的不是“平时就是超级用户”，而是两条路径：

```text
routine operator
  read catalogs / metrics / logs
  run approved bounded procedures
  no arbitrary data access by default

break-glass operator
  time-bound elevation
  ticket + reason + peer/after-the-fact review
  short credential lifetime
  complete action evidence
  explicit revoke
```

PostgreSQL superuser 可以绕过对象 ACL 和 RLS，访问敏感 catalog，执行服务器
文件/程序相关能力；它是信任根，不是普通管理员的方便模式。

还要区分 OS root、PostgreSQL superuser 和平台控制面：

```text
OS root                    可以读数据目录和进程内存
PostgreSQL superuser       可以绕过数据库权限
Pigsty/Ansible controller  可以改 inventory 并重渲染大量节点
secret administrator       可以改变认证材料
backup administrator       可能读出全量历史数据
```

把五者授给同一个长期账号，会让数据库内最精细的 GRANT 失去意义。

### 平台自动化

平台被信任去：

- 根据受评审声明创建 role/database/HBA/service；
- 在限定主机和阶段收敛配置；
- 输出变更记录；
- 检查 drift；
- 回收明确属于平台管理的对象。

平台不应被默认信任去：

- 猜测现有手工对象能否覆盖；
- 在 production 看到差异就无条件“强制收敛”；
- 把 secret 展开到日志、diff 或工单；
- 用一个全局账号服务所有 workload；
- 把“playbook 成功”当成应用授权语义通过。

声明是 desired state，运行 catalog/HBA/连接实验才是 actual state。二者都要
保留，差异本身就是安全事件或变更线索。

### 第三方、扩展与外部系统

第三方包括：

- PostgreSQL extension；
- 备份/归档存储；
- APM、日志、SIEM；
- BI/ETL/CDC；
- cloud/KMS/secret manager；
- 外包运维和供应商 support bundle。

每个集成至少回答：

```text
它获得什么数据和 metadata
credential 存在哪里、有效多久
是否能进一步委托
失败时是否 fail open
日志/备份保存在哪里
删除与撤销如何传播
供应链版本如何验证
```

安装 trusted extension 不等于“其维护者、发行包、依赖和升级以后都可信”。
将数据库日志送往 SaaS 也不自动满足数据驻留和删除要求。第三方边界必须进入
数据流图，而不是写在采购附件里。

### 主体—能力矩阵

一个评审可从这个矩阵开始：

| 主体 | connect | data DML | DDL/owner | secret | backup | audit admin |
|---|---:|---:|---:|---:|---:|---:|
| API runtime | ✓ | 必要子集 | — | 只读自身 | — | — |
| read workload | ✓ | SELECT | — | 只读自身 | — | — |
| migration | 窗口内 | 验证所需 | 受控 | 短期 | — | 产生日志 |
| operator | 受控 | 默认无 | SOP 子集 | 默认无 | 检查 | 只读 |
| break-glass | 临时 | 临时 | 临时 | 受审批 | 临时 | 不得删改 |
| backup agent | 专用 | — | — | 只读自身 | 写仓库 | — |
| audit collector | 专用 | — | — | 只读自身 | — | 写不可变目标 |

`✓` 不是“所有权限”，每一格还要落到 endpoint、role、ACL、network 和证据。

## 23.1.2 网络、凭据、SQL、备份和日志攻击面 {#item-23-1-2}

### 用数据流而不是组件清单建模

“我们有 PostgreSQL、PgBouncer 和防火墙”不是威胁模型。先画流：

```text
client
  -> DNS / VIP / load balancer
      -> HAProxy
          -> PgBouncer
              -> PostgreSQL primary / replica
                  -> WAL archive / backup repository
                  -> logs / metrics / traces
```

对每条箭头问：

1. 谁发起；
2. 如何认证对端；
3. 是否加密；
4. 是否可以重放；
5. metadata 会泄露什么；
6. 失败时转向哪里；
7. 谁能修改路由或信任根；
8. 证据由谁保存。

第 22 章已经说明代理和池化会改变 session 与故障语义。本章再加一项：它们也
是独立认证面。PostgreSQL role 新建成功，不代表 PgBouncer 的 `auth_file`、
`auth_query` 或 HBA 已经接受它。

### 网络攻击面

网络层包括的不只是公开 `5432`：

- PostgreSQL、PgBouncer、HAProxy 服务端口；
- Patroni REST API；
- etcd/DCS；
- SSH/Ansible；
- exporter、Grafana、日志和备份端点；
- DNS、VIP、cloud load balancer；
- 同机 Unix socket；
- 容器/overlay 网络和跨区链路。

常见失败：

```text
0.0.0.0 listen + broad security group
intranet CIDR 被当成永久可信主体
TLS 可用但客户端允许降级
证书验证了 CA，却没有验证 hostname
管理面与业务面共用网络和 credential
监控接口可读 SQL 文本、role、database 和拓扑
DCS/API 被暴露后可以影响选主
```

`listen_addresses`、主机防火墙、安全组、HBA、代理 ACL 和应用身份是串联控制。
任一层收紧都能缩小暴露面，但不能宣称另一层不再需要。

### 凭据攻击面

凭据不仅是 PostgreSQL password：

```text
database password / SCRAM verifier
client private key
server private key / CA private key
SSH key / sudo authority
Patroni / etcd / backup credentials
cloud access token
application secret-manager token
session cookie / OAuth token
```

需要同时保护：

- 生成时的随机性；
- 存储位置和文件权限；
- 注入过程；
- 进程环境、命令行、core dump；
- CI 日志、shell history、debug output；
- 备份和旧版本；
- 轮换期间的双版本窗口；
- 撤销后的既有 session。

SCRAM verifier 不是明文，但仍是敏感认证材料。raw PgBouncer userlist、完整
inventory 和 CA private key 不应进入普通 evidence bundle。

### SQL 攻击面

SQL 注入只是其中一类：

| 路径 | 例子 | 控制 |
|---|---|---|
| 值注入 | 拼接用户输入 | bind parameter |
| 标识符注入 | 动态表/schema 名 | allowlist + identifier API |
| search path | 同名恶意函数/操作符 | 受控 path + qualified name |
| definer 提权 | PUBLIC EXECUTE | secure path + revoke/grant |
| owner 提权 | runtime 拥有表 | owner/login 分离 |
| role 链 | ADMIN/SET 过宽 | membership options + graph test |
| RLS 绕过 | owner/superuser/BYPASSRLS | FORCE + 独立 break-glass |
| policy 错误 | USING 正确、WITH CHECK 缺失 | 正负 DML 测试 |
| DoS | 极端查询、锁、临时文件 | timeout + resource governance |

安全测试必须包含“有效但不该允许的 SQL”。语法错误只证明 parser 工作，不
证明授权边界正确。

### 备份和 WAL 攻击面

数据库表做了 RLS，不代表备份按租户隔离。物理备份和 WAL 通常包含整个
cluster 的历史状态：

- 已删除或更新前的数据可能仍在；
- credential/catalog 也会进入；
- repository 管理员可能读到所有租户；
- retention 超过业务删除期限；
- object storage versioning 会延长实际寿命；
- restore 到隔离区后会出现新的明文副本；
- support bundle 可能携带配置、日志和样本数据。

因此备份安全至少包括：

```text
repository identity
encryption at rest/in transit
key separation
immutable/retention policy
delete/legal-hold semantics
restore sandbox access
evidence cleanup
```

第 21 章验证的是恢复能力；本章补上谁能读取、删除和恢复。

### 日志与可观测攻击面

日志既是证据，也是数据外泄渠道。可能出现：

- SQL literal；
- extended protocol bind value；
- error context；
- connection string；
- tenant/user/email/order id；
- DDL 中的 password 或 secret；
- backup path 和内部地址；
- `application_name` 中的用户输入。

监控也会泄露：

- `pg_stat_activity.query`；
- query sample；
- role/database/schema 名；
- replication/topology；
- dashboard screenshot；
- alert payload。

安全目标不是“少记录”，而是：

```text
记录足以调查的 actor/action/resource/outcome/time/correlation
不记录不必要的 secret 和敏感 payload
限制谁能读、改、删
确保时间、完整性、保留和检索可用
```

### 可用性也是安全属性

认证和授权控制也能造成拒绝服务：

- 外部 IdP 不可用导致所有新连接失败；
- CRL/OCSP 依赖超时；
- 密码轮换不同步导致连接风暴；
- HBA 错序锁死管理员；
- audit 全量记录填满磁盘；
- RLS policy 中的昂贵子查询放大每次访问；
- brute-force 占满认证和连接槽。

威胁模型要写 fail-open/fail-closed 和应急路径。不能为了“高可用”悄悄回退
到弱认证，也不能为了“安全”在没有管理恢复入口时一次性切断所有访问。

### 从攻击路径生成测试

把抽象威胁变成实验：

```text
wrong server name
  -> verify-full connection must fail

client disables TLS
  -> production endpoint must fail；本章 sandbox 反而成功，所以 gate pending

runtime attempts ALTER TABLE
  -> SQLSTATE 42501

tenant A writes tenant B row
  -> WITH CHECK rejects

session tenant context survives pool reuse
  -> reproduce, then replace with transaction-local context

old password after rotation
  -> new connection fails；existing connection remains and must be drained

new PostgreSQL role through pool
  -> absent from pool auth surface, connection fails
```

每个测试都要记录目标、路径、预期 SQLSTATE/事实、清理和解释边界。

## 23.1.3 数据分级、租户边界与应急权限 {#item-23-1-3}

### 分级决定控制，而不是标签颜色

一个实用分级至少回答：

| 维度 | 问题 |
|---|---|
| confidentiality | 泄露给谁会造成什么 |
| integrity | 被改错/伪造的后果 |
| availability | 最长可中断多久 |
| residency | 可以存放在哪些区域/供应商 |
| retention | 保存多久、何时必须删除 |
| audit | 哪些访问和变更必须可追溯 |
| recovery | 恢复副本需要什么同等级控制 |

同一行可以混合不同级别：公开商品名、内部成本、个人地址和支付 token 不应因
都在 `orders` 表里就采用同一日志/访问策略。

数据库实现可以组合：

- schema/table/column privilege；
- view 或 security-invoker API；
- RLS；
- application-level field policy；
- tokenization/encryption；
- 独立 database/cluster/account；
- 备份与日志分级。

不要把“加密列”写成万能答案。密钥与数据库若由同一长期高权主体控制，主要
价值可能只是介质或下游暴露面收缩，而不是防数据库管理员。

### 租户边界的四种常见形态

| 形态 | 优点 | 主要代价/风险 |
|---|---|---|
| shared table + tenant key/RLS | 密度高、统一迁移 | policy/上下文错误影响面大 |
| schema per tenant | 对象和迁移边界更清晰 | 对象爆炸、search_path/运维复杂 |
| database per tenant | catalog/连接/备份边界更强 | 连接、升级、监控规模增加 |
| cluster/account per tenant | 故障/管理员/资源隔离最强 | 成本和平台复杂度最高 |

选择不是“RLS 安全不安全”，而是：

```text
tenant 数量和规模
监管/密钥/驻留要求
故障与 noisy-neighbor 边界
备份/恢复粒度
迁移频率
operator 信任模型
成本
```

RLS 适合共享表的数据库内 defense-in-depth。它不隔离 shared buffer、CPU、
WAL、backup、superuser，也不自动提供每租户 PITR。

### RLS 之外的隐蔽通道

即使行不可见，仍可能通过以下方式推断：

- unique/foreign-key 冲突；
- sequence/identity 变化；
- timing、lock wait、row count；
- error message；
- query plan/statistics；
- aggregate 或 rate limit；
- log/metric label；
- object name。

PostgreSQL 的 referential integrity 检查会绕过 RLS 以维护完整性。不要向低权
用户返回“该 email 已被另一个租户使用”之类能够确认全局存在性的细节，除非
这是明确业务合同。

### 数据边界必须贯穿派生物

租户边界要追到：

```text
primary row
  -> indexes / materialized views / search index
  -> logical replication / CDC
  -> cache
  -> analytics warehouse
  -> backup / PITR restore
  -> logs / traces / support evidence
```

源表 RLS 不会自动复制到这些系统。每个 consumer 要重新定义 identity、filter、
retention 和删除传播。

### 应急权限是一套协议

break-glass 至少包含：

```text
触发条件       正常路径不可用且存在明确风险
批准者         谁能批准，单人还是双人
身份           独立账号，禁止共享
时限           自动过期
范围           cluster/database/action/source
证据           ticket/reason/session/action/outcome
约束           禁止删审计、禁止无关数据浏览
退出           revoke/terminate/rotate
复盘           为什么需要、正常能力缺什么
```

仅把 superuser password 放进保险箱不够。取出后谁知道、已有 session 如何回收、
PgBouncer 是否仍接受、使用了哪些命令、何时换新，都必须可执行。

### 应急时的优先级

凭据疑似泄露时，一个保守序列：

```text
1. 限制暴露面和新认证
2. 保存时间线、连接、日志与配置证据
3. 判断是 login、role、host、CA 还是控制面失陷
4. 创建/验证替代凭据和管理路径
5. 双版本切换合法客户端
6. NOLOGIN / HBA reject / revoke
7. 终止仍有风险的既有 session
8. 轮换上下游与 PgBouncer
9. 验证旧凭据失败
10. 查找未授权动作并恢复
```

直接 `ALTER ROLE ... PASSWORD` 只影响后续认证，不会杀死已认证 session。本章
实验明确证明 password change 和 `NOLOGIN` 后旧连接仍能执行 `SELECT 1`。

### 什么时候升级为安全事件

至少这些情况不应作为普通工单悄悄修复：

- 未知主体获得高权 membership；
- production 出现 `trust`/意外 broad HBA；
- private key、password、SCRAM verifier 进入日志或仓库；
- RLS/ACL drift 造成跨租户可见；
- audit pipeline 被停用或删改；
- backup/restore 落入未批准位置；
- CA/secret manager/Ansible controller 身份失陷；
- operator 使用 break-glass 但无批准或证据。

第 31 章会展开事件指挥。本章先保证检测项和回收动作在平时可练。

### 最小威胁模型模板

```yaml
service: pg36_shop
asset:
  - tenant orders
  - credentials
  - backups and logs
subjects:
  - api runtime
  - migration pipeline
  - operator
trust_crossings:
  - client -> pooled endpoint
  - login -> runtime role
  - runtime role -> tenant row
abuse_cases:
  - wrong tenant context
  - stolen password
  - session state reused by another client
  - unapproved DDL
controls:
  - verify-full + SCRAM
  - SET LOCAL ROLE
  - FORCE RLS
  - separate owner
  - rotation and session drain
evidence:
  - HBA/TLS/role/policy projections
  - positive and negative transactions
  - immutable audit correlation
owner: data-platform
reviewers: application-owner, security
production_exceptions: []
```

模板的价值不在 YAML，而在于让每个控制都对应威胁、owner 和可重放证据。

## 本节检查表

```text
[ ] 列出数据、凭据、备份、日志和控制面资产
[ ] 区分 end user、service identity、session_user、current_user、tenant
[ ] 每个 workload 有独立能力与撤销边界
[ ] OS、database、platform、secret、backup 管理权没有无意合并
[ ] 画出 client 到 database、backup、logs 的每条信任跨越
[ ] 网络位置不被当作充分身份
[ ] SQL、role、owner、RLS 的攻击路径都有负向测试
[ ] 备份、WAL、日志和 support evidence 纳入数据分级
[ ] 租户隔离选择覆盖 backup/recovery/noisy-neighbor
[ ] break-glass 有触发、时限、证据、撤销和复盘
[ ] 凭据失陷流程会处理既有 session 与 PgBouncer
[ ] production exception 有 owner、期限和补偿控制
```

## 参考资料

- [NIST SP 800-207：Zero Trust Architecture](https://csrc.nist.gov/pubs/sp/800/207/final)
- [NIST SP 800-61 Rev. 3：Incident Response](https://csrc.nist.gov/pubs/sp/800/61/r3/final)
- [PostgreSQL 18：Database Roles](https://www.postgresql.org/docs/18/user-manag.html)
- [PostgreSQL 18：Row Security Policies](https://www.postgresql.org/docs/18/ddl-rowsecurity.html)
- [PostgreSQL 18：Schemas](https://www.postgresql.org/docs/18/ddl-schemas.html)
- [Pigsty：Security Considerations](https://pigsty.io/docs/deploy/security/)

---

[返回本章目录](../) · [下一节：认证与连接准入](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
