固若金汤:认证、授权与数据安全
23 固若金汤:认证、授权与数据安全
数据库安全不是在系统外面再围一堵墙,而是让每一次越过边界都留下可验证的 答案:
只检查其中一层会产生危险的“半安全”:
本章先建威胁模型,再沿认证、授权、行级安全、密钥和审计一路向内。最后把 第 22 章的 transaction pool 纳入模型:同一个 PostgreSQL backend 会先后 服务不同客户端,因此 session 级安全上下文不仅会“丢失”,还可能泄漏给下一 个租户。
本章目标
完成本章后,你应当能够:
- 用资产、主体、入口、信任跨越和失败后果编写数据库威胁模型;
- 区分终端用户、应用 login、effective role、object owner 与 break-glass;
- 从
pg_hba_file_rules解释 first-match,而不是凭配置片段猜认证结果; - 区分认证方法、密码存储、传输加密和服务器身份校验;
- 使用 SCRAM,理解 channel binding、密码轮换与客户端兼容边界;
- 说明
sslmode=require、verify-ca与verify-full分别证明什么; - 用
pg_stat_ssl、证书 SAN、HBA 和真实连接共同验收 TLS; - 把 LOGIN、group、owner、migrate、runtime、readonly 角色拆开;
- 正确使用 PostgreSQL 16+ membership 的
ADMIN、INHERIT与SET; - 设计 schema/table/sequence/function/default privilege 的最小权限;
- 识别
PUBLIC、owner、search_path、SECURITY DEFINER和预定义高权 角色造成的越权路径; - 为共享表设计 RLS 的
USING与WITH CHECK; - 解释 default-deny、permissive
OR、restrictiveAND与完整表操作边界; - 使用
FORCE ROW LEVEL SECURITY约束 owner,并明确 superuser/BYPASSRLS仍会绕过; - 通过事务级
SET LOCAL ROLE和 tenant context 支持 transaction pool; - 复现 session
SET跨客户端泄漏,并证明事务局部状态在提交后消失; - 设计生成、分发、双版本轮换、撤销和应急回收的凭据生命周期;
- 区分普通运行日志、对象/会话审计、平台审计和合规证据;
- 在 Pigsty 中声明用户、HBA 和接入层,再从渲染产物与运行事实反查;
- 对一个环境给出“通过、带例外通过、待整改或拒绝”的诚实安全结论。
前置与后续
前置:
- 第 6 章 开发规约 已处理参数绑定、受控
search_path、SECURITY DEFINER和 secret-free 交付; - 第 10 章 并发控制 已建立事务与 session 边界;
- 第 19 章 部署基线 已固定 Pigsty v4.5.0 四机 nonproduction sandbox;
- 第 20 章 高可用 已区分 data plane、control plane 与 break-glass;
- 第 21 章 备份体系与恢复演练 已说明备份、WAL、日志同样是敏感 数据载体;
- 第 22 章 服务接入 已证明 transaction pooling 会复用 backend 并保留某些 session 状态。
后续:
- 第 24 章把安全例外、owner、轮换、SOP 和审批纳入治理;
- 第 25 章把连接、认证失败、角色变更与审计事件接入可观测系统;
- 第 29、30 章处理复制/迁移/升级中的身份与双版本兼容;
- 第 31 章把泄露、越权、凭据失陷与取证放进事件响应;
- 第 32–35 章会再次约束备份、恢复、故障操作和抢救身份。
学习路径
顺序很重要。若先写一条 RLS policy、最后才问“tenant id 从哪里来”,就可能 把用户自己提交的 tenant id 原样写入 GUC,得到一套语法正确却可随意越权的 系统。
六层安全证明
| 层 | 要证明的问题 | PostgreSQL / Pigsty 证据 |
|---|---|---|
| 暴露面 | 哪些端口和网络能到达 | listener、防火墙/安全组、HAProxy、HBA |
| 传输 | 对端是谁、链路是否加密 | sslmode、CA/SAN、pg_stat_ssl、PgBouncer TLS |
| 认证 | login 是谁、凭据是否有效 | HBA first-match、SCRAM/cert/外部身份、认证日志 |
| 授权 | current role 能做什么 | role graph、ACL、owner、default privilege |
| 数据 | 哪些行可见、哪些新值可写 | RLS flag、policy、正负测试、FORCE RLS |
| 治理 | 谁能变更、撤销、调查 | inventory、审批、secret manager、audit/retention |
这六层不能互相代替。例如 PostgreSQL hostssl 只要求连接使用 TLS;客户端若
选择不校验证书名称,仍可能把密码发给错误的服务器。反过来,verify-full
只能验证连接到证书所代表的服务器,不能证明这个 login 应当读取某个租户。
角色分层
本章采用一个可复用的角色图:
职责:
| 角色 | LOGIN | 主要权限 | 明确不应拥有 |
|---|---|---|---|
| application login | 是 | 只允许切换到批准的 runtime role | owner、DDL、ADMIN OPTION |
| runtime | 否 | USAGE + 必要 DML | schema CREATE、TRUNCATE、BYPASSRLS |
| readonly | 否 | USAGE + SELECT | 写入、迁移 |
| migrate | 否 | 可切换到 owner | 日常服务流量、凭据 |
| owner | 否 | 拥有应用对象和 policy | 日常 LOGIN |
| break-glass | 独立 | 紧急高权动作 | 无审批、无时限、无审计 |
PostgreSQL 16 起,membership 自身有 ADMIN、INHERIT 与 SET 选项。
本章使用:
因此 test 登录后不会隐式得到 runtime 权限,但能在批准的事务中
SET LOCAL ROLE;它也不能把 runtime 身份再授予别人。
租户事务合同
共享表 RLS 的最小请求序列:
第三个参数 true 表示 transaction-local。提交或回滚之后,角色和 tenant
context 都不应继续生效。
表同时使用:
policy 分开描述读写:
USING 回答“旧行能否进入操作”;WITH CHECK 回答“新行版本能否存在”。
只写其中一边,常会允许把一行从本租户改到另一个租户,或者插入不可见数据。
正式实验
角色与对象:
RLS 观测:
连接池反例临时把入口 PgBouncer 从:
改为:
客户端 A 用 session 级 set_config(..., false) 设置 tenant A;关闭后,客户端 B
在同一个 backend、没有设置 tenant 的情况下仍读到 tenant A 的两行。这是
有意注入的失败,不是支持方式。
刷新 pool 后,四个事务依次在同一个 backend 上得到:
这证明隔离来自事务边界,而不是恰好换了 backend。随后四个 pool 参数精确
复位,并对三个节点执行 RECONNECT test 清理注入的 session 状态。
TLS 与认证观测:
凭据轮换使用一个不进入 PgBouncer userlist 的 direct-only synthetic role:
密码值、SCRAM verifier、raw userlist 和 private key 都没有进入证据。
生产结论
本章 formal sandbox 结论是:
这不是“Pigsty 不安全”的概括,而是对这一份 dev/test inventory 和运行状态的 精确判断。Pigsty 默认面向可信内网的开发、测试和演示;生产必须依据自己的 威胁模型收紧密码、网络、HBA、证书、审计与 secret 管理。
本章例外
在前四章下卷例外之外,本章保留:
本章目录
23.1 威胁模型与信任边界
23.2 认证与连接准入
23.3 角色与最小权限
- 23.3.1 login、group、owner 与 runtime role
- 23.3.2 schema、table、sequence、function 权限
- 23.3.3 默认权限、所有权迁移与越权路径
23.4 行级安全与连接池上下文
- 23.4.1 RLS policy、owner bypass 与强制 RLS
- 23.4.2 租户身份通过事务参数传递
- 23.4.3 transaction pooling 下使用
SET LOCAL - 23.4.4 验证复用连接不会泄漏上一个租户状态
23.5 密钥、审计与敏感信息
23.6 Pigsty 安全基线
23.7 实战:隔离两个租户
- 23.7.1 建立应用角色、迁移角色和只读角色
- 23.7.2 通过 PgBouncer 事务池验证 RLS
- 23.7.3 注入会话状态泄漏与越权访问并修复
- 23.7.4 输出权限矩阵、轮换证据与应急回收步骤
实验入口
lab-contract.md:权限、变更、证据和 reset 边界;requirements.json:机器验收合同;threat-model.json:资产、主体和信任跨越;role-contract.json:五类 synthetic role;tenant-contract.json:事务级租户上下文;security-adr.md:RLS 与连接池决策;topology.mmd:端点、角色和数据边界;task.sh:唯一安全入口;security-run.json:正式参考结果;negative-cases.json:二十个反例。
task.sh all 只重验既有证据,不登录应用身份、不改 role/password/pool/HBA/
certificate,也不删除 fixture。drill:security 和 reset:fixture 是两条
完全分离、精确守卫的路径。
参考资料
- PostgreSQL 18:Client Authentication
- PostgreSQL 18:pg_hba.conf
- PostgreSQL 18:Password Authentication
- PostgreSQL 18:libpq SSL Support
- PostgreSQL 18:Database Roles
- PostgreSQL 18:Row Security Policies
- PostgreSQL 18:Error Reporting and Logging
- PgBouncer:Features
- PgBouncer:Configuration
- Pigsty:Security Considerations
- Pigsty:HBA Rules
上一章:四通八达:服务接入、连接池与路由 · 返回下卷导读 · 下一章:纲举目张:SLO、SOP 与组织治理 · 查看全书目录 · 查看索引中心
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
返回本章目录 · 下一节:认证与连接准入 · 查看全书目录 · 查看索引中心
23.2 认证与连接准入
客户端拿到数据库连接之前,至少要连续通过五道关:
这五道关回答的问题不同。防火墙放行不等于 HBA 放行,HBA 选中
scram-sha-256 不等于密码正确,认证成功也不等于角色拥有
CONNECT,更不等于它可以读取业务表。安全评审必须逐层给出证据,不能用
“我连上了”概括全部连接准入。
23.2.1 pg_hba.conf 的匹配顺序与证据
HBA 是有序规则,不是规则集合
PostgreSQL 对一条新连接从上到下检查 HBA:
- 找到第一条在连接类型、数据库、用户和来源地址上都匹配的记录;
- 使用这条记录指定的方法认证;
- 认证失败就拒绝,不会继续尝试后面的记录;
- 没有任何记录匹配也会拒绝。
因此下面两段配置语义完全不同:
HBA 不是防火墙 ACL 的“最具体规则优先”,也没有失败后回退。官方文档明确 规定了 first-match 语义;任何生成器、模板或平台都不能改变这一点。 参见 PostgreSQL:客户端认证配置文件。
一条记录匹配哪些维度
常见 record type:
| 类型 | 传输条件 | 典型用途 |
|---|---|---|
local |
Unix-domain socket | 节点本地管理或 peer 认证 |
host |
TCP,TLS 与非 TLS 都可 | 仅当两种传输都明确允许 |
hostssl |
TCP 且已经建立 TLS | 业务和远程管理的常见下限 |
hostnossl |
TCP 且未使用 TLS | 显式拒绝或受控兼容例外 |
hostgssenc |
TCP 且使用 GSS 加密 | 采用 GSSAPI 的环境 |
匹配列还包括:
几个容易误判的细节:
all很宽,不表示“最末默认规则”;sameuser、samerole等数据库关键字有特定语义;- user 列中的
+role_name匹配该角色的直接或间接成员,而不是匹配字符串 前缀; - database 列匹配的是客户端请求的数据库;
- hostname 规则需要正反向名称解析,延迟和失败模式不同于 CIDR;
- replication 连接有专门的 database 关键字和权限要求;
hostssl只说明客户端到该 PostgreSQL listener 的这段链路用了 TLS。
如果入口是 PgBouncer,客户端首先连接的是 PgBouncer。此时:
这是两条独立的准入链。PostgreSQL 的 pg_hba.conf 不会替 PgBouncer
过滤客户端来源;PgBouncer 的 HBA 也不会自动约束绕过代理、直连
PostgreSQL 的流量。
pg_hba_file_rules 是解析证据
不要只读取模板文件。PostgreSQL 提供
pg_hba_file_rules,可把当前 HBA 文件解析为行:
它能证明:
- PostgreSQL 从哪些文件和行解析出规则;
- include 后的最终顺序;
- 方法、地址、选项是否符合预期;
- 是否存在语法或解析错误。
它不能单独证明:
- 某条规则可从目标网段实际到达;
- 防火墙、安全组、HAProxy 或 PgBouncer 是否放行;
- DNS 名称匹配是否如预期;
- 密码、证书或外部身份提供方是否可用;
- 连接最后究竟命中了哪条规则。
因此验收还要从允许与禁止的真实来源分别连接,并把时间、目标地址、目标 数据库、login、TLS 属性和结果关联起来。生产系统不应为了测试负例而从未知 公网来源扫描数据库;应使用预先批准的测试节点。
HBA 不负责对象授权
下面的连接可能通过 HBA 和 SCRAM,却仍被数据库拒绝:
反过来,CONNECT 只是进入数据库:
它没有授予:
- schema 的
USAGE; - table 的
SELECT; - sequence 的
USAGE; - function 的
EXECUTE; - 切换到某个业务角色的 membership。
这也是为什么 HBA 不能被称为“权限配置”。它选择认证方法并执行连接准入, 对象授权要在 23.3 单独证明。
安全变更顺序
HBA 变更采用“声明—解析—负例—正例—回滚”:
在 Pigsty 中,应同时检查 PostgreSQL 与 PgBouncer 的 HBA 声明和渲染产物。
23.6 会把这条流程映射到 pg_hba_rules、pgb_hba_rules 及默认规则。
23.2.2 SCRAM、证书与外部身份
先区分四件事
“数据库密码安全”常把四个问题混在一起:
| 问题 | 典型机制 |
|---|---|
| 服务器保存什么 | SCRAM verifier、外部身份映射、客户端证书映射 |
| 线上如何证明身份 | SCRAM exchange、certificate、GSS/SSPI、LDAP、OAuth |
| 链路是否加密 | TLS 或 GSS encryption |
| 客户端是否找对服务器 | CA chain + hostname/IP identity verification |
只把 password_encryption 设为 scram-sha-256,不会自动启用 TLS;只使用 TLS
也不会自动把数据库中旧的 MD5 verifier 变成 SCRAM。
SCRAM 的角色
PostgreSQL 使用 SCRAM-SHA-256 时,服务器保存的是 salted verifier,而不是 可直接用于登录的明文密码。新密码应在:
返回 scram-sha-256 的受控环境中设置。不要通过命令行参数、shell history、
CI 日志或 Git 文件传递明文:
环境变量也不是天然的 secret manager:它可能被子进程继承、被诊断工具采集, 或留在流水线元数据中。重点是限制创建、读取、传递和销毁它的主体与时间。
PostgreSQL 18 已将 MD5 密码支持标记为弃用。迁移时可以先把 HBA 目标方法改为 SCRAM-compatible 路径,再逐个重置用户密码生成 SCRAM verifier,并验证所有 驱动。官方迁移说明见 PostgreSQL:密码认证。
channel binding
SCRAM channel binding 把认证交换绑定到当前 TLS channel,降低凭据交换被代理 到另一条 TLS 会话的风险。支持它的 libpq 客户端可以要求:
这里两个选项不可互相替代:
部署前必须确认驱动版本、TLS 库和中间代理是否支持。不能因为服务端支持 SCRAM
就假设所有客户端都支持 SCRAM-SHA-256-PLUS。
本章沙箱的直连实验证明 verify-full + channel_binding=require 可以成功;
这是一条兼容性证据,不代表 PgBouncer 客户端入口也自动具备同样属性。
客户端证书
cert 认证由 TLS 客户端证书证明身份,通常还需用 map= 把证书主体映射到
PostgreSQL role。它适合:
- 节点间或服务间受管身份;
- 有成熟 CA、签发、吊销和轮换系统的环境;
- 不希望长期共享密码的管理链路。
它并不自动适合每个终端用户。必须解决:
- private key 存放与文件权限;
- 客户端证书分发;
- SAN/subject 与 role 的映射;
- 有效期和轮换重叠期;
- 离职、设备丢失与 CRL/OCSP;
- 代理终止 TLS 后如何继续传递可信身份。
如果 PgBouncer 终止客户端 TLS,PostgreSQL 后端看到的是 PgBouncer 的连接, 不能凭空看到原始客户端证书。身份终止点必须在架构图和审计模型中明确。
外部身份不是“无密码”捷径
PostgreSQL 还可以接入 LDAP、GSS/SSPI、PAM、RADIUS、OAuth 等方法,具体可用 范围取决于版本和构建。它们把一部分认证判断交给外部系统,但数据库仍需定义:
评审时要问:
- 外部主体如何唯一映射,是否会因重名或大小写碰撞映射错误;
- 身份提供方不可用时是 fail-closed 还是出现旁路;
- token/ticket 的 audience、issuer、有效期和撤销如何验证;
- 数据库本地 break-glass 是否独立保管;
- PgBouncer 是否支持该认证方法,还是需要
auth_query/代理集成; - 外部组变化多久才能反映到数据库 session;
- 已建立 session 在外部身份撤销后何时终止。
身份联邦减少的是一类凭据管理,不会消除 role graph、对象 ACL、RLS 和审计。
PgBouncer 的身份交付面
PgBouncer 需要知道如何验证 client login,并以何种 server identity 连接 PostgreSQL。常见入口包括:
这意味着“PostgreSQL 中创建了 LOGIN role”并不必然让 PgBouncer 接受该用户。 本章轮换探针刻意创建了一个未进入池认证面的临时 login:
这个负例证明两套身份面必须分别验收。不要为了让探针通过而临时把高权用户 加入 PgBouncer userlist。
23.2.3 TLS 验证、吊销与密钥轮换
sslmode 分别证明什么
libpq 的主要模式可理解为:
sslmode |
加密 | 验证 CA | 验证目标名称 | 适用判断 |
|---|---|---|---|---|
disable |
否 | 否 | 否 | 只用于明确受控的非 TLS 路径 |
allow |
不保证 | 不保证 | 不保证 | 兼容优先,不是生产安全基线 |
prefer |
不保证 | 不保证 | 不保证 | 默认兼容行为,不是证明 |
require |
是 | 通常不证明名称 | 否 | 防窃听,不充分防冒充 |
verify-ca |
是 | 是 | 否 | 对端由受信 CA 签发 |
verify-full |
是 | 是 | 是 | 生产客户端通常应达到 |
require 能加密,但若不核对服务器名称,客户端可能把密码交给持有另一张受信
证书或被错误路由的服务器。生产应用应优先使用 verify-full。libpq 的精确
行为和 root certificate 兼容细节见
PostgreSQL:SSL 支持。
名称验证依赖 SAN 和连接名
verify-full 校验的是连接参数中的 host 与证书身份。证书应在
Subject Alternative Name 中声明实际使用的 DNS name 或 IP:
若客户端使用 VIP、HAProxy 名称或 Kubernetes service name,证书必须覆盖这个 稳定入口;只给后端节点名签证书并不能验证入口名称。
本章沙箱节点证书包含 localhost、集群名、节点名、loopback 与节点 IP 的 DNS/IP SAN。正式实验从证书解析 SAN,再执行三种连接:
第三条负例和前两条正例同等重要。没有负例,无法排除客户端根本没有执行名称 校验。
看 pg_stat_ssl,但不要只看它
服务器端可把 session 与 TLS 属性关联:
正式直连观察到:
但 pg_stat_ssl.ssl=true 只证明 PostgreSQL 看到的这一跳使用 TLS。若拓扑是:
PostgreSQL 看到的后端连接自然是 ssl=false,不能据此断言客户端链路未加密。
反之,后端 TLS 为 true 也不能证明 client-to-proxy 使用 TLS。两跳要分别测。
CA、CRL 与 private key
服务端最少要治理:
- server certificate;
- server private key;
- trusted client CA(若验证客户端证书);
- CRL 或其他撤销机制;
- 文件 owner、mode 和可读取主体;
- reload/restart 语义;
- 到期时间、提前轮换窗口和告警。
PostgreSQL 要求 private key 权限受到严格限制。本章节点上的服务端 key 为
0600。这只是静态权限证据,还需确认备份、配置管理缓存、工单附件和监控
采集器没有复制密钥。
沙箱的 CRL file/dir 未配置,因此不能声称已经具备客户端证书撤销闭环。证书 有效期很长也不等于安全或不安全;必须根据签发自动化、暴露面和撤销能力制定 生命周期,而不是只看 expiry date。
双版本轮换,而不是瞬间替换
CA、服务器证书和客户端证书/密码轮换都应保留重叠窗口:
对于 CA:
对于密码:
PostgreSQL role 只有一个当前 password verifier,没有天然的“双密码同时有效”。 应用侧重叠通常要借助两个 login、连接池分批切换,或把数据库密码变更与快速 客户端 rollout 精确协调。
凭据撤销不等于 session 撤销
本章做了一个容易被忽略的实验:
结论是:
若处置的是泄露或人员离职,还要:
- 在应用池和代理层停止新借用;
- 找出目标 role/session;
- 评估事务影响后终止连接;
- rotate downstream secret;
- 检查复制、备份、日志和导出物;
- 保存不含秘密的取证证据。
本章沙箱的诚实结论
形式化验收同时发现:
| 项目 | 运行事实 | 结论 |
|---|---|---|
| PostgreSQL TLS | 开启,最低 TLS 1.2 | 基础能力存在 |
直连 verify-full |
成功,错误名称失败 | 名称校验可用 |
| SCRAM channel binding | 直连成功 | 该客户端路径兼容 |
| 业务 HBA | 内网存在 host 规则 |
非 TLS 直连仍可成功 |
| PgBouncer client TLS | 禁用 | 客户端到池入口不加密 |
| PgBouncer backend | 本地 Unix socket | 该跳不使用 TLS,符合本地链路事实 |
| CRL | 未配置 | 吊销闭环缺失 |
所以沙箱适合验证机制,不满足本章定义的生产安全门槛。正确结论不是因为发现 缺口就隐藏实验,而是输出:
下一节在已经确定 login 身份之后,继续回答 effective role 与对象权限问题。
上一节:威胁模型与信任边界 · 返回本章目录 · 下一节:角色与最小权限 · 查看全书目录 · 查看索引中心
23.3 角色与最小权限
认证确认的是 login identity,授权判断的却是当前 effective role。把应用密码
直接挂在对象 owner 身上,看起来省去了一层 SET ROLE,实际上把“能够连接”
和“能够改变安全边界”绑在了同一个身份上。
本节的目标是建立一条清晰的权限链:
每一条边都要有理由,每一个高权角色都应尽量不可登录。
23.3.1 login、group、owner 与 runtime role
PostgreSQL 只有 role 这一种主体
PostgreSQL 的 user 和 group 都建立在 role 上:
CREATE USER 只是默认带 LOGIN 的语法别名。所谓 group role 通常只是
NOLOGIN role,用 membership 聚合权限。
关键属性包括:
正常应用身份通常全部关闭高权属性:
VALID UNTIL 只约束密码认证的有效期,不会让既有 session 自动断开,也不
约束所有外部认证方式。它是凭据控制的一部分,不是完整账户生命周期。
四类角色不要合并
本章使用:
| 类型 | LOGIN | 用途 | 为什么拆开 |
|---|---|---|---|
| workload login | 是 | 认证应用实例 | 可单独轮换、禁用和归因 |
| runtime | 否 | 正常 DML | 不拥有对象,不做 DDL |
| readonly | 否 | 受控查询 | 与写路径独立授权、撤销 |
| migrate | 否或临时身份切入 | 发布窗口 | 不进入日常流量 |
| owner | 否 | 拥有 schema、table、policy | 隔离隐式 owner 权力 |
| break-glass | 独立 | 限时应急 | 不属于应用 role graph |
推荐图:
不推荐:
owner 的能力来自所有权,不完全来自 ACL。撤销 table 上的 ALL 不能撤销
owner 的 ALTER、DROP、授权和 policy 管理能力。要收回这些能力,必须改变
owner 或改变运行身份。
session_user 与 current_user
连接建立后:
初始通常相同。执行:
之后:
审计时应保留两者。只记录 current_user=app_runtime 会丢失是哪个 workload
login 使用了该能力;只记录 login 又可能误判 SQL 实际以何权限执行。
membership 的三个开关
PostgreSQL 16 起,一条 role membership 有三个独立选项:
语义:
| 选项 | 问题 | 本章默认 |
|---|---|---|
ADMIN |
member 能否继续授予/撤销该 membership | FALSE |
INHERIT |
member 是否自动使用目标角色权限 | FALSE |
SET |
member 能否 SET ROLE 到目标角色 |
TRUE |
这种组合要求应用显式进入受控事务:
如果 INHERIT TRUE,login 在没有 SET ROLE 时就可能使用 runtime ACL,破坏
“没有声明上下文就失败”的设计。如果 SET FALSE,即便是 member 也不能切换
到该 role。完整语义见
PostgreSQL:角色成员关系 和
SET ROLE。
检查 membership 不应只看成员名称:
还要检查 role 自身属性:
SET LOCAL ROLE 的边界
SET LOCAL 只在事务中有局部效果:
它特别适合 transaction pooling,因为角色状态在事务结束时回收。应用不能把 切换角色与业务 SQL 分在两个独立事务里:
第二个事务可能落在不同 backend,且局部角色早已消失。23.4 会把 role 与 tenant context 放进同一个事务合同。
23.3.2 schema、table、sequence、function 权限
权限是一组相互独立的门
一条:
至少受这些条件影响:
因此“给了表权限却仍报 permission denied”并不奇怪。应从外到内定位,而不是
直接 GRANT ALL。
database 与 schema
database 常见权限:
schema 常见权限:
runtime 通常只需:
USAGE 不会自动授予表权限,CREATE 却是一条重要越权路径:若可写 schema
出现在高权函数的 search_path 前部,攻击者可能创建同名函数、operator 或
对象劫持解析。
安全基线通常包括:
但执行前要盘点依赖。已有应用可能把 public 当作共享可写工作区,直接撤销
会暴露历史设计问题。先发现、迁移,再收紧。
table 与 column
table 权限主要有:
不要把 TRUNCATE 当成普通 DELETE。它绕过逐行语义,不触发 ON DELETE
trigger,且不受 RLS policy 逐行过滤。runtime 通常不应拥有它。
REFERENCES 允许创建引用约束,TRIGGER 允许在表上创建 trigger,二者也
不属于日常 DML。最小 runtime grant 示例:
如果只授权部分列:
要同时检查 view、function、COPY、returning expression 和新增列的暴露方式。 列级 grant 不是数据脱敏系统。
sequence 不随 table 自动授权
使用 identity/serial 的 INSERT 可能还要访问 sequence:
table 上的权限不会自动扩展到 sequence。常见症状是:
USAGE 允许 currval/nextval;SELECT 涉及 currval;UPDATE 可影响
setval。runtime 通常不应随意 setval。
function 默认可执行
新 function/procedure 通常会把 EXECUTE 授给 PUBLIC,除非创建者通过
默认权限改变。对安全敏感函数,应在同一事务内创建和撤销:
不要让函数在“已创建但仍对 PUBLIC 开放”的窗口被调用。
SECURITY INVOKER 使用调用者权限,是默认和首选。SECURITY DEFINER 使用
函数 owner 权限,必须:
- owner 不可登录且不是不必要的 superuser;
- 固定安全
search_path,把pg_catalog和受控 schema 放入; - 避免引用可被调用者替换的对象;
- 撤销
PUBLIC EXECUTE; - 验证参数、tenant identity 和动态 SQL;
- 对返回错误与日志进行脱敏;
- 定期审计 owner 和函数定义。
官方安全写法见 PostgreSQL:CREATE FUNCTION。
PUBLIC 是隐式全体角色
每个角色都隐式属于 PUBLIC。审计 ACL 时不能只搜索显式 app_runtime:
这解释了为什么“ACL 里没有这个用户”不等于没有权限。
用权限函数做行为验收
ACL 文本适合审计来源,has_*_privilege 适合回答结果:
二者都不能替代实际负例。例如 has_table_privilege(..., 'SELECT')=true 不会
告诉你 RLS 最终能看到哪些行。
23.3.3 默认权限、所有权迁移与越权路径
default privilege 只影响未来对象
下面语句不是给现有表授权:
它表示:
三个限定都很重要:
- future objects,不追溯现有对象;
- creating role 是
app_owner; - schema scope 是
app。
如果迁移工具实际以 release_login 创建对象,而没有先
SET LOCAL ROLE app_owner,owner 和 default privilege 都可能偏离设计。
角色 membership 的权限不会自动替创建者的默认权限生效。官方细节见
PostgreSQL:ALTER DEFAULT PRIVILEGES。
一个完整初始化事务通常同时处理当前和未来对象:
实际语句需按应用操作矩阵裁剪,不能机械复制。
所有权迁移是安全迁移
把一个旧 login 改为 NOLOGIN 之前,要盘点它拥有的对象:
还要覆盖:
REASSIGN OWNED BY legacy_app TO app_owner 只作用于当前 database 中的对象,
其他数据库要分别执行。DROP OWNED 会撤销 grant、并可能删除对象,是破坏性
动作,不能拿来“顺手清理”生产账号。
推荐迁移:
常见越权路径
最小权限评审至少检查:
| 路径 | 风险 |
|---|---|
SUPERUSER / BYPASSRLS |
绕过大多数数据库内控制 |
CREATEROLE / membership ADMIN |
扩展角色图 |
| owner login | 日常凭据可改变对象和 policy |
INHERIT TRUE |
未显式进入业务角色也能使用其 ACL |
writable search_path schema |
对象名称劫持 |
SECURITY DEFINER + PUBLIC EXECUTE |
以 owner 权限执行攻击输入 |
table owner without FORCE RLS |
owner 默认绕过 RLS |
pg_read_all_data / pg_write_all_data |
跨 schema 广泛读写 |
pg_read_server_files |
读取数据库服务器可见文件 |
pg_write_server_files |
写入服务器文件 |
pg_execute_server_program |
执行服务器程序 |
| extension install/control | 引入高权代码 |
| untrusted procedural language | 数据库进程内执行不受信代码 |
预定义角色是方便的能力包,不是低风险标签。它们随版本演进,升级评审必须 重新阅读目标版本的 预定义角色说明。
本章权限矩阵
正式实验收敛到:
| 行为 | raw login | runtime | readonly | owner | break-glass |
|---|---|---|---|---|---|
schema USAGE |
否 | 是 | 是 | owner | 是 |
schema CREATE |
否 | 否 | 否 | 是 | 是 |
table SELECT |
否 | 是 | 是 | owner | 是 |
INSERT/UPDATE |
否 | 是 | 否 | owner | 是 |
DELETE/TRUNCATE |
否 | 否 | 否 | owner 能力 | 是 |
| 管理 RLS policy | 否 | 否 | 否 | 是 | 是 |
| 绕过 RLS | 否 | 否 | 否 | FORCE 后否 | 是 |
五个 synthetic role 在演练结束时全部:
负例实际得到:
这比一张手工填写的权限表更强,因为它同时证明“应该成功的能成功”和“不该 成功的确实失败”。下一节再把 table ACL 与 RLS 行边界组合起来。
上一节:认证与连接准入 · 返回本章目录 · 下一节:行级安全与连接池上下文 · 查看全书目录 · 查看索引中心
23.4 行级安全与连接池上下文
共享表多租户系统最常见的事故不是 SQL 不会写,而是某一条 SQL 忘了写:
RLS(Row-Level Security)把这个条件从每条业务 SQL 下沉为表级策略。但这
只是第一步。若 $tenant 来自用户可篡改的请求字段,或者作为 session 状态
残留在 transaction pool 的 backend 上,policy 本身完全正确,系统仍会越权。
安全链必须完整:
23.4.1 RLS policy、owner bypass 与强制 RLS
RLS 是 ACL 之后的行过滤
RLS 不替代普通权限。一次查询要先有 schema/table 权限,再由 policy 决定 哪些行可见:
启用:
如果没有适用于当前 command/role 的 policy,PostgreSQL 使用 default-deny:
这是一项很有价值的 fail-closed 属性。但如果 RLS 根本没有启用,已经创建的 policy 不会生效。验收要同时检查:
以及:
USING 看旧行,WITH CHECK 看新行
四类 DML 的核心语义:
| command | 旧行可见/可操作 | 新行可写入 |
|---|---|---|
SELECT |
USING |
不适用 |
INSERT |
不适用 | WITH CHECK |
UPDATE |
USING |
WITH CHECK |
DELETE |
USING |
不适用 |
多租户 UPDATE 必须约束两边:
只写 USING 容易忽略“修改后的行能否移到另一个租户”;只考虑
WITH CHECK 又没有明确旧行选择边界。PostgreSQL 对某些 policy 会在省略
WITH CHECK 时复用 USING,但安全代码应把双边意图写清楚。
还要覆盖复杂命令:
UPDATE ... RETURNING同时涉及 SELECT/UPDATE policy;INSERT ... ON CONFLICT会触发 SELECT、INSERT,走 update path 时还会 触发 UPDATE policy;MERGE按实际 action 应用相关 policy;BEFORE ROWtrigger 可先修改新行,再执行WITH CHECK;- policy 不适用于
TRUNCATE和REFERENCES这类整表操作。
因此 runtime 还必须在 ACL 层失去 TRUNCATE。完整 command matrix 见
PostgreSQL:CREATE POLICY。
多个 policy 怎样组合
policy 默认为 PERMISSIVE,多个适用 policy 用 OR:
RESTRICTIVE policy 用 AND,并与至少一个 permissive policy 组合:
[ P_{\text{effective}}
(P_1 \lor \cdots \lor P_n) \land R_1 \land \cdots \land R_m ]
这意味着“再加一条 permissive policy”是在扩大可见集合。比如:
第二条会让 app_runtime 看见全部行,不是对第一条的补充限制。策略评审必须
查看某 command/role 的完整 policy 集合,而不是逐条认为“看起来都合理”。
owner、superuser 与 BYPASSRLS
默认情况下:
要让 owner 也受 policy 约束:
本章把 owner 设为 NOLOGIN,仍启用 FORCE:
正式测试证明:
FORCE 不是 superuser containment。若 threat model 包含数据库管理员恶意行为, 要依赖组织职责分离、主机/平台控制、不可变审计和密钥治理,不能只依赖 RLS。
row_security=off 不是旁路
这个设置常被误读:
它不会给普通角色绕过 RLS;当查询结果本应被 policy 过滤时,它会报错。这样
备份工具可以避免悄悄导出不完整数据。本章 runtime 负例得到
SQLSTATE 42501,正好证明它不是绕过按钮。
约束与 policy 的隐蔽信道
primary key、unique 和 foreign key 等 referential integrity 检查会绕过 RLS 以维护一致性。攻击者可能从错误差异推断“不可见行是否存在”:
多租户唯一性设计应明确范围:
如果业务要求全局唯一但不能暴露存在性,应用错误映射、接口语义、重试和审计 都要一起设计,RLS 本身不能消除这个 channel。
policy 表达式若查询其他表,还可能出现并发 snapshot/race 和权限问题。优先 让 policy 只依赖当前行与稳定、简单的事务上下文;复杂授权图要专门做并发 安全评审。官方 RLS 文档详细说明了 referential-integrity 与并发风险: PostgreSQL:行安全策略。
view 可能改变 RLS 主体
普通 view 默认按 view owner 的权限访问底层 relation,底层 RLS 也默认使用 view owner 的 policy。PostgreSQL 支持:
此时底层权限与 RLS 使用调用者身份。不能因为 base table 有 RLS,就假设所有
view 路径都等同于直接访问;应检查 view owner、security_invoker、
security_barrier、函数安全属性和 grant。参见
PostgreSQL:CREATE VIEW。
23.4.2 租户身份通过事务参数传递
tenant id 必须来自授权结果
下面的 API 是危险的:
如果应用不经验证就执行:
RLS 只会忠实地允许 $1 对应租户。正确来源应是:
请求 body、URL path 或 header 中的 tenant id 可以作为“用户想访问谁”,但 必须与服务端授权集合比对,不能成为信任根。
还要明确 RLS 的 threat boundary:
| 风险 | 共享 login + 可设置 tenant GUC 是否能防 |
|---|---|
开发者漏写 tenant WHERE |
能 |
| ORM 某条查询未注入 scope | 能 |
| 普通 readonly 报表误查全表 | 能 |
| 外部用户篡改请求 tenant,应用正确鉴权 | 能 |
| 应用进程被攻陷,可任意设置 GUC | 不能 |
| 数据库 login 凭据泄露,可选择任意 tenant | 不能 |
| superuser / BYPASSRLS 恶意访问 | 不能 |
如果必须防住被攻陷的单个 tenant workload,就应使用每租户 login/role、独立 数据库或 schema,或者让数据库从不可伪造的连接身份映射 tenant,而不是让 共享 login 自报 tenant id。
自定义 GUC 是载体,不是鉴权器
本章 helper:
设计意图:
missing_ok=true:缺少设置时返回NULL;NULLIF(..., ''):显式 reset/空值也转成NULL;- cast to
uuid:非法格式直接失败; STABLE:一条 statement 中按稳定表达式处理;- 固定
search_path:不从可写 schema 解析对象。
policy:
当 context 缺失:
正式观察:
这叫 fail-closed。它仍不是鉴权器,因为能够执行 set_config 的 session 可以
尝试设置任意值。可信度来自调用它之前的身份授权流程。
policy helper 的安全属性
helper 能保持 SECURITY INVOKER 就不要使用 SECURITY DEFINER。如果必须从
授权表查询:
- 使用不可登录、最小权限 owner;
- 固定
search_path; - schema-qualified 所有对象;
- 收紧
PUBLIC EXECUTE; - 避免动态 SQL;
- 处理并发快照和授权撤销延迟;
- 对高频查询评估性能;
- 为输入与返回值建立负例。
把一个复杂的 definer function 塞进每行 policy,可能同时引入越权路径和 严重性能成本。
context 还应携带什么
根据审计需求,同一事务还可设置:
不要把 access token、password、完整个人信息或业务秘密放进 GUC。它们可能 出现在:
pg_stat_activity;- error context;
- statement/config logs;
- diagnostics;
- monitoring snapshots。
context value 应短小、不可变、可关联,并有明确的数据分类。
23.4.3 transaction pooling 下使用 SET LOCAL
为什么 session SET 会泄漏
PgBouncer transaction pooling 的基本语义:
如果 client A 执行 session-level:
第三个参数 false 让值在 backend session 中持续。client A 断开不等于
PostgreSQL backend 断开;client B 复用它时可能继承 tenant A。
不能把 server_reset_query = DISCARD ALL 当成当然成立的保护。PgBouncer 在
transaction pooling 下并不默认依赖每次事务后的 session reset,且所有入口、
版本和配置必须分别证明。支持矩阵见
PgBouncer features 与
PgBouncer configuration。
完整事务合同
支持的请求序列:
四条不可拆:
- 必须显式
BEGIN; - effective role 与 tenant context 在同一事务设置;
- 所有依赖它们的业务 SQL 在同一事务;
- 任一错误都
ROLLBACK,不能把 aborted transaction 放回应用池。
SET LOCAL ROLE 和 set_config(..., true) 都在 commit/rollback 后结束。即使
下一个请求复用同一个 backend,也会回到 login 身份和无 tenant context。
应用框架的实现位置
不要让每个 repository method 自己记住设置上下文。应在统一 transaction boundary 中:
需要验证框架是否会:
- 因 autocommit 把每条语句拆成独立事务;
- 在 transaction callback 之前执行隐式查询;
- retry 时更换连接但漏掉初始化;
- nested transaction/savepoint 时改变上下文;
- 把 readonly 与 runtime role 混用;
- 在异步任务/streaming cursor 生命周期中提前提交;
- 把
SET LOCAL参数当成 SQL identifier 拼接; - 发生 timeout/cancel 后未 rollback。
tenant value 必须参数绑定;role 名称不能直接来自用户输入。本章只允许固定
allowlist 中的 runtime 或 readonly。
长事务与租户上下文
transaction-local 并不意味着请求可以无限长。长事务会:
- 长时间占用 pool server connection;
- 延迟授权撤销生效到下一事务;
- 放大 idle-in-transaction 风险;
- 持有 snapshot/lock,影响 vacuum;
- 让一次错误上下文影响更多工作。
应为业务事务设置 deadline,拆分批处理,并让授权变化的 SLA 与最长事务时间 一致。
prepared statement 与缓存
transaction pool 支持哪些 prepared statement、temporary object、advisory lock 和 session feature,取决于 PgBouncer 版本与配置。安全原则不变:
不要把“同名 prepared statement 可复用”误解为角色或 tenant context 也可 跨事务复用。权限、RLS 与当前 GUC 必须在执行时接受测试。
23.4.4 验证复用连接不会泄漏上一个租户状态
让复用成为确定事件
随机并发测试可能碰巧用了不同 backend,从而给出假阴性。本章在确认沙箱无
活动业务 client 后,临时把 test 池从:
改为:
这样先后两个 client 会确定复用同一 server connection。实验在 finally
中精确恢复四项原值,并对三节点执行受控 RECONNECT test。这类临时调整只能
用于明确的 nonproduction 空闲池;不能在生产流量中强行把 pool size 改成 1。
先证明漏洞存在
反例:
实测:
这不是理论警告,而是一条跨逻辑客户端的数据泄漏。它也说明“client disconnect 时清理状态”的假设在 transaction pool 中为什么错误。
再证明合同成立
清理实验 backend 后,用 transaction-local 合同依次执行:
四次都复用了 PID 72578,结果:
| 请求 | effective tenant | 可见行 |
|---|---|---|
| A | tenant A | 2 条 A |
| missing after A | NULL |
0 |
| B | tenant B | 2 条 B |
| missing after B | NULL |
0 |
这里最强的证据不是 A/B 正例,而是两个 missing-context 负例:它们证明前一个 事务的 tenant 状态没有留在同一 backend。
完整负例矩阵
共享表至少要自动测试:
| case | 预期 |
|---|---|
| tenant A SELECT | 只返回 A |
| tenant B SELECT | 只返回 B |
| context missing | 0 rows |
| malformed context | 格式错误 |
| A INSERT B row | policy violation |
| A UPDATE row into B | policy violation |
| readonly INSERT | permission denied |
| raw login without effective role | permission denied |
runtime TRUNCATE |
permission denied |
| runtime disable RLS | permission denied |
| owner without context under FORCE | 0 rows |
row_security=off as runtime |
error, not bypass |
| superuser break-glass | all rows, separately audited |
| same backend after commit | no previous tenant |
| same backend after rollback/error | no previous tenant |
本章已验证其中核心 12 类并由 validator 校验 SQLSTATE;生产实现还应加入应用 驱动层的 rollback、timeout、cancel、retry 和并发测试。
不要只断言行数
若两个租户恰好都有两行,错误地返回 B 也会满足 count(*)=2。证据至少包括:
敏感字段不应为了证明隔离而导出。本章证据只投影 synthetic tenant/account id 与 display name,并明确:
上线门槛
RLS 上线前必须同时满足:
RLS 是强大的纵深防御,但只有当身份来源与连接生命周期同样严格时,才会成为 真正的租户边界。
上一节:角色与最小权限 · 返回本章目录 · 下一节:密钥、审计与敏感信息 · 查看全书目录 · 查看索引中心
23.5 密钥、审计与敏感信息
数据库安全材料不只是一串密码:
其中任何一项若进入 Git、命令行历史、日志、监控标签或实验产物,后续的 权限设计都可能失效。另一方面,为了“绝不记录敏感信息”而关闭所有日志,也 会让越权事件无法发现和调查。
本节处理的是这组张力:秘密必须最小暴露,安全行为必须留下足够而受保护的 证据。
23.5.1 凭据生成、存放、轮换和撤销
先建立 secret inventory
每类 secret 都要有 owner 和生命周期:
| 字段 | 要回答的问题 |
|---|---|
| identity | 它代表哪个人、服务或组件 |
| scope | 能访问哪些入口、数据库和角色 |
| source | 谁生成,熵和算法是否合格 |
| storage | secret manager、HSM、受限文件还是其他载体 |
| delivery | 哪个 workload 如何获得 |
| readers | 哪些人、服务账户和进程可读 |
| lifetime | 创建、启用、到期和最大使用时长 |
| rotation | 是否支持重叠版本,多久轮换 |
| revocation | 如何阻止新认证 |
| session eviction | 如何处理既有连接 |
| downstream | 是否复制到代理、CI、备份或灾备 |
| evidence | 如何证明已完成且不导出秘密 |
如果连“有几份副本”都不知道,就无法声称秘密已经撤销。
生成
机器凭据应由密码学安全随机源生成,避免:
长度和字符集要兼容客户端、URI、配置格式与 secret manager;不要为了规避 转义问题而把熵降得过低。优先通过结构化参数或独立字段传递,避免把密码拼进 连接 URI。
证书与 key 的生成还要固定:
CA private key 与数据库 server key 不应由同一批日常运维主体任意读取。
存放和交付
首选工作流:
如果使用文件:
- 明确 owner/group;
- 通常使用
0600或经过评审的0640; - 目录同样不可遍历;
- 不写入镜像层、共享 volume 或备份;
- 不把内容输出到 diagnostics;
- 用完安全删除临时副本,并考虑文件系统/快照语义。
.pgpass 要求严格权限,它适合受控本地客户端,不是企业 secret manager。
环境变量适合某些运行时注入,但必须处理进程继承、crash dump、support bundle
和调度平台元数据风险。
最危险的交付路径通常很方便:
URI 可能进入 process list、shell history、trace、错误和工单。即使工具会 隐藏部分内容,也不应把安全性押在每个中间层都正确脱敏。
数据库密码变更
交互式 psql 可使用:
它在客户端提示密码并发送 verifier,避免明文出现在命令历史和 server log。
官方说明见 psql \password。直接执行:
可能让明文进入 client history 或 server log;PostgreSQL 官方也明确警告
这一点,参见 ALTER ROLE。
自动化系统应通过 secret-safe API、受控 stdin/fd 或专门管理函数完成,且 验证流水线不会回显命令与异常。
轮换是状态机
密码轮换不能只有“ALTER 成功”:
每个状态需要证据与回滚条件。若 PostgreSQL role 只能保存一个当前 verifier, S2 与 S3 之间的兼容窗口可以采用:
- 蓝绿两个 login role;
- 应用小批量快速 rollout;
- 代理/身份系统支持的双版本机制;
- 计划内短暂重连窗口。
不要假装一个 role 可以同时接受两个普通 PostgreSQL 密码。
撤销新认证与终止旧会话是两件事
本章的受控临时 login 实测:
因此应急撤销至少有两条动作:
以及在识别范围并评估事务影响后:
第二条是有破坏性的:会中断事务,产生 commit outcome uncertainty,并可能 触发重连风暴。必须先阻止代理和应用继续获取旧 secret,再终止既有 session。
代理层也要轮换
PgBouncer 的 auth_file、auth_query、auth_user 和 server login 都可能
持有或派生认证材料。轮换时要明确:
哪一段在变。更新 PostgreSQL verifier 后:
- PgBouncer 是否缓存旧认证材料;
- 是否需要
RELOAD; - 是否需要
RECONNECTserver pools; - 已有 client connection 是否继续使用;
- 所有 PgBouncer 节点是否完成;
- 直连和池化入口是否给出一致的允许/拒绝结果。
本章临时 role 直连新认证成功,但池入口拒绝,因为它没有进入 PgBouncer 认证面。这是预期的最小暴露,不是轮换失败。
证书和 CA 轮换
证书轮换同样要区分:
PostgreSQL reload 新 server certificate 不会让所有现有 TLS session 自动 重新握手。PgBouncer 两侧又各有 TLS 状态。验收必须建立新连接并核对证书 fingerprint/issuer/SAN,而不是只看文件 mtime。
CA rollover 先扩充 trust bundle,再切 server/client certificate,最后等 fleet 全部迁移后删除旧 CA。倒序操作会把尚未更新的客户端全部拒绝。
应急凭据
break-glass credential 应:
- 与日常 workload secret 分开;
- 双人或受审批取用;
- 短时激活;
- 不进入普通自动化;
- 使用后立即轮换;
- 关联 incident/change id;
- 从数据库外部收集不可篡改证据;
- 定期演练“能取出、能使用、能回收”。
一个从未测试、到事故时才发现过期的应急密码,不是恢复能力。
secret-free 证据
证明轮换无需保存 secret:
本章证据显式拒绝:
它检查“secret signature 不存在”,同时对证据文件使用 0600。文件权限不能
替代内容最小化;二者都要做。
23.5.2 审计目标、日志范围与访问控制
从调查问题反推记录
先定义要回答的问题:
“记录所有 SQL”并不能自动回答这些问题,反而可能产生无法检索的海量敏感 数据。
四类证据互相补充
| 来源 | 擅长回答 | 局限 |
|---|---|---|
| PostgreSQL standard log | 连接、错误、慢 SQL、DDL、运行事件 | 不是完整对象审计 |
| pgAudit | 结构化 session/object audit classes | 有容量成本,superuser 不可可靠自审 |
| Pigsty/config repository | 谁声明、评审、发布了配置 | 不证明运行实例已收敛 |
| host/proxy/secret manager/IdP | 网络入口、secret 读取、外部身份 | 不知道 SQL 对象语义 |
还应关联:
共享应用 login 下,数据库看不到每个终端用户,应用必须提供受信 actor/request
关联。不要把用户可自行填写的 application_name 当成强身份。
standard log
PostgreSQL 18 可分别记录 connection receipt、authentication、 authorization、setup duration,以及 disconnect。还可记录:
- error statement;
- DDL/DML/all statement classes;
- duration threshold 或 sampling;
- lock/recovery/autovacuum/checkpoint;
- SQLSTATE、session id、transaction id、query id;
- application/database/user/client address。
log_line_prefix 至少要支持跨行关联,例如:
具体字段按日志格式和数据分类选择。若使用 JSON/CSV,仍要确保 collector、 rotation、磁盘满和转发失败被监控。PostgreSQL logging collector 为避免丢 消息可能在落后时阻塞 backend;syslog 则可能选择丢消息。这也是容量设计, 不是简单开关。
pgAudit 能补什么
pgAudit 将行为分为 READ、WRITE、FUNCTION、ROLE、DDL、MISC
等审计类,并能提供更适合对象审计的记录。部署要点:
只安装 package 或只执行 CREATE EXTENSION 都不等于审计已经运行。官方项目
说明见 pgAudit。
不要默认 pgaudit.log=all:
- 高频 SELECT 会产生巨大日志;
- bind parameter 可能含 PII/secret;
- 日志 IO/转发可能成为负载瓶颈;
- 噪声可能淹没 role/DDL 等关键事件;
- retention 成本和访问面迅速扩大。
应从审计目标选择 session classes 或 object audit,并对新表纳入策略做持续 检查。
superuser 不能可靠地审计自己
pgAudit 官方明确指出,不能可靠审计 superuser。superuser 能改变设置、停用 扩展、修改日志路径或干预本机数据。解决思路不是再加一条数据库内 trigger, 而是:
“不可变”要具体:谁拥有 bucket retention policy、谁能删除 collector、日志 在源端滞留多久、断网时如何缓冲、时间如何同步,都要写进控制设计。
角色和授权变更
高价值事件:
仅记录成功 DDL 不够。还需:
- 失败尝试;
- 变更前后投影或配置 diff;
- 审批 id;
- 执行主体;
- 节点收敛状态;
- 正负验收;
- 回滚结果。
日志本身是敏感数据
日志可能包含:
因此要实施:
- 最小读权限,读日志本身也审计;
- 传输和静态加密;
- retention 与合法删除;
- 环境/租户隔离;
- 索引系统访问控制;
- 防止下载到个人设备;
- incident legal hold;
- collector health 与 ingestion gap 告警。
本章环境结论
沙箱运行事实:
它能支持实验和部分运维诊断,不能被描述为完整生产审计。生产 gate 因此保留
pending,不是把“有日志”写成“满足审计要求”。
23.5.3 参数、SQL 文本与日志脱敏
参数绑定解决注入,不保证不落日志
应用正确使用:
能避免把 $1 当 SQL 语法解释,但 extended query protocol 的 Bind value
仍可能被 statement/duration/audit/error logging 记录。安全评审必须把:
当成两个问题。
PostgreSQL 的参数日志开关
PostgreSQL 18:
精确行为见 PostgreSQL:错误报告与日志。
本章沙箱:
含义是:
- error path 默认不附 bind values;
- 非 error 的 statement/duration logging 路径可能保留完整 bind values。
因此它被标记为生产差距。不能只因为 error 参数为 0 就得出“参数不进日志”。
截断不是脱敏
把每个参数截断到 64 bytes 仍可能完整暴露:
0 能阻止特定 PostgreSQL log path 记录 bind 参数,但:
- SQL literal 仍在 statement text;
- application log 可能记录参数;
- pgAudit/extension 行为要单独验证;
- error message 可能引用业务值;
- trigger/function 自己可能
RAISE LOG; - proxy/APM/driver trace 可能复制 SQL。
脱敏必须是端到端数据流评审。
不要把秘密写进 SQL literal
高风险语句:
即便业务表有 RLS,这些 literal 也可能进入 statement、DDL、audit、客户端 历史或 trace。对于 credential provisioning,使用专门的 secret-safe 通道; 对于业务 secret,使用参数绑定并缩小数据库日志范围。
在源头分类字段
建议把请求数据分成:
| 类别 | 示例 | 日志策略 |
|---|---|---|
| public operational | version、region、status | 可结构化记录 |
| internal identifier | request id、tenant surrogate id | 最小化、受控保留 |
| personal/confidential | email、address、business data | 默认不记 value |
| credential/cryptographic | password、token、private key | 永不记录 |
| regulated/highly sensitive | payment/health/government id | 专门政策与审计 |
数据库团队不能只靠列名猜分类。应用 schema、数据目录和日志 policy 要共享 同一份分类元数据。
SQL fingerprint 与 value 分离
性能分析通常不需要参数值。优先保留:
而不是:
第 25、26 章会用 pg_stat_statements、query id 和计划证据分析性能;这些
方法能显著减少为了可观测而复制业务值的必要。
pipeline 脱敏是第二道防线
collector 侧可以:
- 删除已知敏感字段;
- token/password pattern 检测;
- 限制异常样本和 payload;
- 对 identifier 做受控 pseudonymization;
- 阻止包含 private key/verifier 的事件;
- 记录 redaction rule version。
但 regex 不能成为唯一控制。SQL 语法、编码、嵌套 JSON、base64 和未知字段 会绕过它。首要措施仍是在源端不产出秘密。
脱敏测试
上线前使用纯 synthetic canary:
执行经过批准的测试请求,然后检查:
期望是 credential marker 零命中,其他 marker 只出现在预先批准的位置。不要 用真实 secret 做日志泄漏测试。
审计与隐私的验收
最终不是“多记”或“少记”,而是:
这是安全日志与普通调试日志的根本区别。
上一节:行级安全与连接池上下文 · 返回本章目录 · 下一节:Pigsty 安全基线 · 查看全书目录 · 查看索引中心
23.6 Pigsty 安全基线
Pigsty 能把角色、HBA、服务入口、证书和日志参数声明化,但“声明化”不等于 “使用默认值即可满足任何生产 threat model”。官方安全说明明确指出,默认 配置面向可信内网中的开发、测试和演示;生产环境要按自身威胁模型配置凭据、 网络边界、认证、证书、备份和审计。
本节建立四层对账:
只有最下面一层能证明客户端实际经历了什么;只有最上面一层能解释为什么这样 设计。
23.6.1 角色、HBA、证书与服务入口声明
角色声明
Pigsty 用:
用户按数组顺序创建,因此被引用的 group role 应先定义。下面是结构示例, 不是可直接投产的 secret 文件:
示例标记必须由私密交付机制替换,不能把真实明文或 verifier 提交到本书、Git、 工单或聊天记录。Pigsty 支持 plaintext 或 SCRAM verifier,但官方也把 plaintext inventory 标为不推荐。角色字段与 membership object 格式见 Pigsty:User/Role。
这里还要注意:
roles管理是 additive;未声明的历史 membership 不会自动消失;- 要撤销旧 membership,应显式使用
state: absent; - PostgreSQL 16+ 才支持 membership 的
set/inherit细分; pgbouncer: true会把 login 纳入 PgBouncer 认证面;NOLOGINowner/runtime/readonly 不应加入池用户列表;connlimit与 PgBouncer pool limit 是不同层的限制。
已存在集群修改角色:
它是声明式、可重复入口。删除角色的 state: absent 会涉及断开连接、转移
ownership 和 DROP ROLE,属于破坏性变更;必须先 dry-run/依赖审查和审批,
不能把“脚本支持安全删除”理解为无需变更控制。官方管理流程见
Pigsty:用户管理。
membership 与对象 ACL 分开
Pigsty pg_users 可声明 cluster role 和 membership;schema/table/function
ACL、owner、default privilege 与 RLS policy 仍应由经过版本控制的 migration
完成:
不要在多个无序启动脚本里同时管理同一份 grant。平台和应用必须明确各自 owner 和收敛时点。
PostgreSQL 与 PgBouncer HBA
Pigsty 有四组参数:
| 参数 | 范围 | 作用 |
|---|---|---|
pg_default_hba_rules |
global | PostgreSQL 环境默认规则 |
pg_hba_rules |
global/cluster/instance | PostgreSQL 增量规则 |
pgb_default_hba_rules |
global | PgBouncer 环境默认规则 |
pgb_hba_rules |
global/cluster/instance | PgBouncer 增量规则 |
规则会按 order 排序,数值越小越靠前。未显式指定通常进入 1000+ 区域;
如果前面已有宽泛默认 allow,它可能永远匹配不到。
生产应用入口的概念示例:
auth: ssl 的当前 Pigsty alias 会渲染为 hostssl ... scram-sha-256;生产变更
仍应检查目标版本的渲染结果,不能永久依赖书中的 alias 解释。HBA 字段、
alias、role filter 和 order 规则见
Pigsty:HBA Rules。
若应用只允许经 pool/service 入口访问,可进一步在网络与 PostgreSQL HBA
中拒绝应用子网直连 5432,只允许本机 PgBouncer 或指定代理身份访问后端。
这能避免客户端绕过 PgBouncer 的连接限制、认证面与事务池合同。
intra 只是地址别名
Pigsty 的 intra/intranet 通常展开为 RFC 1918:
这些地址不是天然可信。企业办公网、VPN、容器网、其他租户 VPC 和开发环境都 可能落在其中。生产规则应尽量使用准确的应用 subnet/security group,而不是 把整个 RFC 1918 视为一个 trust zone。
刷新而非手工改文件
修改 inventory 后:
会重新渲染并 reload PostgreSQL/PgBouncer 相关 HBA。不要直接编辑:
下次 playbook 会覆盖手工改动,而且 inventory 与运行事实从此分叉。紧急手工 变更若无法避免,也必须同步回声明源、记录例外并尽快恢复收敛。
证书声明和使用
Pigsty 默认基础设施 CA 会为 PostgreSQL、Patroni、etcd、MinIO、Nginx 等 内部服务签发证书。生产评审要区分:
这六件事不能合并成“开启 SSL”。
证书 SAN 应覆盖客户端实际使用的:
- HAProxy/VIP DNS name;
- PostgreSQL direct service name;
- 节点名/IP(若允许直连);
- planned disaster-recovery endpoint。
不要让应用用 hostaddr 绕过期望的 DNS name verification,除非同时提供可
校验的 host 语义并完成测试。
CA private key 所在目录是根信任资产。需要离线/受限备份、读取审计和恢复演练。 证书签发与客户端安装流程见 Pigsty:CA and Certificates。
两段 TLS 分别声明
若应用经 PgBouncer:
Pigsty 的 PostgreSQL server TLS 默认开启,不表示 HBA 默认要求 TLS; PgBouncer client TLS 默认也不是开启状态。当前官方安全说明明确列出:
生产要逐入口决定:
| 链路 | 加密 | 对端验证 | 允许来源 | 认证主体 |
|---|---|---|---|---|
| app → HAProxy/PgBouncer | required | service name | app subnet | app login |
| PgBouncer → PostgreSQL | TLS 或受控 local socket | node/service | proxy nodes | server login |
| admin → PostgreSQL | required | admin endpoint | bastion/infra | named admin |
| replica → primary | required or isolated equivalent | node identity | cluster only | replication role |
service entrypoint
第 22 章区分了 direct PostgreSQL、PgBouncer、HAProxy primary/replica/offline 等入口。安全基线要为每个入口建立:
未使用入口应关闭或从网络上不可达。保留一个“以后可能调试”的公网 5432
会变成长期旁路。
23.6.2 管理面、监控面和数据库面的网络边界
先画流量矩阵
至少区分:
| 平面 | 典型组件 | 主要主体 | 失陷后风险 |
|---|---|---|---|
| 管理面 | meta/Ansible、SSH、sudo、secret store | 平台管理员/自动化 | 改写全部配置与密钥 |
| 控制面 | Patroni REST、etcd | cluster agent | 错误选主、拓扑控制 |
| 数据面 | HAProxy、PgBouncer、PostgreSQL | 应用/分析/迁移 | 数据读写与租户越权 |
| 监控面 | exporter、Prometheus、Grafana、日志 | monitoring identities | 查询/拓扑/日志泄露 |
| 备份面 | pgBackRest repo、WAL/archive | backup identities | 全量数据泄露或恢复破坏 |
“都是内网服务”不是边界设计。每条 flow 应记录:
管理面
管理节点通常能:
- 通过 SSH 到所有节点;
- sudo/root;
- 读取 inventory 或 vault integration;
- 运行 Ansible/playbook;
- 访问 CA material;
- 变更防火墙、HBA 和 service。
因此它不是普通“运维跳板”。应:
把数据库 superuser 密码从 inventory 移走,却允许所有工程师无审计地
sudo -iu postgres,并没有实现职责分离。
控制面
Patroni REST 和 etcd 决定 cluster topology。它们不应暴露给业务 subnet。 控制面规则通常只允许:
health check 入口与管理 API 要区分。HAProxy 为角色判断访问 Patroni health endpoint,不表示应用客户端也应访问完整 Patroni API。
控制面 TLS、认证和 ACL 必须按目标 Pigsty 版本验证。不能因为 etcd 使用 TLS, 就推断 Patroni REST 也已经使用 TLS。
数据面
建议把应用路径收敛为:
然后按实际需要开放 direct path:
网络控制至少三层:
HBA 不是防 DDoS 的边界:连接已经到达 PostgreSQL 才会检查。安全组/防火墙也 不理解 database/user。二者互补。
Pigsty node_firewall_mode=zone 的默认可信 intranet 必须与真实边界对账。
官方建议也明确提醒 RFC 1918 范围可能过宽,生产 demo 配置通常应移除无必要
的 5432 暴露。参见
Pigsty:Security Recommendations。
监控面
monitor role 看起来只读,但往往能访问:
pg_stat_activity查询文本;- replication/topology;
- database/object names;
- slow-query samples;
- connection source/user;
- logs and dashboards。
因此:
- monitor 不应使用 superuser;
- exporter query 必须受版本控制;
- dashboard 与 Prometheus API 要认证;
- label 不能含 password/token/高基数 PII;
- 跨环境 metrics/logs 要隔离;
- support snapshot 要脱敏;
- 监控不可用不能使数据库认证 fail-open。
备份面
backup repository 持有跨 RLS、跨租户的完整数据与 WAL。网络上只允许备份 主体和恢复路径;读取、删除、retention 改动应分权。数据库 RLS/HBA 无法保护 一份已被复制到对象存储的备份。
恢复环境同样要隔离:把生产备份恢复到宽松开发网,常比攻击生产数据库更容易 造成泄露。第 21、32、33 章会继续处理备份与恢复的控制。
egress 也要控制
数据库节点出站能力可能被:
- extension/FDW;
COPY PROGRAM高权能力;- untrusted language;
- backup/archive command;
- 运维脚本;
- 被攻陷进程
用于外带数据。仅做 inbound firewall 不够。生产需要明确允许的软件源、 backup endpoint、DNS/NTP/monitoring 等 egress,并监控异常。
23.6.3 从配置渲染到运行事实的差异检查
四份状态
对同一项控制记录:
示例:
| 控制 | desired | rendered | runtime | observed |
|---|---|---|---|---|
| app TLS | auth: ssl |
hostssl |
pg_hba_file_rules |
disable fails, verify-full succeeds |
| role | NOINHERIT |
generated SQL | pg_roles |
raw login SELECT fails |
| membership | SET true |
grant statement | pg_auth_members |
SET LOCAL succeeds |
| RLS | migration | table/policy DDL | pg_class/pg_policies |
cross-tenant writes fail |
| pool identity | pgbouncer: true |
userlist presence | SHOW USERS projection |
pool auth succeeds |
任何一列缺失都不能宣告闭环。
inventory 评审
先检查:
- 是否把 secret 直接写入 Git;
- role 高权 flag;
inherit和 membershipadmin/set/inherit;pgbouncer暴露范围;- HBA order 与宽规则;
host/hostssl;world/intraalias 展开;- primary/replica/offline role filter;
- listener/service port;
- PostgreSQL/PgBouncer/Patroni TLS;
- logging/audit;
- firewall intranet。
inventory diff 应显示结构,不应把 password/verifier 展开到 PR。
渲染检查
变更前先生成/审查候选,应用后检查节点:
不要把 /etc/pgbouncer/userlist.txt 原文放进证据;它可能含 verifier。
HBA 应验证:
并确认 error IS NULL、顺序与 inventory 相符。
runtime 检查
角色:
membership:
对象与 RLS:
TLS:
日志:
observed 检查
必须从与真实 workload 相同的网络和 driver 路径执行:
只在 database node 上用 Unix socket psql,无法验收远程网络、proxy HBA、
TLS name 或客户端 CA distribution。
本章沙箱差异
在 Pigsty v4.5.0 nonproduction sandbox 中,正式捕获发现:
| 控制 | 期望/能力 | 运行事实 | 判定 |
|---|---|---|---|
| PostgreSQL TLS | server 可加密 | on,最低 TLS 1.2 | 能力通过 |
| cert identity | SAN + verify-full | 正例成功,错名失败 | 通过 |
| direct channel binding | SCRAM over TLS | 成功 | 通过 |
| business direct HBA | 生产应强制 TLS | +dbrole_readonly 内网为 host |
待整改 |
| PgBouncer client TLS | 生产应按 threat model 启用 | disabled | 待整改 |
| pool → PostgreSQL | 受控链路 | local Unix socket | 事实符合设计 |
| CRL | 撤销路径 | file/dir unset | 待整改 |
| pgAudit | 目标要求对象审计 | absent/not preloaded | 待整改 |
| bind logging | secret-minimized | 非错误参数上限 -1 |
待整改 |
它诚实地同时证明:
第一条不能抵消第二条。
分阶段整改
安全变更也会造成可用性风险,应分阶段:
一次性把所有 host 改成 hostssl,如果客户端尚未安装 CA 或仍用 IP 不匹配
证书,会把安全整改变成全站故障。
例外不是口头备注
暂时保留非 TLS/旧客户端时,例外记录至少包含:
无到期日的例外就是新默认。
生产判定
最终报告只允许:
| 判定 | 含义 |
|---|---|
pass |
所有必须项有运行与负例证据 |
pass-with-exception |
明确、批准、限时、受补偿控制的差距 |
pending |
机制可用,但必需控制尚未实施/证明 |
reject |
存在不可接受暴露或证据冲突 |
本章沙箱是 pending。它完全适合教学实验,却不能被包装成生产审批。这种区分
正是安全工程成熟度的一部分。Pigsty 当前生产注意事项见
Pigsty:Security Considerations。
上一节:密钥、审计与敏感信息 · 返回本章目录 · 下一节:实战:隔离两个租户 · 查看全书目录 · 查看索引中心
23.7 实战:隔离两个租户
本节把前六节压成一条可重放的安全证明:
实验面向第 19 章保留的 Pigsty nonproduction sandbox。它会保留 synthetic schema/roles,临时改变一个 PgBouncer runtime pool,并短暂启用一个轮换探针 login。不能删除 guard 后指向生产。
实验合同:
23.7.1 建立应用角色、迁移角色和只读角色
环境与范围
正式环境:
实验明确不做:
风险分级
| action | 风险 | 行为 |
|---|---|---|
capture |
L0 | 只读当前快照 |
verify / review / all |
L0 | 只重验既有 evidence |
drill:security |
L2 | 建 fixture、临时 pool/rotation mutation |
reset:fixture |
L3 | 删除本章 schema 和五个 synthetic role |
all 的含义不是“执行所有实验”,而是:
这种命名刻意防止 CI 或读者误触有状态演练。
两份私密输入
完整 drill 需要:
普通情况下两者可以来自同一份经过评审的 private inventory。书中正式运行把 它们分开,因为本地当前声明已与第 19 章冻结样本发生演进;不能拿新声明冒充 旧部署的 baseline。
runner 只读取既有 test login 的 credential。它不:
- 输出 credential;
- hash credential 到报告;
- 修改
test的 password 或 role attributes; - 将 private inventory 复制进仓库。
exact mutation guards
使用一个全新的空 evidence directory:
target、authority、confirm 任一不完全相同,inventory 缺失/权限不安全,或 output 非空,runner 都拒绝。执行前还会跑完整第 19 章 preflight;结束后再跑 postflight。
角色图
fixture 使用已有 login 与五个 synthetic role:
五个 synthetic role 都要求:
setup 在复用同名 role/schema 前,检查 owner、comment、属性与完整 fixture shape。若发现同名非实验对象,不会接管。
schema 与 synthetic data
对象:
固定 fixture:
脚本验证 exact columns、constraints、row ids、owner 与 comment;同 schema 中
出现第五条或未知 id 就拒绝。所有数据都是 synthetic,evidence 不导出
secret_note。
完整 SQL 见 setup.sql。
权限
先撤销 PUBLIC、raw test login 和 Pigsty 宽 group role 对实验 schema/
table/function 的权限,再授予:
明确不授予:
owner 拥有对象但不可登录,migrate 能显式切为 owner 而不继承/转授 owner。
policy
helper:
五条 policy:
其中 UPDATE 和 owner 明写 USING 与 WITH CHECK,最后:
default privileges 同时约束未来由 owner 创建的 table/function,防止下一次
migration 恢复 PUBLIC 暴露。
23.7.2 通过 PgBouncer 事务池验证 RLS
应用事务
runner 模拟应用:
role 来自固定 allowlist,tenant id 使用参数绑定,并被标记为 synthetic server-authorized mapping。实验不把终端用户输入直接信任为 context。
正例
通过 primary pooled entry:
观测:
| effective role/context | 结果 |
|---|---|
| runtime + tenant A | 2 条且全是 A |
| runtime + tenant B | 2 条且全是 B |
| runtime + missing | 0 条,context NULL |
| owner + missing, FORCE RLS | 0 条 |
| owner + tenant A | 2 条且全是 A |
migrate SET ROLE owner |
session/current role 链正确 |
| superuser break-glass | 4 条 |
最后一项单独从节点本地受控管理路径观察,不是应用能力。
负例
实际 SQLSTATE:
cross-tenant UPDATE 使用 A context,尝试把 A 行的 tenant_id 改为 B:
它专门证明 WITH CHECK,不是只证明不可见行被 USING 过滤。
为什么缺失 context 返回 0 而不是报错
helper 的 missing value 是 NULL:
对读路径,这能 fail-closed。malformed value 则因 UUID cast 报错。应用层仍应 把 missing context 视为 bug 并告警,不能因为数据库返回空集合就安静吞掉。
同时确认 transport 事实
RLS 测试经过 pool,但沙箱 PgBouncer client TLS 是 disabled。runner 没有把 这条路径包装成生产安全链,而是分别测试:
直连协商为 TLS 1.3 / TLS_AES_256_GCM_SHA384 / 256 bits。证书 public metadata
包含节点对应 DNS/IP SAN,private key 只检查 mode 0600,从不读取内容。
23.7.3 注入会话状态泄漏与越权访问并修复
临时单 backend pool
为让复用确定发生,runner 先确认 test pool 无活动 client,再保存:
临时设置:
然后在三节点对 test pool 执行 RECONNECT。无论后续成功或失败,finally
都会恢复四项 exact baseline,再次 reconnect 并比较 SHOW CONFIG。
泄漏注入
client A 在事务外执行 session setting:
然后 A 和一个新 client B 都只在事务内设置 runtime role,不再设置 tenant。
结果:
B 从未声明 tenant A,却继承 A 的 session state。这条反例是 validator 的必须 项;如果没有复现,实验不会用“可能没复用”蒙混通过。
修复验证
清理 server connection 后,四个逻辑 client 依次执行完整事务合同:
全部复用 PID 72578:
因此修复证据同时包含:
若只验证两个 backend PID 不同,不能证明 transaction-local 合同。
密码轮换注入
pg36_ch23_rotate 只用于 direct PostgreSQL 认证,不进入 PgBouncer 声明面。
runner 用随机、只存在内存/私密进程输入中的 v1/v2:
这同时证明:
- PostgreSQL 与 PgBouncer authentication surface 不相同;
- password rotation 控制新认证;
NOLOGIN不终止既有 session;- 演练结束没有留下可登录的 rotation role 或 verifier。
失败时的恢复边界
脚本可以安全自动恢复的只有已知、可比较状态:
它不自动:
若拓扑不再唯一稳定或 pool restore compare 失败,报告失败并保留证据,交由 operator 检查;不猜测性修复。
23.7.4 输出权限矩阵、轮换证据与应急回收步骤
evidence tree
完整私密证据:
所有 drill evidence file 要求 mode 0600。完整 bundle 含 internal addresses、
role graph、public certificate metadata 和 backend PID,应放 private evidence
store,不提交 Git。仓库只保留 secret-free 聚合
security-run.json。
manifest
manifest 关联:
validator 会重新计算 evidence hash,防止在运行后悄悄替换观测文件。source hash 让报告对应到具体 runner/contract 版本。
权限矩阵
正式投影:
| capability | raw login | runtime | readonly | owner | superuser |
|---|---|---|---|---|---|
| table SELECT ACL | 否 | 是 | 是 | implicit | 是 |
| INSERT | 否 | 是 | 否 | implicit | 是 |
| UPDATE | 否 | 是 | 否 | implicit | 是 |
| DELETE | 否 | 否 | 否 | implicit | 是 |
| TRUNCATE | 否 | 否 | 否 | implicit | 是 |
| schema CREATE | 否 | 否 | 否 | 是 | 是 |
| policy management | 否 | 否 | 否 | 是 | 是 |
| rows without context | 无 ACL | 0 | 0 | 0 under FORCE | 4 |
“owner implicit”不表示生产 owner 应日常执行 DML;它不可登录,仅由 migration 在审批窗口显式切换。
read-only 重验
拿到一个既有完整 bundle 后:
应给出:
对抗性反例
negative-cases.json 要求 validator 拒绝
20 类篡改,包括:
测试 validator 对坏证据的拒绝能力,才能避免“只要 JSON 字段存在就 pass”的 自证循环。
应急回收
生产 credential suspected compromised:
应急时不能只运行:
本章已经证明既有 session 仍能查询。
destructive reset
正常 drill 不调用 reset。只有明确要删除 synthetic fixture 时:
reset 会先检查:
然后只删除:
它保留已有 test login 和全部非 fixture 对象。该命令是破坏性操作;本书正式
验收没有执行它,fixture 留作复查。
生产门槛
实验正式通过:
但生产仍为 pending:
这些差距与实验通过不矛盾。前者说明机制被正确验证,后者说明当前 sandbox 还不是生产安全批准。
本章完成定义
读者应能独立解释并证明:
如果只能创建一条 policy,却无法证明身份来源、复用连接、负例、轮换和日志 边界,本章还没有完成。
参考资料
- PostgreSQL 18:Client Authentication
- PostgreSQL 18:Database Roles
- PostgreSQL 18:Row Security Policies
- PostgreSQL 18:SSL Support
- PgBouncer:Features
- Pigsty:Security Considerations
上一节:Pigsty 安全基线 · 返回本章目录 · 下一章:纲举目张:SLO、SOP 与组织治理 · 查看全书目录 · 查看索引中心