PostgreSQL的权限体系设计得非常精密,但很多团队在落地时仍然停留在“能连上就行”的阶段。默认的public角色权限、过于宽松的schema授权、以及被忽略的行级安全策略,往往是生产环境数据泄露的根源。真正要做的不是简单收回几个权限,而是建立一套从角色层级到列级控制再到审计追踪的完整防线。
角色与继承关系的重新梳理PostgreSQL中的角色同时承担着“用户”和“用户组”的双重身份,这种设计虽然灵活,但也容易造成权限蔓延。首先要打破一个习惯:不要直接给每个业务账号赋予大量对象权限。正确的做法是创建纯粹的组角色,也就是不带login属性的角色,将对象权限集中授予组角色,然后通过grant将组角色授予具体的登录用户。这样做的好处是当业务人员变动时,你只需要回收组角色成员关系,而不必在几十张表上逐一操作。
角色继承默认是开启的,这意味着一个登录用户会自动继承它所属组角色的所有权限。但在某些敏感场景下,你可能希望用户必须显式切换角色才能使用某些权限,这时可以创建带noinherit属性的角色。比如一个数据分析师平时只能读取脱敏数据,只有在执行特定任务时通过set role切换到高权限角色,这样即使他的账号凭证泄露,攻击者也拿不到完整数据。检查现有角色的继承关系可以通过查询pg_roles视图,重点关注rolinherit字段。
schema级别的权限隔离PostgreSQL默认新建的schema中,public角色拥有create和usage权限,这是一个经常被忽视的安全隐患。任何能连接数据库的用户都可以在这个schema里创建对象,甚至可能通过创建函数来提权。对于生产环境,建议立即在template数据库和所有业务数据库中执行revoke create on schema public from public。如果应用不需要public schema,直接将其删除或重命名更为彻底。
更精细的做法是为不同业务模块创建独立的schema,每个schema只对特定的组角色授权usage。比如财务模块的schema只授权给finance_group,用户模块的schema只授权给user_group,这样即使应用代码出现SQL注入漏洞,攻击者也只能在单个schema的范围内活动,无法横向跨模块读取数据。schema的权限控制还能与search_path配合使用,通过为用户设置合理的search_path顺序,既能简化开发中的表名引用,又能避免恶意用户通过修改search_path来劫持表名。
表级与列级权限的实战配置表级权限是大多数人都会配置的基础层,但常见的错误是直接使用grant all on all tables in schema来批量授权,这把delete和truncate权限也一并放了出去。对于OLTP系统的应用账号,通常只需要select、insert、update和references四种权限。delete操作应该通过软删除或存储过程来执行,truncate则应该完全禁止应用账号持有。
列级权限才是真正体现精细化程度的地方。用户表中password_hash、手机号、身份证号这些敏感列,即使在同一张表中,也不应该对所有查询者可见。通过grant select (column_list) on table to role的语法,可以精确控制到列。但这里有一个容易踩的坑:列级权限和表级权限是独立计算的,如果用户同时拥有表级的select权限,那么列级限制会被绕过。正确的做法是只授予列级权限,不授予表级权限。验证时可以查询information_schema.column_privileges视图,逐列确认授权范围。
行级安全策略的深度应用行级安全是PostgreSQL权限体系中最强大的特性,但默认是关闭的。通过alter table enable row level security开启后,表的所有者和管理员仍然能看到全部数据,但普通用户只能看到策略允许的行。行级安全的核心在于using表达式和with check表达式的编写,前者控制哪些行可读,后者控制哪些行可写。
一个典型的多租户SaaS场景中,所有租户的数据都存在同一张orders表里,通过tenant_id字段区分。创建策略create policy tenant_isolation on orders for all using (tenant_id = current_setting('app.current_tenant_id')::integer)后,应用只需在连接建立时执行set app.current_tenant_id = '123',后续所有查询都会自动过滤,完全不需要在业务SQL中写where tenant_id = ?。这种隔离比应用层控制更可靠,因为即使有人绕过应用直接连接数据库,也无法跨租户读取数据。
更复杂的场景是共享数据与私有数据混合。比如有一个documents表,普通用户只能看自己的文档,但部门经理可以看到整个部门的文档。可以创建多条策略,PostgreSQL会按permissive规则合并执行,只要满足任意一条策略就能看到对应行。但要注意策略之间的性能影响,每条查询都会触发策略表达式的计算,复杂子查询或函数调用可能导致执行计划变差。建议对策略中用到的列建立索引,并定期通过explain analyze检查策略是否导致了全表扫描。
函数与安全定义者的陷阱存储过程和函数中的权限切换是另一个容易被利用的攻击面。创建函数时如果指定了security definer,函数执行时会使用函数所有者的权限而不是调用者的权限。这个特性常被用来实现提权操作,比如让普通用户通过函数访问他原本无权访问的表。但问题在于,如果函数内部拼接了动态SQL并且没有做好输入校验,攻击者可以通过SQL注入以函数所有者的身份执行任意命令。
对于security definer函数,必须严格遵循最小权限原则:函数所有者只应拥有完成该函数功能所必需的最小权限,不要用超级用户或数据库所有者来创建这类函数。同时要在函数内部通过set子句临时调整search_path,防止攻击者通过修改search_path来劫持函数内部调用的其他函数或操作符。一个安全的写法是在函数定义中加上set search_path = pg_catalog, pg_temp,确保只会使用系统内置对象。
默认权限与未来对象的自动化管理很多DBA在手动调整完现有对象的权限后就认为万事大吉,却忽略了后续新建的表、视图会恢复到默认权限。PostgreSQL提供了alter default privileges命令来预设未来创建对象的权限。这个命令需要针对特定的schema和创建者来设置,比如alter default privileges for user creator_user in schema app_schema grant select on tables to read_role。这意味着只要creator_user在app_schema中新建表,read_role就会自动获得select权限。
但默认权限有一个局限:它只对设置之后创建的对象生效,而且不同创建者需要分别设置。如果团队中多人有建表权限,必须为每个人都执行一遍alter default privileges。更稳妥的做法是统一由自动化迁移工具或CI/CD流程来建表,这样只需要为这一个创建者设置默认权限即可。同时建议定期通过查询pg_default_acl表来审计当前的默认权限配置,确保没有意外的授权存在。
权限审计与异常检测权限配置完成后,持续的审计才能保证安全水位不下降。PostgreSQL的日志系统可以通过设置log_statement = 'ddl'来记录所有权限变更操作,但这还不够。应该建立周期性的权限快照审计,将当前所有对象的acl信息导出并与上一周期的快照进行diff对比。关键视图包括pg_class中的relacl字段、pg_namespace中的nspacl字段、以及pg_proc中的proacl字段。任何非预期的grant或revoke操作都应该触发告警。
对于敏感表的查询行为,可以借助pg_stat_statements扩展来收集SQL执行统计,结合log_statement = 'mod'记录所有写操作。如果发现某个低权限账号突然开始大量查询敏感表,或者出现了异常的权限升级尝试,需要立即排查。还可以创建事件触发器,在ddl_command_start事件中捕获grant和revoke操作,将操作详情写入审计表,实现权限变更的实时追踪。
连接层与网络层的协同防御权限控制不能只依赖数据库内部机制,pg_hba.conf文件是数据库的第一道大门。这里最常见的错误配置是允许所有IP通过md5密码认证访问,这等于把安全完全寄托在密码强度上。生产环境应该严格限制允许连接的IP段,使用scram-sha-256替代md5,并且对敏感数据库强制使用ssl连接。可以为不同业务线配置不同的认证方式,比如内部服务间通信使用cert证书认证,分析师使用ldap集成企业账号体系。
连接池和中间件层面的权限控制同样重要。很多团队在连接池中使用统一的数据库账号,所有微服务共享同一个连接池,这完全抹平了数据库层面的角色隔离。正确的做法是为每个微服务分配独立的数据库账号,在连接池配置中使用不同的凭证,让数据库能够区分不同服务的请求来源。这样当某个服务出现异常查询时,可以通过pg_stat_activity视图中的application_name和usename字段快速定位到具体服务。
备份与恢复中的权限考量pg_dump导出数据时默认会包含权限语句,这在跨环境迁移时需要特别留意。开发环境和生产环境的权限配置通常不同,如果直接导入生产环境的dump文件到开发库,可能会把生产环境的高权限账号覆盖到开发环境。建议在pg_dump时使用--no-acl参数跳过权限导出,或者使用--no-owner参数跳过所有者设置,然后在导入后根据目标环境重新配置权限。
对于逻辑复制和订阅场景,发布端和订阅端的权限是独立的。发布端的表级权限不会自动同步到订阅端,需要在订阅端单独配置。如果使用逻辑复制来实现读写分离,要确保只读节点的账号确实只有select权限,防止应用错误地将写请求发送到只读节点。可以通过在只读节点设置default_transaction_read_only = on作为兜底,这样即使权限配置有遗漏,写操作也会被拒绝。
