Skip to content
安全与权限管理

安全与权限管理

PostgreSQL 的安全体系是四层闸门:pg_hba 决定"能不能进门",认证方法决定"你是谁",GRANT 决定"你能碰什么",RLS 决定"你能看见哪几行";TLS、加密与审计负责传输和事后追责。本章按这个顺序逐层给出加固动作与可跑示例,基线 PG 18。


认证链路全景:先 pg_hba,再角色

一次连接在服务端的真实顺序是:匹配 pg_hba 规则 → 执行该行指定的认证方法 → 确认角色存在且带 LOGIN → 检查 CONNECTION LIMIT。也就是说,角色往往还没被查到,连接就已经被 hba 规则拒了。日志里出现 no pg_hba.conf entry 和 password authentication failed 是两类完全不同的问题:前者改规则表,后者查密码和存储哈希格式。排查安全事件时先分清是哪一类,方向立刻清楚(pg_hba 文件本身怎么写、怎么生效,见 00-安装与配置.md,这里只讲安全视角)。

规则是自上而下、首条命中即生效,这个匹配模型决定了整张表的安全态势:宽规则放在窄规则前面,会把后面的窄规则全部吃光。

#  TYPE   DATABASE  USER        ADDRESS           METHOD
local    all       all                           peer
host     all       all         127.0.0.1/32      scram-sha-256
hostssl  mydb      app         10.0.0.0/8        scram-sha-256
host     all       all         0.0.0.0/0         reject

三个常被忽略的点。hostssl 只放行 TLS 连接,且它前面不能存在更宽的 host 行,否则"强制加密"形同虚设;ADDRESS 列就是天然的来源 IP 白名单,生产环境把它收敛到应用子网,比任何应用层校验都硬;PG 16 起用户/库名支持 /正则 匹配、文件支持 include_dir 拆分管理,规则一多可以用,但新规则一律追加在兜底行之前、先在测试会话验证再 reload。

注意 改 pg_hba 不需要重启,SELECT pg_reload_conf(); 秒级生效。但 reload 前务必保留一个已登录会话做逃生通道——把 scram 行写错一个词、全实例应用同时掉线的事每年都有人做一遍。

认证方法怎么选

trust 不验证任何人,等于把"能连上 TCP"当身份证,只允许出现在开发机。password 是明文口令过线,除非配 hostssl 强制 TLS,否则不可用。md5 走挑战-应答,但服务端存储格式是 md5 || MD5(password || username)——一旦 pg_authid 的哈希落到攻击者手里或抓到一次认证流量,GPU 离线破解 MD5 的成本极低。scram-sha-256 实现 SCRAM 挑战-应答(RFC 5802),存储的是带盐、带迭代次数的 SHA-256 校验子,是当前唯一推荐;PG 14 起 password_encryption 的默认值已经是 scram-sha-256(官方 release notes 确认),现代版本新建集群时 initdb 写进 pg_hba 的 host 行默认方法同样是它。cert 用客户端证书认证(见 TLS 一节),ldap 接企业目录,两者都要求外部基础设施;PG 18 还新增了 oauth 方法,面向 IdP 签发 token 直连数据库的玩法。本机连接首选 peer(OS 用户即数据库用户,无密码过线)。

md5 到 scram 的迁移姿势

只改 GUC 不会动任何存量密码——存储哈希是在重设密码那一刻按当时的 password_encryption 重算的。所以正确节奏是三步:先确认服务端默认加密方式、再逐个角色重置密码、最后把 pg_hba 的 md5 换成 scram-sha-256 并 reload。

-- postgresql.conf: password_encryption = scram-sha-256(14+ 默认即是)
-- 确保新哈希按 scram 存储后,逐个账号重设一遍:
ALTER ROLE app_rw PASSWORD 'New-Strong-Password!2026';

两个边界情况。若 pg_hba 已切 scram-sha-256 而某角色存储的还是 md5 格式哈希,该角色连接会直接认证失败——scram 方法不接受 md5 校验子,存量账号必须先重设密码;批量迁移期可以 md5 与 scram-sha-256 两行并存过渡,全部重设完毕再把 md5 行删掉。迭代次数由 scram_iterations(PG 16 起)控制,合规要求高可上调,代价在每次认证的服务端 CPU;注意迭代次数固化在存储的校验子里,改参数对已有密码不追溯,要生效得再重设一次。

连接数与来源:把爆炸半径写进规则

角色层还有两个安全旋钮。ALTER ROLE app_rw CONNECTION LIMIT 40;(-1 为不限)挡住失控连接池打满 max_connections;pg_hba 的来源网段则是网络层白名单。两者都发生在干重活之前,比应用层限流更早止损。连接池与这个上限怎么对齐,见 08-Go客户端pgx.md。


角色模型:角色才是原子

CREATE USER 只是 CREATE ROLE ... LOGIN 的语法糖,库里存的角色都是 pg_roles 里的一行。属性跟着角色走:LOGIN、SUPERUSER、CREATEROLE、CREATEDB、BYPASSRLS、REPLICATION、NOINHERIT、CONNECTION LIMIT;权限跟着成员关系走。理解这一层,后面所有授权动作才顺。

最危险的三个属性值得单独点名。SUPERUSER 绕过一切权限检查(包括 RLS),不该出现在任何连接串里。BYPASSRLS 是"只绕过行级安全"的弱化版特权,给运维和备份角色用它,而不是 SUPERUSER(呼应后文 RLS)。CREATEROLE 常被低估:PG 16 之前它可以改任何非超级用户角色的密码、给别人加减成员,是一条现成的提权链——16 起官方收紧,加减成员需要对该角色持有 ADMIN OPTION,CREATEDB/REPLICATION/BYPASSRLS 等属性也只能授出自己所持有的(release notes 确认)。旧文档里"CREATEROLE 当半个管理员用"的说法要更新。

REPLICATION 属性是给 walsender 的:复制连接、pg_basebackup 只需要 REPLICATION + LOGIN,不该附带任何表权限,专用角色写法见 10-复制与高可用.md。

NOINHERIT 决定"成员身份是否自动生效"。默认成员被动继承组角色的全部权限;加了 NOINHERIT 就必须显式 SET ROLE 切过去,好处是会话身份可区分——session_user 始终是登录者,current_user 随切换而变,审计日志分得清谁在动手。

CREATE ROLE dba_admin LOGIN PASSWORD '...' NOINHERIT;
GRANT rw_group TO dba_admin;
-- 平时只有基础权限,需要写时:
SET ROLE rw_group;   -- current_user 变为 rw_group
RESET ROLE;

组角色模式是授权体系的地基:权限挂在组上,人只挂组,收权时撤销一个成员关系就干干净净。

CREATE ROLE ro_group NOLOGIN;                 -- 只读组
CREATE ROLE rw_group NOLOGIN;                 -- 读写组
CREATE ROLE report_a LOGIN PASSWORD '...';
CREATE ROLE app_svc  LOGIN PASSWORD '...';
GRANT ro_group TO report_a;
GRANT rw_group TO app_svc;

组角色三原则:NOLOGIN(不可直接登录)、不拥有对象、只当权限容器。另外 PG 17 起在"表权限"与"超级用户"之间多了一档 MAINTAIN:可按表 GRANT,也有预定义角色 pg_maintain,覆盖 VACUUM / ANALYZE / REINDEX / CLUSTER / LOCK TABLE / REFRESH MATERIALIZED VIEW(release notes 确认)——给日常做维护的运维账号这个,而不是 SUPERUSER。


授权体系:三层门、批量授权与默认权限

权限检查自上而下三层:CONNECT ON DATABASE → USAGE ON SCHEMA → 对象权限。三层缺一层都会呈现"能连库却查不到表",新手最常见的是漏了 SCHEMA USAGE——没有 USAGE 时表连"存在"都不可感知,报错还是 relation does not exist,非常误导排查。

各对象类型可授权的权限(按 PG 18 的 GRANT 语法核对官方文档):

对象 权限
DATABASE CONNECT / CREATE(在其内建新 schema)/ TEMPORARY(建临时表)
SCHEMA USAGE(可见性)/ CREATE(在其中建对象)
TABLE SELECT / INSERT / UPDATE / DELETE / TRUNCATE / REFERENCES(建外键引用它)/ TRIGGER(在其上建触发器)/ MAINTAIN(17+)
SEQUENCE USAGE(nextval)/ SELECT(currval)/ UPDATE(setval)
FUNCTION / PROCEDURE / ROUTINE EXECUTE
TYPE / DOMAIN USAGE

列级权限是真实存在的能力,适合"报表账号只能改备注列"这类诉求。注意只有四种权限能到列粒度(语法上 DELETE 没有列形式):

GRANT UPDATE (notes) ON TABLE app.tasks TO report_a;

批量授权覆盖存量对象:

GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO rw_group;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA app TO rw_group;

ALL TABLES 同时作用于视图与外部表(GRANT 文档原话),序列、函数也各有 ALL SEQUENCES / ALL ROUTINES;ALL ROUTINES 才包含存储过程,ALL FUNCTIONS 不含。

新对象不继承授权:ALTER DEFAULT PRIVILEGES

生产事故里出现频率极高的一句话是"应用账号怎么查不到刚建的表",根因在这:ALL TABLES IN SCHEMA 只覆盖执行那一刻已存在的对象,之后迁移脚本建的表,权限默认只归创建者。堵这个洞靠 ALTER DEFAULT PRIVILEGES——为"某个角色将来创建的对象"预设初始权限:

ALTER DEFAULT PRIVILEGES FOR ROLE migrate_role IN SCHEMA app
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO rw_group;
ALTER DEFAULT PRIVILEGES FOR ROLE migrate_role IN SCHEMA app
    GRANT USAGE, SELECT ON SEQUENCES TO rw_group;

两个必须刻进脑子的边界。第一,这条命令不影响已存在的对象(官方文档原话),历史欠账仍要回头跑一次 GRANT ... ALL TABLES。第二,FOR ROLE 指定的是对象创建者,而新对象创建时只应用"当前角色自己"的默认权限,不会从它所属的组角色继承(文档确认)——所以要么所有迁移固定走一个 migrate_role,要么每个会建表的角色的默认权限都配一遍。漏写 FOR ROLE 时改的是执行者自己的默认权限,跑之前先 SELECT current_user; 确认身份。

PUBLIC、所有权与回收

TO PUBLIC 是给全体角色(含未来新建的)加权限。默认权限里 PUBLIC 真正拥有的只有两样:DATABASE 级的 CONNECT/CREATE/TEMPORARY(新库从模板复制而来)和 PG 15 之前 public schema 上的 CREATE——正是下一节要拆的雷。给业务角色授 PUBLIC 等于给以后出现的每个账号开门,能用组角色就不用 PUBLIC。

对象的天然特权属于 owner:改对象、删对象、对该对象 GRANT/REVOKE 都是 owner 的事(超管除外)。转移所有权:

ALTER TABLE app.tasks OWNER TO app_owner;

普通角色转移所有权有个前置条件:既要能 SET ROLE 到目标角色,又是当前 owner;超管无限制。GRANT 文档还专门警告:如果一个角色要被继承却不允许被 SET ROLE,就应让它不拥有任何对象——否则它的成员可以重定义它的函数做特洛伊木马。

回收时 RESTRICT 与 CASCADE 处理的是转授依赖:经 WITH GRANT OPTION 转授出去的关系构成下游,RESTRICT(PostgreSQL 的默认行为)在存在下游时拒绝执行,CASCADE 连下游一起收掉。拿不准先只收转授权本身:

REVOKE GRANT OPTION FOR INSERT ON app.tasks FROM contractor;  -- 只收"再转授"的能力
REVOKE INSERT ON app.tasks FROM contractor CASCADE;

默认配置的三处雷

PG 出厂目标是"跑得起来"而不是"安全",三处默认值必须逐条验证:

public schema 人人可建对象。 15 之前所有角色在 public 上都有 CREATE,配合任意用户可连,构成 CVE-2018-1058 的 search_path 投毒前提(它的另一半防御在函数侧:SECURITY DEFINER 必须固定 search_path,安全模板见 13-函数与存储过程.md):攻击者在会话 search_path 靠前的 schema 里建一个同名对象,就能劫持别人的语句。PG 15 起新集群与新数据库不再给 PUBLIC 这个 CREATE,public 的 owner 也改为 pg_database_owner(每个库的 owner 天然拥有自己库的 public),但 release notes 说得很清楚:原地升级(pg_upgrade)和旧 dump 恢复会保留旧权限。所以库哪怕升到 18,这条也要手动确认:

REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- 之后用迁移角色把对象放进独立的业务 schema,别再堆 public

postgres 库人人可连。 新库权限从模板库复制,模板没收紧则新建库自动带上 PUBLIC 的 CONNECT。对存量:

REVOKE CONNECT ON DATABASE postgres FROM PUBLIC;

顺带把模板库本身收紧——但注意多数工具默认拿 template1 当模板,动它之前确认没有脚本在依赖(createdb -T template1 会连不上)。

超级用户一把梭。 一个 SUPERUSER 连接串泄漏等于整实例投降:读服务器文件(COPY FROM FILE)、SET ROLE 冒充任意角色、绕过 RLS,全在里面。动作就是后文最小权限三分法,日常维护用 pg_maintain / BYPASSRLS 的窄角色顶替 SUPERUSER。


行级安全 RLS:把策略写进表

表权限管"能不能碰这张表",RLS 管"能看见哪些行"。启用与绑定 owner 是两步:

ALTER TABLE app.tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE app.tasks FORCE ROW LEVEL SECURITY;  -- 连 owner 也纳入策略

生效语义(PG 18 文档核对):超级用户与带 BYPASSRLS 的角色永远绕过;表 owner 默认也绕过,FORCE 就是把 owner 拉回策略约束内;开启/关闭 RLS、增删策略则只有 owner(或超管)能做。策略按角色维度收束:TO rw_group 只约束该角色的成员,不在任何策略 TO 名单里、又被 FORCE 约束的角色会看到零行——这是"明明配了策略却什么都查不到"的一个经典原因,记得要么 TO PUBLIC 要么把角色放进名单。

多租户最小完整示例,读写都管,并且 fail-closed(忘设变量时宁可什么都看不见,也不泄露全部):

CREATE SCHEMA app;
CREATE TABLE app.tasks (
  id bigserial PRIMARY KEY,
  tenant_id text NOT NULL,
  title text NOT NULL
);
CREATE INDEX ON app.tasks (tenant_id, id);
ALTER TABLE app.tasks ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON app.tasks
    AS PERMISSIVE FOR ALL TO rw_group
    USING (tenant_id = current_setting('app.tenant_id', true))
    WITH CHECK (tenant_id = current_setting('app.tenant_id', true));

INSERT INTO app.tasks (tenant_id, title) VALUES ('acme', 'demo');

-- 以 rw_group 成员身份验证:
BEGIN;
SET LOCAL app.tenant_id = 'other';
SELECT * FROM app.tasks;   -- 0 行:USING 不匹配
COMMIT;

USING 管既有行是否可见,WITH CHECK 管新写入的行是否合规(文档:对 INSERT/UPDATE/MERGE,WITH CHECK 在 BEFORE 触发器之后、实际修改之前校验)。同一张表上多条 PERMISSIVE 策略之间相 OR,RESTRICTIVE 策略再相 AND——要"既按租户又按部门"就叠一条 PERMISSIVE 加一条 RESTRICTIVE,不必写复杂的嵌套布尔。

应用侧姿势只推荐一种:事务内 SET LOCAL app.tenant_id = ...,策略里 current_setting() 读取。SET LOCAL 随事务提交消失,不会因连接归还池子把上一个请求的租户身份漏给下一个请求——裸 SET 在连接池场景就是事故源。Go 端配合 pgx:

conn, _ := pool.Acquire(ctx)
defer conn.Release()
tx, _ := conn.Begin(ctx)
if _, err := tx.Exec(ctx, `SET LOCAL app.tenant_id = $1`, tenantID); err != nil { ... }

三个性能与边界情况顺带讲清。RLS 谓词本质上也是 WHERE:tenant_id 没有索引时每条查询按策略逐行过滤,大表直接退化,把 tenant_id 放复合索引第一列能让策略谓词走 index qual(见 04-索引实战.md,验证方式看 09-查询优化与EXPLAIN.md 的 Filter 与 Index Cond 区别)。策略表达式与查询条件的求值顺序有坑:优化器可能先于安全检查执行非 leakproof 函数,造成信息侧漏,所以策略保持"列与 GUC 比较"的最简形态,别在策略里塞外部函数。唯一约束与外键检查无视 RLS(完整性必须跨租户可见),别用约束报错的措辞去推断"别的租户是不是有这行"。备份要显式带 row_security=off——它不是绕过策略,而是让"被策略过滤"直接报错而不是静默漏行,否则 pg_dump 可能被策略悄悄掏空表(见 11-备份恢复与运维.md)。DBA 运维账号给 BYPASSRLS 而不是 SUPERUSER,呼应前文。


TLS:传输加密不等于身份信任

服务端开启:

# postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file  = 'server.key'
ssl_ca_file   = 'ca.crt'   # 仅需要校验客户端证书(mTLS)时配置

自签证书配 sslmode=require 足够内网使用;跨公网或有合规要求,就用内部 CA 或 Let’s Encrypt 签发的证书,让客户端能走 verify-full。客户端强度从弱到强是 disable/allow/prefer/require/verify-ca/verify-full,只有 verify-full 同时验证 CA 链与主机名。Go DSN 写法(呼应 08-Go客户端pgx.md):

postgres://app_rw@db.internal:5432/mydb?sslmode=verify-full&sslrootcert=/etc/pg/ca.pem

pg_hba 用 hostssl 强制该条目只接受 TLS 连接(前文规则示例第三行)。双向认证 mTLS 的完整形态三行:服务端配 ssl_ca_file,对应用 cert 方法——PG 18 文档明确,cert 方法会把客户端证书的 Common Name 与请求登录的数据库用户名比对,不一致可用 pg_ident.conf 做映射:

# postgresql.conf
ssl_ca_file = 'ca.crt'
# pg_hba.conf
hostssl mydb  svc  10.0.0.0/8  cert

一句话摆正位置:TLS 解决的是"信道不被窃听、不被篡改",不解决"连接者是谁"——除非用 cert 方法把证书身份映射成角色,否则认证仍然靠 scram,TLS 只是让它不用明文过线。


注入防线

唯一的正解是参数化:值永远是值,不参与语法。psql/libpq 侧是 $1 占位符走扩展协议,Go 端 pgx 的 Query(ctx, sql, args...) 默认 prepared,参数不会拼进 SQL 文本(见 08-Go客户端pgx.md)。先演示拼接为什么炸:

-- 错误示范:应用把输入直接拼进 SQL 字符串
-- 输入:willow' OR '1'='1
SELECT id FROM users WHERE nickname = 'willow' OR '1'='1';
-- 条件恒真,认证旁路;若驱动走简单查询协议,
-- 再拼上 '; DROP TABLE users; --' 就能接着执行第二条语句

但占位符只能替换"值",替不了表名、列名、ORDER BY 方向——这类动态标识符必须在应用层做白名单,再由服务端用 format() 做引号处理:%I 处理标识符(校验并加双引号),%L 处理字面量(加单引号并转义)。PL/pgSQL 动态 SQL 的正确与危险姿势对照:

-- 危险:EXECUTE 'SELECT id FROM app.tasks ORDER BY ' || sort_col || ' ' || dir;
-- 正确:白名单校验后交给 %I
DO $$
DECLARE col text := 'created_at';   -- 来自白名单 {id, created_at, title}
BEGIN
  EXECUTE format('SELECT id, %I FROM app.tasks ORDER BY %I DESC LIMIT 50', col, col);
END
$$;

若方向关键字需要动态,只能 %s 且来源必须是受限枚举(asc/desc)——关键字既不能 %I 也不能 %L,这是 %s 唯一的正当用途。ORM 也逃不出这条规则:框架的参数化只保护值位,当 ORDER BY、列名、表名来自请求体时仍是注入面,白名单要自己守住。


加密与数据保护

分层看。传输层就是 TLS,上一节讲过;存储层是 PostgreSQL 的空白——没有原生 TDE,数据文件明文躺在磁盘上。现实路线在基础设施层:LUKS 或云盘 KMS 加密卷,对"盘被整块拷走"负责,对"拿到 SQL 权限的人"无能为力。

列级加密(pgcrypto)能做的事有限,谨慎推荐:

CREATE EXTENSION pgcrypto;
SELECT pgp_sym_encrypt('身份证号', 'passphrase-from-kms');

代价三条:密文列无法范围查询与排序,等值检索只能确定性加密或哈希索引,而这两者都把"相等关系"泄露出去;密钥管理是无解的工程题——写进 SQL 参数会进日志(PG 文档明确提醒日志可能含敏感数据甚至明文口令),写进配置就要求 KMS 与轮换;密钥一旦丢失,数据等同于销毁。所以列级加密只用于"对 DBA 也要藏"的极少量列,别指望它替代磁盘加密。

备份文件加密(呼应 11-备份恢复与运维.md):

pg_dump mydb | gzip | gpg --symmetric --cipher-algo AES256 -o mydb.sql.gz.gpg

用 WAL-G 的话它自带原生加密(AES256GCM,也可接 KMS),异地备份链路建议默认开启。


审计:先分清"日志"和"审计"

log_statement 是枚举:none(默认)/ ddl / mod / all。ddl 只记 CREATE/ALTER/DROP 类结构变更,开销可忽略;mod 在 ddl 之上追加数据修改语句(INSERT/UPDATE/DELETE/TRUNCATE/COPY FROM,扩展协议下绑定值也会入日志);all 连 SELECT 一起记,高 QPS 实例会把日志变成瓶颈,而且日志本身就成了敏感数据载体。生产基线是 log_statement = ddl,追慢 SQL 用 log_min_duration_statement 或采样参数,而不是 all。

日志没有身份字段等于没记,log_line_prefix 至少带上用户、来源和库:

# postgresql.conf:%m 毫秒时间 %p PID %u 用户 %d 库 %r 客户端IP:端口 %a application_name
log_line_prefix = '%m [%p] %q%u@%d %r %a '
log_destination = 'csvlog'

合规级审计的社区标准答案是 pgAudit 扩展:按语句类别记录(pgaudit.log = 'ddl, role, read, write, function'),可实例级与会话级配置,补上 log_statement 粗颗粒的缺口。表结构变更追责还可以挂事件触发器(事件触发器全家——DDL 钩子、权限限制、写法——在 14-触发器.md 有专章),把 DDL 落进一张审计表:

CREATE TABLE app.ddl_audit (
  ts timestamptz DEFAULT now(),
  who text,
  command_tag text,
  object_identity text
);
CREATE OR REPLACE FUNCTION app.log_ddl() RETURNS event_trigger AS $$
BEGIN
  INSERT INTO app.ddl_audit (who, command_tag, object_identity)
  SELECT current_user, command_tag, object_identity
  FROM pg_event_trigger_ddl_commands();
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER catch_ddl ON ddl_command_end EXECUTE FUNCTION app.log_ddl();

最后一层认知:审计不等于日志。日志会轮转覆盖、会被有机器权限的人删改,只到"事后尽力查"的档位;真正的审计要求独立落盘或异地投递、不可篡改与保留期承诺——pgAudit 加采集 agent 是当前社区最常见的组合。


最小权限落地:三分法

生产实例建议至少三种角色,各用各的连接串;超级用户不作为常态存在,只留在 break-glass 流程里:

角色 属性与权限 用途
app_rw LOGIN、经 rw_group 得 DML、无 DDL、CONNECTION LIMIT 应用服务
migrate LOGIN、schema 对象 owner、DDL,配好 ALTER DEFAULT PRIVILEGES FOR ROLE migrate 迁移工具(Flyway/goose 等),只在发布窗口启用
report_ro LOGIN、仅经 ro_group 只读 BI 与报表

日常维护(VACUUM/ANALYZE/REINDEX)单给 pg_maintain 或按表 MAINTAIN(17+),不需要 SUPERUSER——这是很多团队至今没拆干净的一档。DSN 里不该出现超级用户,连接串从环境变量或 KMS 注入、不进代码仓库,呼应 08-Go客户端pgx.md。云托管实例的高权限账号(RDS 系的 rds_superuser 一类)不是真正的 SUPERUSER——文件系统访问、shared_preload_libraries 都受限——但它的提权半径对业务数据已经足够致命,别拿"它本来就不是超管"当多给权限的借口。


安全上线检查清单

  • pg_hba 无 trust(开发机除外);生产条目用 hostssl 且前面没有更宽的 host 抢匹配
  • password_encryption = scram-sha-256,且所有存量账号已重设密码完成 md5 → scram
  • REVOKE CREATE ON SCHEMA public FROM PUBLIC 已执行(旧集群升级会保留旧权限,15+ 新建库才默认收紧)
  • REVOKE CONNECT ON DATABASE postgres FROM PUBLIC,模板库按需收紧
  • 任何应用连接串中不存在 SUPERUSER;超管走 break-glass 审批
  • app_rw / migrate / report_ro 三分角色,权限挂在组角色上
  • ALTER DEFAULT PRIVILEGES FOR ROLE migrate_role 已配置,新表不再漏授权
  • 复制账号仅 REPLICATION + LOGIN(见 10-复制与高可用.md)
  • ssl = on,客户端 sslmode=verify-full(或对服务间 mTLS)
  • 多租户表 ENABLE + FORCE,应用每请求 SET LOCAL,策略谓词有索引支撑
  • 代码评审过全量 SQL 拼接点;动态标识符全部走白名单 + format('%I')
  • log_statement = ddl,log_line_prefix 含 %u %r %a
  • DDL 有事件触发器或 pgAudit 兜底,日志投递到实例之外
  • 备份文件加密后再离站(gpg 或 WAL-G 原生加密,见 11-备份恢复与运维.md)
  • CONNECTION LIMIT 与连接池上限对齐(见 08-Go客户端pgx.md)

实战建议

安全加固的顺序建议就按本章层次来:先管入口(pg_hba 规则顺序 + TLS),再管身份(角色属性、组角色、NOINHERIT),再管权限面(三层门 + DEFAULT PRIVILEGES 堵新对象),最后才轮到 RLS、pgcrypto、pgAudit 这些精细工具。反过来先上 RLS 是白费功夫——一个带 SUPERUSER 的连接串就能绕过一切。

多数生产事故不是 0-day,而是出厂默认值原封不动跑了三年:public schema 的 CREATE、postgres 库的 PUBLIC CONNECT、一个共享的超级用户密码。这三条今天花十分钟就能收掉,而且都不需要停机——REVOKE 加 reload 而已。升级大版本时把 pg_hba 与权限收紧当成迁移脚本的一部分写进流程,pg_upgrade 不会替你把旧集群的宽松 ACL 换成新默认,这是 15 release notes 白纸黑字写明的行为。

最后用攻击者视角过一遍:能不能连上(hba + 网段 + TLS)、能不能登录(强认证 + 无共享超管)、登进来能读什么(三层门 + 默认权限)、能不能越权写(列级权限 + RLS + FORCE)、动了会不会留痕(日志格式 + 事件触发器 + pgAudit)。五问都能答上"否"或"会",这一章的目的就到了。