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模式的所有权和权限设置,确保符合当前安全基线。

最后,安全优化不是一次性的工作。数据库的权限模型会随着业务变化而调整,新的表、新的角色、新的连接来源都会引入新的风险点。建议将权限管控纳入变更管理流程,每次数据库结构变更时同步评估权限影响。同时定期进行安全基线检查,确保权限配置始终处于预期状态。