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 请求至少包含五种身份:
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 又可能是组织、项目、
账户或数据域,并不一定等于人或数据库角色。
本章实验刻意记录:
SELECT session_user, current_user;在 runtime 事务中应类似:
session_user = test
current_user = pg36_ch23_runtime这能证明数据库执行权限被收窄,却不能证明终端用户是谁。后者需要应用把 request id、actor id、授权结果与数据库 transaction 关联到受保护的审计链。
终端用户
终端用户可以被信任去:
- 提交业务输入;
- 持有自己的认证因子;
- 发起自己被授权的动作。
不能被信任去:
- 声明“我属于 tenant B”后直接控制数据库上下文;
- 选择 effective database role;
- 决定查询是否绕过 RLS;
- 控制审计字段、来源 IP 或
application_name的安全含义。
因此:
HTTP header X-Tenant-ID
-> 只能作为一个待校验输入
-> 应用根据已认证 actor 和授权关系求出 authorized tenant
-> 再用 bind parameter 写入 transaction-local database context若直接做:
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;这是有标签的实验例外,不是生产模板。
运维人员
运维需要的不是“平时就是超级用户”,而是两条路径:
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 revokePostgreSQL superuser 可以绕过对象 ACL 和 RLS,访问敏感 catalog,执行服务器 文件/程序相关能力;它是信任根,不是普通管理员的方便模式。
还要区分 OS root、PostgreSQL superuser 和平台控制面:
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。
每个集成至少回答:
它获得什么数据和 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、备份和日志攻击面
用数据流而不是组件清单建模
“我们有 PostgreSQL、PgBouncer 和防火墙”不是威胁模型。先画流:
client
-> DNS / VIP / load balancer
-> HAProxy
-> PgBouncer
-> PostgreSQL primary / replica
-> WAL archive / backup repository
-> logs / metrics / traces对每条箭头问:
- 谁发起;
- 如何认证对端;
- 是否加密;
- 是否可以重放;
- 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 网络和跨区链路。
常见失败:
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:
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 可能携带配置、日志和样本数据。
因此备份安全至少包括:
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。
安全目标不是“少记录”,而是:
记录足以调查的 actor/action/resource/outcome/time/correlation
不记录不必要的 secret 和敏感 payload
限制谁能读、改、删
确保时间、完整性、保留和检索可用可用性也是安全属性
认证和授权控制也能造成拒绝服务:
- 外部 IdP 不可用导致所有新连接失败;
- CRL/OCSP 依赖超时;
- 密码轮换不同步导致连接风暴;
- HBA 错序锁死管理员;
- audit 全量记录填满磁盘;
- RLS policy 中的昂贵子查询放大每次访问;
- brute-force 占满认证和连接槽。
威胁模型要写 fail-open/fail-closed 和应急路径。不能为了“高可用”悄悄回退 到弱认证,也不能为了“安全”在没有管理恢复入口时一次性切断所有访问。
从攻击路径生成测试
把抽象威胁变成实验:
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 数据分级、租户边界与应急权限
分级决定控制,而不是标签颜色
一个实用分级至少回答:
| 维度 | 问题 |
|---|---|
| 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 安全不安全”,而是:
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 已被另一个租户使用”之类能够确认全局存在性的细节,除非 这是明确业务合同。
数据边界必须贯穿派生物
租户边界要追到:
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 至少包含:
触发条件 正常路径不可用且存在明确风险
批准者 谁能批准,单人还是双人
身份 独立账号,禁止共享
时限 自动过期
范围 cluster/database/action/source
证据 ticket/reason/session/action/outcome
约束 禁止删审计、禁止无关数据浏览
退出 revoke/terminate/rotate
复盘 为什么需要、正常能力缺什么仅把 superuser password 放进保险箱不够。取出后谁知道、已有 session 如何回收、 PgBouncer 是否仍接受、使用了哪些命令、何时换新,都必须可执行。
应急时的优先级
凭据疑似泄露时,一个保守序列:
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 章会展开事件指挥。本章先保证检测项和回收动作在平时可练。
最小威胁模型模板
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 和可重放证据。
本节检查表
[ ] 列出数据、凭据、备份、日志和控制面资产
[ ] 区分 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
- NIST SP 800-61 Rev. 3:Incident Response
- PostgreSQL 18:Database Roles
- PostgreSQL 18:Row Security Policies
- PostgreSQL 18:Schemas
- Pigsty:Security Considerations
返回本章目录 · 下一节:认证与连接准入 · 查看全书目录 · 查看索引中心