23.1 威胁模型与信任边界
安全设计的起点不是“打开 TLS”或“创建一个只读用户”,而是回答:
没有威胁模型,最小权限就没有“最小”的参照;审计也不知道该记录什么。
本章不要求先写一份几十页的合规文档。对一个数据库服务,先把下面六列填满 就足以发现大部分架构空洞:
| 资产 | 主体 | 入口 | 不允许的动作 | 首要控制 | 验收证据 |
|---|---|---|---|---|---|
| 租户订单 | 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 用户、应用、运维、平台与第三方
一个请求里有不止一个“用户”
典型 API 请求至少包含五种身份:
它们不可互换。
session_user 是连接时通过 PostgreSQL/PgBouncer 认证的 login。current_user
是当前做权限检查的 effective role;SET ROLE 后二者可以不同。终端用户往往
根本没有数据库 login,其身份由应用认证系统维护。tenant 又可能是组织、项目、
账户或数据域,并不一定等于人或数据库角色。
本章实验刻意记录:
在 runtime 事务中应类似:
这能证明数据库执行权限被收窄,却不能证明终端用户是谁。后者需要应用把 request id、actor id、授权结果与数据库 transaction 关联到受保护的审计链。
终端用户
终端用户可以被信任去:
- 提交业务输入;
- 持有自己的认证因子;
- 发起自己被授权的动作。
不能被信任去:
- 声明“我属于 tenant B”后直接控制数据库上下文;
- 选择 effective database role;
- 决定查询是否绕过 RLS;
- 控制审计字段、来源 IP 或
application_name的安全含义。
因此:
若直接做:
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;这是有标签的实验例外,不是生产模板。
运维人员
运维需要的不是“平时就是超级用户”,而是两条路径:
PostgreSQL superuser 可以绕过对象 ACL 和 RLS,访问敏感 catalog,执行服务器 文件/程序相关能力;它是信任根,不是普通管理员的方便模式。
还要区分 OS root、PostgreSQL superuser 和平台控制面:
把五者授给同一个长期账号,会让数据库内最精细的 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。
每个集成至少回答:
安装 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、备份和日志攻击面
用数据流而不是组件清单建模
“我们有 PostgreSQL、PgBouncer 和防火墙”不是威胁模型。先画流:
对每条箭头问:
- 谁发起;
- 如何认证对端;
- 是否加密;
- 是否可以重放;
- metadata 会泄露什么;
- 失败时转向哪里;
- 谁能修改路由或信任根;
- 证据由谁保存。
第 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 网络和跨区链路。
常见失败:
listen_addresses、主机防火墙、安全组、HBA、代理 ACL 和应用身份是串联控制。
任一层收紧都能缩小暴露面,但不能宣称另一层不再需要。
凭据攻击面
凭据不仅是 PostgreSQL password:
需要同时保护:
- 生成时的随机性;
- 存储位置和文件权限;
- 注入过程;
- 进程环境、命令行、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 可能携带配置、日志和样本数据。
因此备份安全至少包括:
第 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。
安全目标不是“少记录”,而是:
可用性也是安全属性
认证和授权控制也能造成拒绝服务:
- 外部 IdP 不可用导致所有新连接失败;
- CRL/OCSP 依赖超时;
- 密码轮换不同步导致连接风暴;
- HBA 错序锁死管理员;
- audit 全量记录填满磁盘;
- RLS policy 中的昂贵子查询放大每次访问;
- brute-force 占满认证和连接槽。
威胁模型要写 fail-open/fail-closed 和应急路径。不能为了“高可用”悄悄回退 到弱认证,也不能为了“安全”在没有管理恢复入口时一次性切断所有访问。
从攻击路径生成测试
把抽象威胁变成实验:
每个测试都要记录目标、路径、预期 SQLSTATE/事实、清理和解释边界。
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 安全不安全”,而是:
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 已被另一个租户使用”之类能够确认全局存在性的细节,除非 这是明确业务合同。
数据边界必须贯穿派生物
租户边界要追到:
源表 RLS 不会自动复制到这些系统。每个 consumer 要重新定义 identity、filter、 retention 和删除传播。
应急权限是一套协议
break-glass 至少包含:
仅把 superuser password 放进保险箱不够。取出后谁知道、已有 session 如何回收、 PgBouncer 是否仍接受、使用了哪些命令、何时换新,都必须可执行。
应急时的优先级
凭据疑似泄露时,一个保守序列:
直接 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,而在于让每个控制都对应威胁、owner 和可重放证据。
本节检查表
参考资料
- NIST SP 800-207:Zero Trust Architecture
- NIST SP 800-61 Rev. 3:Incident Response
- PostgreSQL 18:Database Roles
- PostgreSQL 18:Row Security Policies
- PostgreSQL 18:Schemas
- Pigsty:Security Considerations
返回本章目录 · 下一节:认证与连接准入 · 查看全书目录 · 查看索引中心