PostgreSQL的权限体系并不复杂,但真正能把权限管控做到位、同时兼顾安全优化的部署并不多。多数问题集中在默认权限过宽、Public角色权限未收敛、行级安全策略缺失、连接与密码策略松散,以及审计日志不完整。下面直接拆解这些环节的具体问题和可落地的解决方法。
先处理最容易被忽略的Public角色权限
PostgreSQL安装完成后,默认会存在一个名为PUBLIC的特殊角色。它不是一个真正的用户,而是一个隐式包含所有角色的组。很多数据库在初始化后,PUBLIC对public模式拥有CREATE和USAGE权限,对数据库拥有CONNECT权限,甚至对部分系统函数拥有EXECUTE权限。这意味着任何能登录到数据库的角色,哪怕没有显式授权,也可能在public模式下创建对象,或者调用敏感函数。
第一步是回收PUBLIC在public模式上的CREATE权限。在PostgreSQL 15及以上版本,这一默认行为已经调整,但在大量存量实例中仍然存在。执行以下语句可以完成回收:
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
同时建议回收PUBLIC对数据库的默认CONNECT权限。新创建的数据库默认允许PUBLIC连接,如果业务上只有特定应用账号需要连接,应该显式回收:
REVOKE CONNECT ON DATABASE yourdb FROM PUBLIC; GRANT CONNECT ON DATABASE yourdb TO app_user;
对于敏感的系统函数,尤其是那些可以读取文件、执行系统命令或者查看连接信息的函数,需要逐一检查PUBLIC的执行权限。例如pg_read_file、pg_ls_dir、pg_read_binary_file等函数,在较老版本中PUBLIC默认拥有EXECUTE权限。可以通过以下查询找出PUBLIC仍然持有EXECUTE权限的函数:
SELECT routine_name FROM information_schema.routines WHERE routine_type='FUNCTION' AND grantee='PUBLIC';
对于非业务必需的函数,直接回收权限:
REVOKE EXECUTE ON FUNCTION pg_read_file(text) FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_dir(text) FROM PUBLIC;
角色与权限的分层设计
权限管控的核心不是给每个用户单独授权,而是建立角色分层。通常至少需要三层:登录角色、业务角色、只读角色。登录角色只负责连接,不直接拥有对象权限;业务角色拥有对业务表的读写权限;只读角色仅拥有SELECT权限。登录角色通过继承业务角色或只读角色来获得实际权限。
CREATE ROLE app_readonly NOLOGIN; CREATE ROLE app_readwrite NOLOGIN; CREATE ROLE app_user LOGIN PASSWORD 'strong_password' INHERIT; GRANT app_readwrite TO app_user;
这种设计的优势在于,当需要调整权限时,只需要修改业务角色,所有继承该角色的登录账号自动生效。同时避免每个登录账号直接绑定大量对象权限,降低权限混乱的风险。
授权时遵循最小权限原则。对于业务表,只读角色只授予SELECT,读写角色授予SELECT、INSERT、UPDATE、DELETE。不要为了方便直接授予ALL PRIVILEGES。对于序列,如果业务表使用自增主键,读写角色需要序列的USAGE和SELECT权限,否则插入数据时会报权限错误。
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_readwrite;
对于函数,只授予业务角色必要的EXECUTE权限,避免将敏感函数暴露给普通业务账号。
Schema级别的隔离与默认权限管理
很多团队习惯将所有表都放在public模式下,这在多租户或多业务模块共用一个数据库时会造成权限边界模糊。更好的做法是按业务模块划分Schema,每个Schema单独授权。例如财务模块使用finance Schema,用户模块使用user Schema。这样可以在Schema级别直接控制访问范围。
CREATE SCHEMA finance AUTHORIZATION finance_owner; GRANT USAGE ON SCHEMA finance TO finance_readwrite; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA finance TO finance_readwrite;
默认权限管理同样关键。PostgreSQL的ALTER DEFAULT PRIVILEGES可以控制某个角色在未来创建的对象上自动授予哪些权限。如果不设置默认权限,新创建的表不会自动授权给业务角色,导致应用在新建表后出现权限缺失。
ALTER DEFAULT PRIVILEGES FOR ROLE finance_owner IN SCHEMA finance GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO finance_readwrite; ALTER DEFAULT PRIVILEGES FOR ROLE finance_owner IN SCHEMA finance GRANT USAGE, SELECT ON SEQUENCES TO finance_readwrite;
这样finance_owner角色在finance Schema下新建的任何表和序列,都会自动将对应权限授予finance_readwrite角色,避免手动补授权。
行级安全策略的实战应用
列级权限和表级权限只能控制到“能不能访问某张表或某个字段”,但无法控制“能访问表中的哪些行”。行级安全策略(Row Level Security,RLS)可以解决这个问题。启用RLS后,表的所有者仍然可以访问全部数据,但普通用户只能看到策略允许的行。
启用RLS的前提是表所有者需要执行ALTER TABLE ... ENABLE ROW LEVEL SECURITY。之后如果没有定义任何策略,默认拒绝所有普通用户的访问。策略通过CREATE POLICY定义,可以指定针对SELECT、INSERT、UPDATE、DELETE等操作的过滤条件。
ALTER TABLE orders ENABLE ROW LEVEL SECURITY; CREATE POLICY user_orders_policy ON orders FOR SELECT USING (user_id = current_user);
上述策略让每个用户只能看到orders表中user_id等于当前数据库用户名的行。对于UPDATE和DELETE操作,同样需要定义策略,否则即使有表级权限也无法操作任何行。
CREATE POLICY user_orders_update_policy ON orders FOR UPDATE USING (user_id = current_user) WITH CHECK (user_id = current_user);
RLS的另一个常见场景是多租户SaaS系统。通过设置一个租户ID上下文变量,例如使用自定义GUC参数,可以在策略中动态过滤数据。
ALTER DATABASE yourdb SET app.current_tenant_id TO 'default';
CREATE POLICY tenant_orders_policy ON orders
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant_id')::integer);
应用在建立连接后通过SET命令设置当前租户ID,RLS会自动过滤数据。这种方式比在应用层拼接WHERE条件更安全,因为即使应用代码出现遗漏,数据库层也会强制过滤。
连接安全与密码策略
权限管控之外,连接层面的安全同样重要。pg_hba.conf是控制客户端认证的核心配置文件。建议生产环境全部使用scram-sha-256认证方式,禁止使用trust和password明文认证。对于来自内网的应用服务器,可以使用hostssl条目强制SSL连接。
hostssl all all 192.168.1.0/24 scram-sha-256 host all all 0.0.0.0/0 reject
密码复杂度可以通过passwordcheck模块实现。该模块在编译PostgreSQL时启用,或者在安装contrib包后通过shared_preload_libraries加载。它能强制密码满足最小长度、包含大小写字母和数字等规则。
shared_preload_libraries = 'passwordcheck'
对于高权限账号,尤其是超级用户,建议限制其远程登录。可以在pg_hba.conf中将postgres用户的连接来源限制为本地Unix Socket或特定管理IP。
local all postgres peer host all postgres 10.0.0.5/32 scram-sha-256
审计与监控
没有审计的权限管控是不完整的。PostgreSQL原生日志可以记录连接、断开、错误和DDL语句。通过调整log_statement参数,可以记录所有DDL或者所有语句。对于安全要求较高的环境,建议至少记录DDL和角色权限变更。
log_connections = on log_disconnections = on log_statement = 'ddl' log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,remote=%r '
对于DML语句的审计,可以使用pgaudit扩展。它提供更细粒度的审计能力,可以按对象、按角色、按操作类型记录日志。启用pgaudit后,可以配置审计规则,例如只审计特定表上的INSERT、UPDATE、DELETE操作。
shared_preload_libraries = 'pgaudit' pgaudit.log = 'write, ddl' pgaudit.log_relation = on
定期检查权限分配情况也很重要。可以编写查询脚本,定期导出每个角色拥有的表级权限、Schema权限和函数权限,与预期基线进行对比。发现异常授权时及时回收。
SELECT grantee, table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee NOT IN ('postgres', 'pg_database_owner')
ORDER BY grantee, table_schema, table_name;
安全优化中的几个实用细节
除了权限管控,还有一些容易被忽视的安全优化点。第一,及时更新小版本。PostgreSQL的小版本更新通常包含安全修复,长期不更新会让已知漏洞暴露。第二,限制超级用户数量。超级用户拥有绕过一切权限检查的能力,生产环境中除了postgres之外,应尽量避免创建其他超级用户。第三,对于需要临时提权的场景,使用SET ROLE而不是直接授予超级用户权限。第四,定期清理不再使用的角色和权限。离职人员的账号如果未及时回收,可能成为安全隐患。
还有一个常见问题是public模式的所有者。在PostgreSQL 15之前,public模式的所有者是postgres,普通用户无法在public模式下创建对象,但PUBLIC角色拥有CREATE权限。在PostgreSQL 15之后,public模式的所有者变更为pg_database_owner,这意味着数据库所有者可以管理public模式。如果你的数据库是从旧版本升级而来,建议检查public模式的所有权和权限设置,确保符合当前安全基线。
最后,安全优化不是一次性的工作。数据库的权限模型会随着业务变化而调整,新的表、新的角色、新的连接来源都会引入新的风险点。建议将权限管控纳入变更管理流程,每次数据库结构变更时同步评估权限影响。同时定期进行安全基线检查,确保权限配置始终处于预期状态。
