pgaudit 扩展
log_statement 是个合格的「留痕开关」,但离「审计系统」还有距离:它只有三挡粗粒度,日志行没有结构化的类别与对象字段,也做不到「只审计某几张表」。当你需要回答「过去一周谁对 payment 表做过写操作」这种精确到对象的问题时,就该请出本页的主角——pgaudit。
pgaudit 是 PGDG(PostgreSQL Global Development Group)维护的官方审计扩展,代码独立于 contrib 之外,仓库位于 GitHub 的 pgaudit/pgaudit。它在标准的 PostgreSQL 日志机制之上,产出带统一格式标记的审计日志:每条记录明确标注会话审计还是对象审计、语句类别、命令类型、对象名、语句文本与参数。正因为格式标准,它可以被日志平台直接解析、入库、出报表,这是它与裸 log_statement 最大的区别。
pgaudit 提供两个层次的审计能力:会话审计(session audit logging)记录某用户/数据库会话中按类别选定的全部语句;对象审计(object audit logging)只记录对指定对象的 READ / WRITE 操作。两者可以叠加使用。本页从安装讲起,逐个解析关键 GUC,给出两类审计的完整示例,并讨论与结构化日志、日志收集链路的集成方式与性能代价。
前置知识
基础概念
pgaudit 的核心词汇表不大,但每个词都会在配置里反复出现:
| 术语 | 英文 / 记法 | 说明 |
|---|---|---|
| 会话审计 | session audit logging | 按类别记录某会话中的全部语句,由 pgaudit.log 控制 |
| 对象审计 | object audit logging | 只记录对特定对象的读写,由 pgaudit.role + GRANT 声明 |
| 审计类别 | class | READ / WRITE / FUNCTION / ROLE / DDL / MISC / MISC_SET / ALL |
| 主审计角色 | master audit role | pgaudit.role 指定的角色,被授予该角色的权限决定被审计的对象 |
| AUDIT 记录 | AUDIT: entry | 带固定字段顺序的日志行,前缀 AUDIT: |
| 预加载 | shared_preload_libraries | pgaudit 必须随服务器启动加载,不支持动态加载 |
会话审计与对象审计
两种审计模式回答的问题不同:
- 会话审计:以「会话/角色/数据库」为筛选维度,「这个库里的所有 DDL 都给我记下来」「这个角色的所有写操作都给我记下来」。粒度由
pgaudit.log的类别组合决定,作用域可以用ALTER DATABASE ... SET pgaudit.log/ALTER ROLE ... SET pgaudit.log下沉到库或角色。 - 对象审计:以「对象」为筛选维度,「只审计
public.payment这张表上发生的一切读写」。实现方式很巧妙——不是声明一张对象清单,而是创建一个主审计角色,把要审计的对象的权限GRANT给它;凡是主角色拥有权限的对象,其读写操作就会被记录。
对象审计只覆盖 READ 与 WRITE 两个类别(也就是 SELECT / INSERT / UPDATE / DELETE / TRUNCATE / COPY 等表级读写),DDL 与管理操作仍要靠会话审计覆盖——这个分工决定了「DDL 全审计 + 敏感表对象审计」是标准的组合姿势。
AUDIT 日志的固定格式
pgaudit 的每条日志以 AUDIT: 开头,字段固定为九段、逗号分隔:
AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.account,select * from account,<not logged>| 段位 | 示例值 | 含义 |
|---|---|---|
| 1 | SESSION / OBJECT | 审计类型:会话审计或对象审计 |
| 2 | 1 | 语句 ID(会话内自增) |
| 3 | 1 | 子语句 ID |
| 4 | READ | 语句类别(class) |
| 5 | SELECT | 命令类型(command tag) |
| 6 | TABLE | 对象类型 |
| 7 | public.account | 对象全名 |
| 8 | select * from account | 语句文本 |
| 9 | <not logged> | 参数(未开 pgaudit.log_parameter 时为 <not logged>) |
固定格式是 pgaudit 的核心价值:采集端按逗号切分即可入库建索引,「过去一周对 public.payment 的所有 UPDATE」变成一条过滤查询而不是一次人肉翻日志。
版本分支与 PG 大版本的对应
pgaudit 的版本号与 PostgreSQL 大版本强绑定:每个 PG 大版本都有专属的 pgaudit 分支,安装时必须选择与实例大版本一致的分支。较新的发布直接沿用 PG 大版本号(如 17.x 对应 PG17、18.x 对应 PG18),更早的版本沿用 1.x 编号(如 1.7 对应 PG15)。跨大版本升级实例时,pgaudit 需要同步更换为对应分支的包并重建扩展。
pgaudit 在日志管线中的位置
理解 pgaudit 的产出如何到达你眼前,有助于在「审计没记到」时快速定位该查哪一环。pgaudit 自己没有存储、没有后台进程、也不产生独立的审计文件——它挂载在每条语句的执行钩子上,判定类别与对象后,把 AUDIT 文本作为一条普通的服务端日志消息交给标准日志管线,之后的一切与普通日志完全相同:
SQL 语句
│ 解析 / 规划 / 执行
▼
执行器 / 工具语句钩子(pgaudit 挂载点,逐语句判定类别与对象)
│ 拼装 AUDIT 文本
▼
服务端日志消息(级别默认 LOG)
▼
logging_collector ──► stderr / csvlog / jsonlog 文件
▼
采集器(Filebeat / Fluent Bit / Promtail)──► 集中日志平台这条链路带来三个直接推论:
- pgaudit 与日志基础设施共存亡。
logging_collector未开、日志磁盘写满、采集器断流,任何一环断了审计就断了。日志基础设施的巡检项全部适用于审计,无需另建一套。 - AUDIT 记录默认以 LOG 级别输出。 在服务端日志分级中 LOG 的位置高于 ERROR(仅次于 FATAL / PANIC),因此默认
log_min_messages = warning下审计记录必然落盘;只有把该参数调到fatal这类极端值才会把 AUDIT 行过滤掉(分级细节以官方文档为准)。 - 审计延迟 = 日志链路延迟。 pgaudit 是「事后留痕」而非「实时阻断」,从语句执行到审计平台可检索,中间隔着采集与索引延迟,做取证时要给时间窗留出余量。
语法与用法
安装
推荐直接使用 PGDG 仓库的二进制包(包名随 PG 大版本变化,形如 postgresql-<major>-pgaudit):
# Debian/Ubuntu(PGDG 仓库)
sudo apt install postgresql-18-pgaudit
# RHEL/CentOS(PGDG 仓库)
sudo yum install pgaudit18_18也可以从源码编译(与内核版本对应的 pgaudit 分支):
git clone https://github.com/pgaudit/pgaudit.git
cd pgaudit && git checkout REL_18_STABLE
make USE_PGXS=1 PG_CONFIG=/usr/pgsql-18/bin/pg_config
sudo make USE_PGXS=1 install预加载与启用
# postgresql.conf —— 两步缺一不可
# 第一步:预加载(需要重启实例,不是 reload)
shared_preload_libraries = 'pgaudit'sudo systemctl restart postgresql-18-- 第二步:重启后创建扩展
CREATE EXTENSION pgaudit;
-- 验证
SELECT * FROM pg_extension WHERE extname = 'pgaudit';必须先预加载再建扩展
pgaudit 依赖 shared_preload_libraries 预加载。跳过第一步直接 CREATE EXTENSION pgaudit 会得到报错,扩展无法工作。修改 shared_preload_libraries 后必须重启实例,规划窗口时别把它当成可以热更的参数。
会话审计配置
# postgresql.conf:全实例记录读 + 写 + DDL
pgaudit.log = 'read, write, ddl'
# 常用配套
pgaudit.log_parameter = on # 记录绑定参数(敏感库慎用)
pgaudit.log_relation = on # 每个涉及的关系单独一条记录
pgaudit.log_statement_once = off # 每条 AUDIT 都带语句文本(默认)也可以只对某个库/角色开启,不影响全局:
-- appdb 记录除 FUNCTION/MISC 外的全部类别
ALTER DATABASE appdb SET pgaudit.log = 'all, -misc';
-- 运维角色记录一切
ALTER ROLE ops_admin SET pgaudit.log = 'all';
-- 验证某会话的生效值
SHOW pgaudit.log;对象审计配置
-- 1. 确认扩展已创建、预加载已生效
CREATE EXTENSION pgaudit;
-- 2. 创建主审计角色并指定
CREATE ROLE auditor NOLOGIN;
SET pgaudit.role = 'auditor';
-- 生产环境建议写入 postgresql.conf:pgaudit.role = 'auditor'
-- 3. 把要审计的表的对象权限授予审计角色
GRANT SELECT ON public.payment TO auditor;
GRANT INSERT, UPDATE, DELETE ON public.payment TO auditor;
-- 4. 之后对 payment 的读写都会产生 OBJECT 类型审计记录
SELECT * FROM public.payment LIMIT 1;AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.payment,SELECT * FROM public.payment LIMIT 1,<not logged>安装与生效检查清单
「装了」不等于「在记」。按下表逐项核对,每一步都有明确的验证命令——这套清单同时适用于新装与迁移后的重建:
| 检查项 | 验证方式 | 期望结果 |
|---|---|---|
| 预加载生效 | SHOW shared_preload_libraries; | 值中包含 pgaudit |
| 扩展已创建 | SELECT extname, extversion FROM pg_extension WHERE extname = 'pgaudit'; | 返回一行,版本与实例大版本匹配 |
| 语句日志三参数归位 | 查 pg_settings 中 log_statement / log_min_duration_statement / log_min_error_statement | 分别为 none / -1 / error |
| 会话审计挡位 | SHOW pgaudit.log; | 与策略一致(默认 none) |
| 对象审计挂载点 | SHOW pgaudit.role; | 与策略一致(默认空) |
| 端到端实测 | 执行探针语句后检查日志 | 日志中出现对应 AUDIT 行 |
-- 端到端实测:用一张探针表验证整条链路
CREATE TABLE public.pgaudit_probe (id int);
DROP TABLE public.pgaudit_probe;
-- 立即查看日志(或日志平台),应能检索到两条 DDL 类 AUDIT 记录常见故障与现象对照:
| 现象 | 根因 | 处置 |
|---|---|---|
CREATE EXTENSION pgaudit 直接报错 | 未配置预加载就建扩展 | 补 shared_preload_libraries 并重启 |
| 配置齐全但日志中永远没有 AUDIT 行 | 换环境时 preload 丢失,扩展虽在但不工作 | 核对 SHOW shared_preload_libraries,补配并重启 |
| AUDIT 行有,但用户 / 库名缺失 | log_line_prefix 未配置会话信息 | 补齐前缀(见语句日志一页) |
| 升级后扩展版本与实例大版本不匹配 | 未更换对应分支的包 | 安装对应分支的包后重建扩展 |
详细说明
为什么必须 shared_preload_libraries
pgaudit 的工作方式是挂进后端进程的语句执行钩子(executor / utility hooks),在语句真正执行前后生成审计记录。这类钩子必须在进程启动阶段就装配好,而 PostgreSQL 的普通扩展只在被 CREATE EXTENSION 后按需加载,无法保证「每一条语句都被钩住」——特别是抢先于审计挂载之前执行的语句。
预加载还有一个副作用:pgaudit 在服务器启动时就接管并校验语句日志相关的三项参数(log_statement、log_min_duration_statement、log_min_error_statement),要求它们保持在其要求的默认值(none / -1 / error)上;运行时改动会触发报错。这是有意为之的设计——pgaudit 自己负责全部语句审计,从机制上杜绝「裸语句日志与审计日志混写、重复记录」。
八个审计类别逐条解析
pgaudit.log 的取值是类别的任意组合(逗号分隔、大小写不敏感),支持 -class 形式做排除(如 'all, -misc'):
| 类别 | 覆盖的语句 | 典型用途 |
|---|---|---|
READ | SELECT;源为关系的 COPY | 数据导出审计 |
WRITE | INSERT / UPDATE / DELETE / TRUNCATE;目标为关系的 COPY | 数据变更审计 |
FUNCTION | 函数调用与 DO 块 | 函数体可能包含隐藏写操作,敏感库建议开启 |
ROLE | GRANT / REVOKE / CREATE / ALTER / DROP ROLE 等角色与权限类语句 | 权限变更留痕(合规硬要求) |
DDL | 不属于 ROLE 类的全部 DDL | 结构变更留痕 |
MISC | DISCARD / FETCH / CHECKPOINT / VACUUM / SET | 噪音较大,通常不纳入日常审计 |
MISC_SET | 其余 SET 类命令(SET ROLE、RESET ALL 等) | 排查会话环境篡改时临时开启 |
ALL | 以上全部 | 仅限管理角色或排障窗口 |
两个容易误判的点:其一,DDL 类别不包含 ROLE 类语句——想同时抓建表和授权,要写 'ddl, role';其二,READ 类会把所有 SELECT 记下来,在查询密集的 OLTP 库上这是最大的日志量来源,通常只在对象审计或特定角色上开启。
AUDIT 字段在两种模式下的差异
同一条语句在会话审计与对象审计下的记录形态略有不同,理解差异有助于设计解析规则:
- 会话审计(
SESSION)的一条记录通常覆盖语句涉及的全部对象(pgaudit.log_relation = on时改为每个关系一条); - 对象审计(
OBJECT)只对「主审计角色拥有对应权限」的对象各产生一条记录,且第 4 段固定为READ/WRITE; - 对象审计不依赖
pgaudit.log的开关——即使pgaudit.log = 'none',只要pgaudit.role生效且有GRANT,对象审计照常工作。这让「平时静默、只盯核心表」的极简策略成为可能。
失败语句也会留下审计记录
审计的价值不仅在于记录成功的行为,更在于记录被拒绝的尝试。按 pgaudit 的官方说明,审计挂在语句解析之后:语法错误一般不会产生 AUDIT 记录,但解析通过后在规划或执行阶段失败的语句——权限拒绝、对象不存在、约束冲突——都会留下记录(具体行为以所用版本文档为准)。
这对安全很有意义:攻击者常用「报错探测」摸清表结构与权限边界,一批以 permission denied 收场的 SELECT 的 AUDIT 记录就是清晰的越权探测信号。配合采集端规则(同一来源短时间大量「错误 + 查询」组合),可以把这类行为变成告警。注意 AUDIT 记录本身的字段与成功语句相同,失败与否要结合日志中伴随的错误行判断——这也是 pgaudit 要求 log_min_error_statement 保持 error 的原因之一:错误详情与审计记录在同一份日志里才能对上。
关键 GUC 速查
| GUC | 默认值 | 说明 |
|---|---|---|
pgaudit.log | none | 会话审计类别组合 |
pgaudit.role | 未设置 | 对象审计主角色 |
pgaudit.log_catalog | on | 关闭后,仅访问系统目录(pg_catalog)的语句不再记录,显著降噪 |
pgaudit.log_client | off | 开启后 AUDIT 消息同时发回客户端(用 pgaudit.log_level 指定级别);审计日志应以服务端为唯一落点,生产保持 off |
pgaudit.log_level | log | AUDIT 记录使用的日志级别;仅超级用户可设置,可选 debug1~debug5 / info / notice / warning / log |
pgaudit.log_parameter | off | 开启后第 9 段记录绑定参数值 |
pgaudit.log_parameter_max_size | 0 | 开启 log_parameter 后,限制可变长参数记录的最大字节数,0 表示不限制 |
pgaudit.log_relation | off | 开启后,会话审计为语句涉及的每个关系各出一条记录 |
pgaudit.log_rows | off | 开启后,AUDIT 记录附加返回/影响行数 |
pgaudit.log_statement_once | off | 开启后,语句文本与参数只在同语句的首条 AUDIT 记录中出现,降低多记录语句的日志量 |
pgaudit.log_nested | off | 开启后,记录函数内部执行的嵌套语句(默认只记外层调用) |
常用取值组合的取舍
把类别组合当成「菜单」随手勾选,容易配出噪音与盲区并存的配置。下表是几组经过验证的常用组合与适用场景:
| 组合 | 适用环境 | 日志量 | 备注 |
|---|---|---|---|
ddl, role | 一切实例的底线 | 极低 | DDL 与授权低频,几乎零成本 |
ddl, role + 对象审计 | 标准生产形态 | 低 | 核心表读写走 OBJECT 记录 |
all(限运维角色) | 管理账号 | 低 | 人少、低频、全量可解释 |
read, write(会话级) | 低流量分析库 | 中高 | OLTP 库慎用,READ 是大头 |
all, -misc | 排障窗口 | 高 | 临时放大用,用完收回 |
write + 慢查询抽样 | 高频业务库 | 中 | 沿用语句日志一页的抽样思路 |
取舍的两条主线:类别越靠近 READ,越应该用对象审计而不是会话审计——把范围交给 GRANT 声明,而不是全量拦截;类别越靠近 DDL / ROLE,越应该无条件会话审计——低频高危,不做任何妥协。pgaudit.log_relation 与 pgaudit.log_statement_once 是量化的微调旋钮,只在确认需要逐关系记录时才打开。
pgaudit.log_catalog:压掉工具噪音
psql 的元命令、各类运维工具、连接探活脚本都会高频访问 pg_catalog,这类语句在审计上几乎没有价值,却能在 pgaudit.log = 'read' 时把日志刷成 catalog 查询的海洋。pgaudit.log_catalog = off 的语义是:当语句涉及的全部关系都在系统目录里时,不产生审计记录。业务表上的查询不受影响。实践中这几乎是必开项,尤其是有监控探针每几秒查一次系统视图的环境。
对象审计的 GRANT 语义与陷阱
对象审计的声明式设计很优雅,但有几条必须理解的语义:
- 审计跟着权限走。
GRANT SELECT ON public.payment TO auditor之后,任何人对payment的SELECT都会被记录——不管执行者是谁、有没有被授予过其他审计角色。 - 撤销审计同样用
REVOKE ... FROM auditor。要审计整张表的全部读写,就把相应的读/写权限都授给主角色。 - 多个审计范围用多个角色管理:创建若干二级角色,分别授不同对象集,再把它们都授给
pgaudit.role指定的主角色,即可实现「按业务域分组审计」。 - 主审计角色应该是
NOLOGIN的纯声明角色——它不需要登录,只作为权限的挂载点。这也让它天然不进入「账号滥用」的嫌疑名单。 - 修改
pgaudit.role或权限后,新语句立即按新声明审计,无需重启。
与结构化日志(csvlog / jsonlog)的集成
AUDIT 记录本质上是普通日志行,因此 pgaudit 与日志形态的集成非常直接:
# PG15+ 推荐 jsonlog:每行日志是一个 JSON 对象
logging_collector = on
log_destination = 'jsonlog'
log_line_prefix = '%m [%p] %q%u@%d ' # jsonlog 下多数字段自动结构化,前缀仅作补充{"timestamp":"2026-10-07 14:22:08.912 CST","user":"ops_admin","dbname":"appdb",
"pid":33120,"remote_host":"10.0.8.4","session_start":"2026-10-07 14:21:55 CST",
"level":"LOG","message":"AUDIT: SESSION,1,1,DDL,ALTER TABLE,TABLE,public.payment,ALTER TABLE public.payment DROP COLUMN remark,<not logged>"}jsonlog 下,用户、库、来源 IP、PID 都是独立字段,AUDIT 内容在 message 里——采集端只需对 message 做一次 AUDIT: 前缀判断加逗号切分,即可得到一张可直接查询的审计事实表。PG15 之前的版本用 csvlog 达到同样效果。顺带一提,AUDIT 行同样会带上 log_line_prefix / 结构化字段里的会话信息,这正是第 1~3 章强调统一前缀的原因:连接审计、语句审计、pgaudit 共用同一套归因元数据。
性能影响评估
pgaudit 的开销集中在三类:
- 每语句的钩子处理:对每条被审计的语句都要拼装 AUDIT 文本并写日志,语句越短、QPS 越高,相对开销越明显。以中等规模 OLTP(数千 QPS)实测经验看,开启
read, write的全量会话审计会带来个位数百分点的吞吐下降与可感知的日志 I/O 增长;只开ddl, role时几乎无感。 - 日志 I/O 与磁盘:与
log_statement同源,预算方法一致(见语句日志一页的磁盘预算小节)。pgaudit.log_relation = on会让多表 JOIN 的每条语句产生 N 条记录,开启前必须评估。 - 对象审计的权限解析:GRANT 集合很大时,每次语句都要判断「主角色是否拥有该对象权限」,极端场景有可测量的开销。保持对象审计范围聚焦在核心敏感表上即可。
降本手段与语句日志一脉相承:按角色/库分域设置 pgaudit.log、用 pgaudit.log_statement_once 折叠重复文本、用 pgaudit.log_catalog = off 压噪音、把 READ 类别留给对象审计而不是全量会话审计。
日志量估算:放大系数与预算
上审计之前先做一笔账。方法:以当前日志量(或语句日志实测值)为基数,乘以「类别 × 形态」决定的放大系数,得到 AUDIT 日志的日增量预估:
日增 AUDIT 体积 ≈ Σ(各类别语句量 × 该类别平均 AUDIT 行体积 × 放大系数)
经验参考(须以自己环境实测为准):
ddl, role 系数≈0 低频事件,体积可忽略
对象审计(核心表) 与表流量成正比 每行约 200~500 字节(视语句长度)
read/write 会话审计 ≈ 全量语句日志 再叠加 log_relation 的多表放大
log_relation = on × 涉及关系数 多表 JOIN 逐关系展开
log_statement_once = on 折叠重复文本 多记录语句场景显著省量一个演算示例:核心表 50 QPS、平均 AUDIT 行 300 字节,对象审计的日增量约为 50 × 300 × 86400 ≈ 1.2 GB(未压缩);接入压缩归档后通常降到数百 MB 量级。这笔账要写进策略文档,作为存储预算与保留期讨论的输入——审计日志「默默吃满磁盘」是比安全事件本身更常见的生产事故。
升级与运维注意
- 实例跨大版本升级(pg_upgrade / 逻辑复制迁移)时,新实例必须安装对应分支的 pgaudit 并重建
CREATE EXTENSION;shared_preload_libraries的配置要同步带过去,否则新实例的审计是静默失效的。 pgaudit的 GUC 变更大多reload即可生效(pgaudit.log、pgaudit.log_catalog等),但改动pgaudit.role后建议抽查一条目标对象语句验证生效。- 监控上除了「扩展是否安装」,更要监控「AUDIT 日志是否在增长」——预加载丢失、扩展被误删都会让审计静默失效,用日志量指标兜底是最简单可靠的自检。
实战示例
示例一:全实例 DDL + 权限变更审计
最常见的落地形态:结构变更与权限变更全量留痕,业务流量零感知。
# postgresql.conf
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'ddl, role'
pgaudit.log_catalog = off
pgaudit.log_client = off-- 重启后
CREATE EXTENSION pgaudit;
-- 模拟一次变更
CREATE TABLE public.t_audit (id int PRIMARY KEY);
GRANT SELECT ON public.t_audit TO app_read;
DROP TABLE public.t_audit;AUDIT: SESSION,1,1,DDL,CREATE TABLE,TABLE,public.t_audit,CREATE TABLE public.t_audit (id int PRIMARY KEY),<not logged>
AUDIT: SESSION,2,1,ROLE,GRANT,TABLE,public.t_audit,GRANT SELECT ON public.t_audit TO app_read,<not logged>
AUDIT: SESSION,3,1,DDL,DROP TABLE,TABLE,public.t_audit,DROP TABLE public.t_audit,<not logged>注意第二条记录的类别是 ROLE 而不是 DDL——GRANT 属于 ROLE 类,这正是 'ddl, role' 必须成对出现的原因。
示例二:敏感表对象审计 + 业务角色会话审计
需求:public.payment(支付流水)与 public.customer(客户主档)的一切读写单独留痕;同时 app_write 角色的写操作按会话审计。
-- 对象审计声明
CREATE ROLE auditor NOLOGIN;
SET pgaudit.role = 'auditor';
GRANT SELECT, INSERT, UPDATE, DELETE ON public.payment TO auditor;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.customer TO auditor;
-- 会话审计下放到角色
ALTER ROLE app_write SET pgaudit.log = 'write';-- 以 app_write 会话执行
UPDATE public.payment SET status = 'void' WHERE id = 1001;
SELECT * FROM public.customer WHERE id = 88;AUDIT: OBJECT,1,1,WRITE,UPDATE,TABLE,public.payment,UPDATE public.payment SET status = 'void' WHERE id = 1001,<not logged>
AUDIT: OBJECT,2,1,READ,SELECT,TABLE,public.customer,SELECT * FROM public.customer WHERE id = 88,<not logged>两条记录都是 OBJECT 类型:只要对象在审计角色的权限集内,读写都会被对象审计捕获,与执行者是谁无关。若此时 app_write 再写一张未列入审计的表(如 orders),会话审计(pgaudit.log = 'write')会以 SESSION 记录兜住它——两层网同时在工作。
示例三:接入 jsonlog 与审计事实表
log_destination = 'jsonlog'
logging_collector = on
pgaudit.log = 'ddl, role'
pgaudit.role = 'auditor'采集器(以 Filebeat 为例)把 json 日志行送入 Elasticsearch 后,message 字段按九段格式切分入库;也可以在数据库侧直接用 SQL 验证格式(读取原始日志文件的简化演示):
-- 演示:按 AUDIT 前缀切分字段(假设日志已导入 audit_raw 表,line 为日志行文本)
SELECT split_part(line, ',', 1) AS audit_type,
split_part(line, ',', 4) AS class,
split_part(line, ',', 5) AS command,
split_part(line, ',', 7) AS object_name,
split_part(line, ',', 8) AS statement_text
FROM audit_raw
WHERE line LIKE 'AUDIT:%'; audit_type | class | command | object_name | statement_text
------------+-------+-------------+------------------+--------------------------------------------
AUDIT: OBJECT | WRITE | UPDATE | public.payment | UPDATE public.payment SET status = 'void'
AUDIT: OBJECT | READ | SELECT | public.customer | SELECT * FROM public.customer WHERE id = 88落到这一步,「过去一周谁对 payment 表做过什么」就只是一条 WHERE object_name = 'public.payment' 的查询了。
示例四:从 log_statement 迁移到 pgaudit
环境原状:log_statement = 'ddl',现要迁到 pgaudit 获得结构化字段与对象审计能力。pgaudit 接管后会锁定语句日志参数,因此迁移是「一次切换」而不是「并行运行」,按清单执行:
# 迁移前的现状快照(记录下来用于回滚)
log_statement = 'ddl'# 1. 安装对应分支的包并预加载(需要重启窗口)
sudo apt install postgresql-18-pgaudit
# postgresql.conf: shared_preload_libraries = 'pgaudit'
sudo systemctl restart postgresql-18-- 2. 重启后创建扩展
CREATE EXTENSION pgaudit;# 3. 切换生效配置:旧开关归零,新挡位对齐原语义并补齐 ROLE 类
log_statement = 'none'
pgaudit.log = 'ddl, role' # 原本只有 ddl,迁移时补上 role
pgaudit.log_catalog = off# 4. reload + 验证
sudo systemctl reload postgresql-18
psql -c "CREATE TABLE public.mig_probe(id int); DROP TABLE public.mig_probe;"
grep -c "AUDIT:" /var/log/postgresql/postgresql-18-main.log # 应大于 0
grep "statement:" /var/log/postgresql/postgresql-18-main.log # 不应再出现裸 DDL 语句行# 回滚方案(若验证失败):恢复旧配置并 reload
# log_statement = 'ddl';pgaudit.log = 'none'
# 即回到迁移前的记录形态,无数据风险迁移的收益清单:AUDIT 行带类别 / 命令 / 对象的结构化字段、可下放到库与角色、可叠加对象审计;代价是语句日志参数被接管——这正是设计意图,不是缺陷。
示例五:审计健康度每日自检
审计链路最怕「静默失效」。用一条 cron 任务每天检查四个点,任何一项异常即告警(脚本骨架,按环境调整路径与连接方式):
#!/usr/bin/env bash
# pgaudit 每日自检骨架(输出接入监控告警)
PSQL="psql -XAt -d postgres"
PGLOG=$(ls -t /var/log/postgresql/*.log | head -1) # 最新日志文件
# 1) 扩展在位
$PSQL -c "SELECT extname FROM pg_extension WHERE extname = 'pgaudit'" | grep -q pgaudit \
|| echo "ALERT: pgaudit extension missing"
# 2) 预加载在位
$PSQL -c "SHOW shared_preload_libraries" | grep -q pgaudit \
|| echo "ALERT: pgaudit not preloaded"
# 3) 会话审计挡位与策略一致(示例策略要求包含 ddl)
$PSQL -c "SHOW pgaudit.log" | grep -qi ddl \
|| echo "ALERT: pgaudit.log drifted"
# 4) 今日 AUDIT 行数大于 0(preload 丢失 / 参数被改 / 日志目录写满都会归零)
if [ "$(grep -c 'AUDIT:' "$PGLOG")" -eq 0 ]; then
echo "ALERT: zero AUDIT rows in $PGLOG"
fi把脚本输出接进监控(或直接把四项检查做成日志平台的探活查询),「审计还在工作」就从一句口头保证变成每天被验证的事实。四项中第 4 项最重要:它是唯一能发现「配置看起来都对、链路却断了」的端到端检查。
审计记录的检索示例
AUDIT 行是固定前缀的文本,用简单的正则就能做初筛。常见检索(stderr 格式,其他格式按字段调整):
# 全部 DDL 类审计(九段中第 4 列为类别)
grep -E 'AUDIT: [A-Z]+,[0-9]+,[0-9]+,DDL,' postgresql-18-main.log
# 全部对象审计记录
grep 'AUDIT: OBJECT,' postgresql-18-main.log
# 特定对象的读写记录(第 6、7 段为对象类型与对象名)
grep 'AUDIT: OBJECT,[0-9]*,[0-9]*,[A-Z]*,[A-Z]*,[A-Z]*,public.payment,' postgresql-18-main.log
# 谁在执行角色与权限变更(ROLE 类别)
grep -E 'AUDIT: [A-Z]+,[0-9]+,[0-9]+,ROLE,' postgresql-18-main.log进入日志平台后,同样的意图用结构化查询表达(LogQL 提取管道示意,字段名随采集配置调整):
{job="pgaudit"} |= "AUDIT: OBJECT,"
| regexp `AUDIT: (?P<type>\w+),(?P<stmt>\d+),(?P<sub>\d+),(?P<class>\w+),(?P<command>[^,]*),(?P<objtype>[^,]*),(?P<objname>[^,]*),(?P<statement>.*),`
| class = "WRITE" and objname =~ "public.(payment|refund)"两个工程要点:其一,语句文本本身可能包含逗号与换行,纯文本正则要从右往左锚定字段,或者直接在 csvlog / jsonlog 格式下用结构化字段规避文本解析——这也是推荐 PG15+ 用 jsonlog 的原因之一;其二,检索规则要纳入版本升级的回归清单,AUDIT 格式的字段增补会让旧正则静默漏配。
注意事项
shared_preload_libraries 漏配会让审计静默失效。 最常见的翻车:扩展创建过、GUC 配了,但换机器/换镜像时 preload 丢了。结果是没有 AUDIT 日志、也没有报错。把它纳入实例基线检查,并用「AUDIT 日志量 > 0」做存活监控。
pgaudit.log 与 log_statement 不应同时开启。 pgaudit 会校验并锁定 log_statement 等语句日志参数为其要求的默认值;迁移到 pgaudit 后,记得把旧的 log_statement 挡位归零,避免配置漂移带来的困惑。
DDL 类别不包含角色与权限语句。 需要 GRANT / REVOKE / 角色变更留痕时,类别清单里要显式加上 role。只写 pgaudit.log = 'ddl' 的审计在权限变更上是盲的。
对象审计只覆盖 READ / WRITE。 想审计某张表的 DDL 或 TRUNCATE 之外的维护操作,要靠会话审计类别;TRUNCATE 本身属于 WRITE 类,对象审计可以覆盖。
pgaudit.log_parameter = on 会把绑定参数值写入审计日志。 敏感字段(手机号、证件号、口令)会因此进入日志副本。敏感库要么保持 off,要么用 pgaudit.log_parameter_max_size 截断,并把审计日志的访问控制对齐「含敏感数据的副本」管理。
pgaudit.log_relation = on 的日志放大要认真评估。 一条十表 JOIN 会产生十条 AUDIT 记录;配合 pgaudit.log_statement_once = on 可以只让首条带语句文本,减少重复。
AUDIT 消息默认只进服务端日志。 不要为了调试常开 pgaudit.log_client = on——把审计明细回显给客户端会扩大暴露面,也改变错误处理路径的行为。
版本分支必须与实例大版本一致。 PG18 实例装 PG17 分支的 pgaudit 无法编译或工作异常;跨版本升级清单里要有「更换对应分支的 pgaudit 包 + 重建扩展」这一项。
MISC / MISC_SET 是噪音大户。 除非在排查 SET ROLE 之类的会话环境篡改,日常审计清单里不要包含它们;misc_set 尤其容易被探活脚本刷屏。
pgaudit 记录行为,不记录数据内容。 AUDIT 记录的是语句、参数与对象,SELECT 返回了哪些行数据并不会出现在日志里。回答「导出了多少数据」要靠 pgaudit.log_rows 的行数字段与 COPY 类记录,不要指望从审计日志还原数据本身。
审计不等于阻断。 pgaudit 在语句执行链路上只做记录,不改变执行结果;想把「未授权的访问」挡在门外,要靠权限体系、RLS 与网络层,而不是把希望寄托在审计日志上。
pgaudit 的版本升级要关注格式与 GUC 的变化。 大版本分支之间 AUDIT 格式与参数集合可能增补,升级内核或扩展后要回归验证解析规则——采集端字段错位是升级后最常见的问题。
预加载的回滚同样要重启。 迁移演练时把「卸载 pgaudit」也当成一次重启操作来演练:应急回滚预案里只写「改配置」不写「要重启窗口」,事发时会卡住。
对象与 schema 字段是跨库归因的唯一依据。 AUDIT 记录的对象名在跨库、同名表场景下必须结合数据库名字段解读;检索规则不要只匹配表名,否则同名对象的记录会混在一起。
对象审计的 GRANT 声明要跟表生命周期走。 新建表不会自动进入对象审计范围,GRANT SELECT ... TO auditor 要写进建表模板或变更流程;审计角色的授权清单定期与表清单对账,防止新表脱管。
pgaudit.log_nested 打开后注意日志放大。 函数内部的循环 SQL 会逐条落审计,存储过程密集的实例上日志量可能放大一个量级;仅在深挖函数行为时临时开启,用完收回。
用计数对账验证类别覆盖。 预发环境执行一组已知语句后,比对「实际执行条数」与「AUDIT 行数」是否一致——类别漏配(比如忘了 write)在数字对不上时立刻暴露。
最佳实践
- 用「DDL 全审计 + 敏感表对象审计 + 运维角色全审计」的三层组合起步。 它覆盖了 90% 的合规条款,日志量却与业务流量基本解耦。
- 主审计角色用
NOLOGIN声明角色,并按业务域拆分二级角色。 审计范围的增减变成纯 GRANT/REVOKE 操作,可走标准变更流程,不需要改配置。 pgaudit.log_catalog = off写进基线。 不关它,监控探针和 psql 元命令会让审计日志失去可读性。- PG15+ 优先
jsonlog、老版本用csvlog,让 AUDIT 行天然携带结构化的用户/库/来源字段;采集端按九段格式入库,把审计变成可查询的数据。 - 参数记录分级处理: 分析型环境开
pgaudit.log_parameter,生产敏感库保持off或配合pgaudit.log_parameter_max_size截断;两类环境的审计日志访问权限不同。 - 把 AUDIT 日志量纳入监控。 「每分钟 AUDIT 行数」骤降为零通常意味着预加载丢失或配置被改,比等审计检查时发现要早得多。
- 按角色/数据库分域下放
pgaudit.log。 全局保持none或最低集合,ALTER DATABASE/ALTER ROLE精确放大;这继承了语句日志一页「日志量与审计价值正相关」的原则。 - 升级实例时把 pgaudit 分支切换列入迁移清单。 与内核同版本安装、重启后重建扩展、验证 AUDIT 恢复,三步缺一不可。
- 对象审计的授权变更走变更流程。 GRANT/REVOKE 给 auditor 的每一条变更都要有审批与记录——审计范围本身也是被审计对象。
- 定期抽样回放验证。 每月在预发环境执行一组已知语句(DDL、GRANT、对审计表的读写),核对 AUDIT 记录的类别与对象字段,确认解析规则没有随版本升级而失配。
- 不要用 pgaudit 替代连接审计。 pgaudit 记语句、不记连接生命周期;
log_connections/log_disconnections仍需保持开启,两者合起来才是完整的证据链。
下一节
继续学习:审计策略设计
参考链接
- pgaudit:PGDG 官方审计扩展(GitHub 仓库)
- pgaudit README(安装、设置与示例)
- PostgreSQL 官方文档:Error Reporting and Logging(AUDIT 记录依赖的日志机制)
- PostgreSQL 官方文档:log_destination 与输出格式(stderr / csvlog / jsonlog)
- PostgreSQL 官方文档:File-Based Log Configuration(csvlog / jsonlog 字段说明)
- PostgreSQL 官方文档:Predefined Roles(审计角色的最小权限参考)
- PostgreSQL 官方文档:CREATE ROLE
- PostgreSQL 官方文档:GRANT
- PGDG 软件包仓库(apt.postgresql.org)