Skip to content

审计策略设计 ​

前面三页解决了「怎么记」:语句日志记行为、连接日志记边界、pgaudit 记结构化审计事件。但工具齐了不等于审计就位——真正决定审计是否有效的是策略:审什么、何时审、保留多久、谁能看。这四个问题任何一个没想清楚,都会让审计沦为「日志在悄悄长大,没有人看,出了事发现什么都没记到」的摆设。

审计策略的本质是在三个约束之间求平衡:合规要求(必须记什么)、安全价值(最值得记什么)、运行成本(记得起什么、查得动什么)。好的策略不是「能记的都记」,而是让每一类被记录的事件都有明确的用途——要么对应某条合规条款,要么对应某个具体的威胁场景(如批量导出、越权变更)。

本页先建立审计策略的要素框架,再讨论常见合规标准的要求轮廓、分级审计模型、审计账号与最小权限、日志的防篡改存储、告警规则设计、与 RLS / 脱敏的配合、性能与采样,最后给出一套中小企业的完整落地示例。读完后你应该能为自己负责的实例写出一份可执行、可验证的审计策略文档。

前置知识

建议先阅读:pgaudit 扩展

基础概念 ​

一份数据库审计策略至少要回答四个问题。把答案写成文档、随架构一起评审,是审计从「配置」升级为「制度」的第一步:

要素要回答的问题典型答案
审计对象审什么(对象 / 操作 / 人)全量 DDL;核心业务表读写;管理角色全操作
触发范围何时审(环境 / 时段 / 挡位)生产全时段;预发只审 DDL;排障窗口临时放大
保留策略保留多久、存哪里本地 30 天滚动,集中平台 180 天,压缩归档 1 年
访问控制谁能看、谁能改审计平台只读权限给安全组;数据库侧日志文件仅 DBA 组可读

合规驱动的审计要求轮廓 ​

不同行业的合规框架对「数据库审计」的措辞不同,但收敛出的要求高度相似。以下按轮廓概述(具体条款以对应标准的正式文本为准):

  • 等级保护(等保 2.0):对重要用户行为和安全事件要有审计,审计记录要受保护、定期备份、留存足够时长;审计进程不可被非授权中断。
  • PCI-DSS(支付卡行业):持卡人数据环境(CDE)的所有访问必须可追踪到个体;日志要集中收集、防篡改、覆盖足够保留期,并有定期复查机制。
  • GDPR(通用数据保护条例):强调问责制与最小化——对个人数据的处理活动要有记录,访问要能证明「最小必要」,泄露时要能还原事件过程。
  • SOX(萨班斯法案):面向财务报告内控,要求对影响财务数据的应用与数据库变更、访问有留痕与审批链。

这些要求落到 PostgreSQL 上,就是本阶段前几页的配置组合:连接日志(可追踪个体)+ 语句/对象审计(可还原行为)+ 集中存储与访问控制(防篡改)+ 告警与复查(让日志有人看)。合规审计师检查的从来不是某个参数,而是「策略 → 配置 → 证据 → 复查」这条链是否闭环。

分级审计模型 ​

按「影响面 × 频率」给操作分级,是控制日志量的核心方法:

级别覆盖内容频率特征实现手段
L1 全量全部 DDL、全部角色/权限变更极低频、高影响pgaudit.log = 'ddl, role'
L2 聚焦核心敏感表的读/写中低频、高价值pgaudit 对象审计(pgaudit.role + GRANT)
L3 角色管理与运维角色的全部语句低频、可追溯个体ALTER ROLE ops_admin SET pgaudit.log = 'all'
L4 抽样业务角色的 DML / 慢查询高频、按需log_min_duration_statement、会话审计按角色下放

分级的原则:级别越高的事件,记录要求越刚性;频率越高的流量,记录方式越灵活。 L1/L2 是底线,任何环境不可关闭;L3 跟随账号生命周期;L4 按需调节。

一份审计策略文档的骨架 ​

策略要「可执行、可审计」,文档本身就要有确定的骨架。推荐的最小结构:

text
1. 目的与范围
   - 覆盖哪些实例 / 环境 / 数据域;不覆盖什么(及为什么)
2. 角色与职责
   - 策略 owner、配置执行者、日志平台管理者、告警值班、演练负责人
3. 审计矩阵
   - 对象 × 操作 × 主体 的分级清单(L1~L4),每格标注实现手段
4. 配置基线
   - 与矩阵对应的 postgresql.conf / 角色 / GRANT 声明(版本化存放)
5. 保留与访问控制
   - 本地 / 在线 / 归档三层保留期;各层访问权限与审批流程
6. 告警规则
   - 场景、阈值、级别、处置动作、值班归属
7. 验证与演练
   - 季度演练剧本、覆盖度核对 SQL、问题清单闭环记录
8. 变更历史
   - 版本号、日期、变更内容、评审人

骨架里最容易省略、也最不该省的是第 2 项与第 8 项:没有明确 owner 的策略会在第一次人员变动后失修;没有变更历史的策略无法向审计师证明「这条配置是何时、经谁批准生效的」。

语法与用法 ​

审计账号与最小权限配置 ​

审计功能自身也要遵循最小权限:查看审计证据的角色不需要、也不应该拥有修改数据的能力。

sql
-- 1. 主审计角色(对象审计挂载点,不可登录)
CREATE ROLE auditor NOLOGIN;

-- 2. 审计查阅角色:能读监控视图与统计,不能写
CREATE ROLE audit_viewer NOLOGIN;
GRANT pg_monitor TO audit_viewer;        -- 内置预置角色:pg_stat_* 等监控视图读取权
GRANT pg_read_all_data TO audit_viewer;  -- 可读全部表数据(按需授予,配合日志取证)

-- 3. 对象审计范围声明
SET pgaudit.role = 'auditor';
GRANT SELECT, INSERT, UPDATE, DELETE ON public.payment TO auditor;
GRANT SELECT ON public.customer TO auditor;

-- 4. 查阅权限下放(例如给安全团队)
GRANT audit_viewer TO sec_oncall;

关于预置角色与角色创建的细节,参见角色管理章节的创建角色一页。要点是:pg_monitor 覆盖了 pg_stat_activity、pg_stat_database 等监控视图的读取权,pg_read_all_data 则要谨慎——它等于全库数据的「只读通行证」,只授予确实需要做日志取证的岗位。

分级审计的落地配置 ​

conf
# postgresql.conf —— 分级审计的基线(配合前几页的连接日志)
pgaudit.log = 'ddl, role'          # L1:全量 DDL + 权限变更
pgaudit.role = 'auditor'           # L2:对象审计声明
pgaudit.log_catalog = off          # 压掉 catalog 访问噪音
log_connections = on               # 连接边界
log_disconnections = on
log_min_duration_statement = 1s    # L4:慢查询抽样
log_parameter_max_length = 128     # PG13+:限制参数值暴露
log_destination = 'jsonlog'        # 结构化输出,便于集中采集
sql
-- L3:管理角色全审计
ALTER ROLE ops_admin SET pgaudit.log = 'all';

-- L4:高频只读角色只靠慢查询抽样
ALTER ROLE bi_reader SET log_min_duration_statement = '2s';

告警规则的载体 ​

告警规则通常落在日志采集端(ELK 的 watcher、Loki + Alertmanager、云厂商的日志服务),不在数据库内部。规则的本质是「对 AUDIT 行与连接行做模式匹配」:

text
# 采集端规则示例(伪代码)
rule high_risk_ddl:
  match message contains "AUDIT:" and (contains "DROP TABLE" or contains "TRUNCATE")
  → 通知 DBA 群,级别 P2

rule grant_to_public:
  match message contains "AUDIT:" and contains "GRANT" and contains "PUBLIC"
  → 通知安全组,级别 P1

rule brute_force:
  match count("password authentication failed") by remote_host > 10 within 1m
  → 自动封禁来源 IP,级别 P2

rule offhours_admin_login:
  match message contains "connection authorized" and user in ["postgres","dba_ops"]
    and time not in ["09:00","20:00"]
  → 通知安全组,级别 P2

rule bulk_export:
  match message contains "AUDIT:" and contains "COPY" and object_name in sensitive_tables
  → 通知安全组,级别 P1

详细说明 ​

审什么:对象、操作与人 ​

「审什么」要从三个维度同时圈定,缺一个都会出现盲区:

  • 对象维度:哪些库、哪些 schema、哪些表是「敏感资产」。典型清单:支付/交易流水、用户主档(含 PII)、权限与账号表、配置与字典表。对象清单应与数据分级制度对齐,并有 owner。
  • 操作维度:读(SELECT/COPY 导出)、写(DML/TRUNCATE)、结构(DDL)、权限(ROLE)。高频操作里,「读」最容易被忽视——数据泄露的主通道恰恰是查询与导出,而不是写入。
  • 主体维度:谁在操作。人类运维账号、应用账号、ETL 账号、审计自身账号,各有不同基线。应用账号的行为用抽样,人类账号的行为要全量——「人」才是审计的主要对象,程序行为靠变更流程管理。

三个维度交叉形成审计矩阵(例如「payment 表 × 写操作 × 任何主体」= 必须对象审计全量),这张矩阵就是策略文档的核心插图。

分级审计:全量 DDL + 关键表 DML + 管理操作 ​

把「基础概念」中的模型展开成实施顺序:

  1. 第一步:L1 全量 DDL + ROLE。 几乎零成本(DDL 低频),却覆盖了「结构被谁改了」与「权限给了谁」两个最高危的问题。这是所有 PostgreSQL 实例的默认底线,无论规模大小。
  2. 第二步:L2 对象审计。 与数据分级清单对齐,把 PII / 资金相关的表逐张 GRANT 给 auditor。注意「读」也要审:SELECT 与 COPY 是泄露主通道。
  3. 第三步:L3 管理角色。 运维账号数量少、行为应可完全解释,全量审计成本可忽略。同时用连接日志约束其来源(只允许从跳板机连入)。
  4. 第四步:L4 按需。 业务账号的 DML 不做全量语句日志,用慢查询抽样 + 对象审计覆盖高危对象即可;确有事件需要回溯时,临时放大该角色的挡位。

审计账号与最小权限 ​

审计体系自身的账号 hygiene 直接决定审计证据的可信度:

  • 审计角色最小化:auditor 只做对象审计挂载(NOLOGIN),audit_viewer 只读监控视图与数据,不授予任何写权限。
  • 审计功能不可被业务角色触碰:pgaudit.* 参数是超级用户级;把普通业务角色限制在应用所需的最小权限上(参考 RLS 与权限体系),防止「应用账号自己关审计」这类荒诞但真实的风险。
  • 职责分离:能改数据的人不能清理审计日志;能管理日志平台的人不应拥有数据库账号。集中收集后,数据库本机的日志文件删除权限与审计平台的管理权限分属不同团队。
  • 审计自身的操作也留痕:对 auditor 的每一次 GRANT/REVOKE 本身就是 L1 的 ROLE 类事件——审计范围的变化是可追溯的。

审计日志的存储与不可篡改 ​

审计日志的生命周期管理要点:

  • 集中收集:本地文件只是中转。日志应实时(或准实时)汇聚到独立的日志平台,即使数据库主机被完全控制,攻击者也无法抹掉已外发的记录。
  • 完整性保护:日志平台侧开启防篡改能力(对象存储的 WORM / 对象锁、不可变存储桶、append-only 索引)。至少要做到:普通管理员无法静默删除或改写历史日志,删除操作本身有记录。
  • 异地容灾:审计平台与数据库生产环境尽量物理/逻辑隔离(不同账号体系、不同网络),并定期把归档复制到第二存储位置。
  • 保留期:由合规要求倒推(等保、PCI-DSS、GDPR 的常见要求在 90 天到 1 年量级,以正式文本为准),在线保留 + 冷归档分层,超期销毁要有销毁记录。
  • 传输链路加密:采集器到平台的链路用 TLS;日志里含 IP、账号、参数值,按「敏感数据副本」对待。

保留期的磁盘预算核算 ​

保留期是合规底线,磁盘是物理约束,两者要对账。核算方法(示意):

text
在线体积 ≈ 日增体积 × 在线保留天数 × 冗余系数(1.3~1.5)
日增体积 ≈ 日均 AUDIT 行数 × 平均行宽 + 日均连接日志行数 × 平均行宽

示例:对象审计 2000 万行/天 × 300 B ≈ 5.7 GB/天
     连接日志    50 万行/天 × 200 B ≈ 0.1 GB/天
     在线 90 天 × 1.3 冗余           ≈ 765 GB
     归档 365 天(压缩后约 1/5)     ≈ 480 GB

三个控制手段:压缩归档——日志文本的压缩比通常在 5:1 以上,冷数据进对象存储最省钱;分级保留——AUDIT 与连接日志按合规要求保留,调试类日志 7~14 天滚动即可;源头削减——挡位调优让日增先小下来,比事后压缩更有效。

磁盘告警阈值应该按「保留期承诺还剩多少天」计算,而不是只看物理容量百分比——先触顶的往往是保留期承诺,不是磁盘本身。预算随策略变更重算,并把结果写回策略文档第 5 节。

审计告警规则设计 ​

日志的价值在「有人看」,告警是把「看」制度化的手段。规则设计从威胁场景反推:

场景信号级别建议
数据被结构性破坏DROP TABLE / TRUNCATE / DROP SCHEMA 的 AUDIT 记录P2(工作时间)→ P1(夜间)
权限被放大GRANT 到 PUBLIC、GRANT 超级权限类语句P1
批量数据导出敏感表上的 COPY / 大结果集查询、pg_dump 客户端连接P1
凭证被爆破同一来源 IP 认证失败频率超阈值P2 + 自动封禁
越轨访问运维账号在非工作时段登录、来自陌生网段的连接P2
审计失效AUDIT 日志量骤降为零、采集链路断流P1(审计失效本身就是事件)

三条工程经验:告警阈值先宽松后收紧(先收集两周基线再定阈值,否则告警风暴会让人关掉告警);每条规则要有明确的处置动作与值班归属;「审计失效」类规则优先级最高——审计被关掉比事件本身更值得恐慌。

与 RLS / 脱敏的配合 ​

审计回答「谁动过数据」,RLS 与脱敏回答「谁配看到数据」,三者构成纵深:

  • RLS 缩小必须审计的面:行级安全把租户数据隔离在策略之后(见RLS 应用场景),应用账号越权访问的窗口变小,审计重点可以聚焦到持有 BYPASSRLS 能力的管理角色。
  • 脱敏降低审计日志的敏感度:动态脱敏(视图/扩展实现)让普通查询会话看到的本来就是假数据,这类会话的审计记录即使包含参数值,泄漏的也是脱敏后的值——审计与脱敏组合后,pgaudit.log_parameter 才敢在生产开启。
  • 反向依赖:脱敏策略的变更(规则表、视图定义)本身是 L1 审计对象;脱敏视图的 security_barrier 与属主设置由管理角色执行,其操作走 L3 全量审计。

审计对性能的影响与采样 ​

审计开销的控制手段在前几页已分别出现,策略层面归结为一句话:用分级和采样,把「全量记录」留给低频高危操作,把「抽样记录」留给高频低危流量。

  • L1/L2/L3 是刚性记录,频率天然低,无需采样;
  • L4 的高频流量用三类阀门采样:时长阈值(log_min_duration_statement)、角色挡位(按角色分域)、采集端聚合(相同骨架的语句折叠计数);
  • 对象审计中的大结果集 SELECT 可通过 pgaudit.log_rows = on 拿到行数字段,再在采集端按行数阈值升级告警,避免在数据库侧做全量 READ 记录。
  • 每次调整挡位前后,用 pg_stat_statements 与日志量对比实测,把结论写回策略文档——审计策略是活文档,不是一次性配置。

多环境的策略差异化 ​

生产、预发、开发三类环境的审计强度应当递减,但底线一致:

维度生产预发开发
L1(DDL + ROLE)全量全量全量
L2(对象审计)按分级清单按分级清单可省(结构同生产则保留)
L3(管理角色全审计)全量全量全量
连接审计开开开
慢查询抽样1s 挡位500ms 挡位300ms 挡位
参数记录off(敏感库)on(数据已脱敏)on
告警全量接入值班只接高危规则不接告警

两条一致性原则:L1 与连接审计在任何环境都不可关闭——预发与开发是演练与验证的主场,审计失效演练、版本升级回归都在这里发生,环境本身没有审计反而是最大的盲区;预发用脱敏数据时才允许 pgaudit.log_parameter = on——参数记录的敏感性跟随数据敏感性,而不是环境名称。

与账号生命周期的联动 ​

L3(管理角色全审计)跟着账号走,策略要规定账号生命周期各环节的审计动作:

生命周期环节审计动作责任人
账号创建同步配置 ALTER ROLE ... SET pgaudit.log;登记进策略矩阵DBA + 审批人
权限变更变更本身就是 L1 的 ROLE 类记录;复核挡位是否需要同步放大DBA
账号停用清理该角色的审计配置;从告警白名单移除DBA + 安全
账号删除确认矩阵中该账号行已归档(历史事件归因的依据)安全

停用环节常被遗漏的坑:账号删了,但 rolconfig 残留、日志平台白名单残留,会让「这个账号已不存在」的事实与「它的配置还在生效」的证据打架。用一条查询定期找「带审计配置但不可登录」的孤儿角色(注意:作为组角色、成员经 SET ROLE 切换使用且有意配置审计的场景属正常,需人工甄别):

sql
-- 带 pgaudit / 慢查询类审计配置、却不可登录的角色(多为停用或配置残留)
SELECT rolname, rolconfig
FROM pg_roles
WHERE NOT rolcanlogin
  AND rolconfig IS NOT NULL
  AND (rolconfig::text LIKE '%pgaudit%' OR rolconfig::text LIKE '%log_min_duration%')
ORDER BY rolname;

把这条查询加进季度演练的覆盖度核对,输出随策略文档归档。

演练与验证:审计覆盖度检查 ​

策略必须可验证。季度演练的标准动作:

  1. 覆盖度核对:对照策略矩阵,逐格确认「这一格的记录手段已配置且生效」。对象维度用 SQL 核对:
sql
-- 核对对象审计授权与策略清单是否一致
SELECT table_schema, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'auditor'
ORDER BY table_name, privilege_type;
  1. 端到端回放:在预发环境用演练账号执行一组剧本语句(DDL、GRANT、敏感表读写、失败登录、COPY 导出),核对每一件事在日志平台可检索、字段完整、告警触发。
  2. 取证演练:随机抽一条历史 AUDIT 记录,走一遍「查到主体 → 会话 → 语句 → 对象」的证据链,验证保留期内的日志真的取得到。
  3. 失效演练:故意在某测试实例移除 preload、断开采集器,验证「审计失效」告警是否触发——没触发的监控项等于没配。
  4. 文档回写:演练发现的所有差异记入问题清单,修复后复核。策略文档更新版本号与日期。

取证的标准作业程序(SOP) ​

事件发生时的取证动作要有预案,临时发挥最容易破坏证据。最小 SOP:

  1. 锁定时间窗与范围:先明确「哪个库、哪些对象、大致时间窗」,避免在全量日志里漫游。
  2. 只读检索:取证全程只在日志平台 / 审计库做只读查询;确需在数据库本机操作时,用只读命令并双人复核。
  3. 固定证据:对关键日志段做带时间戳的导出,计算校验和并登记(文件名、SHA-256、导出人、时间)。对象存储的版本控制 / WORM 能力是最好的「不可篡改」证明。
  4. 串联证据链:按「AUDIT 行 → 会话行(connection authorized)→ 账号与来源 → 应用层日志」的顺序拼接,每一步记录所用的检索语句——检索过程本身也是证据的一部分。
  5. 结论与闭环:事件报告写明「结论、证据编号、不确定项」,整改动作进问题清单;保留期内的原始日志不因结案而提前销毁。

第 4 步的关联键要提前设计好:数据库侧的 %c 会话 ID、PID、时间戳,与应用侧 request id 之间的映射方式,写在策略文档里,而不是事件当晚现场发明。

与变更管理流程的集成 ​

审计配置的每一次变更(挡位、角色、GRANT、保留期)都应该走与 schema 变更同级的变更流程——审计配置的错误与数据错误同样危险,「忘了开」与「开错了」都会造成不可逆的证据缺口:

变更类型审批等级生效方式回滚预案
挡位调整(pgaudit.log 等)例行变更reload上一版配置文件
审计角色 / GRANT 变更例行变更(变更本身是 L1 事件,双向留痕)即时反向 GRANT 语句
预加载 / 扩展变更重大变更(需重启窗口)重启恢复 preload + 重建扩展
保留期 / 归档策略重大变更(合规影响)平台侧恢复原保留策略
告警规则例行变更平台侧关闭新增规则

流程上的两个要点:变更单必须写明验证方法——配置生效后用什么查询或命令确认审计仍在记录;变更实施人与验证人分离,配置漂移的窗口期才有第二双眼睛盯着。

实战示例 ​

示例一:一套中小企业的审计策略落地 ​

背景:30 人技术团队,1 个主业务库(含支付与客户 PII),2 个边缘库,运维 3 人,已接入 Grafana Loki。目标:满足等保三级自查 + 客户安全问卷。

第一步:定义审计矩阵(策略文档摘要)

text
| 范围            | 对象               | 操作            | 手段                          |
|-----------------|--------------------|-----------------|-------------------------------|
| 全实例          | 所有库             | DDL + ROLE      | pgaudit.log = 'ddl, role'     |
| 敏感对象        | payment / customer | 读 + 写         | pgaudit 对象审计(auditor)   |
| 管理角色        | ops_admin / dba    | 全部语句        | ALTER ROLE ... 'all'          |
| 连接边界        | 所有库             | 建立 / 断开     | log_connections/disconnections|
| 业务流量        | 所有库             | 慢查询 + 错误   | log_min_duration_statement    |

第二步:配置基线

conf
# postgresql.conf
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'ddl, role'
pgaudit.role = 'auditor'
pgaudit.log_catalog = off
log_connections = on
log_disconnections = on
log_min_duration_statement = 1s
log_parameter_max_length = 128
log_line_prefix = '%m [%p] %q%u@%d '
logging_collector = on
log_destination = 'jsonlog'
log_rotation_age = 1d
log_truncate_on_rotation = on
sql
CREATE ROLE auditor NOLOGIN;
SET pgaudit.role = 'auditor';
GRANT SELECT, INSERT, UPDATE, DELETE ON public.payment TO auditor;
GRANT SELECT ON public.customer TO auditor;

ALTER ROLE ops_admin SET pgaudit.log = 'all';

第三步:Loki 告警规则(摘要)

text
1. DROP/TRUNCATE 语句        → 立即通知 DBA + 安全(P1)
2. GRANT 到 PUBLIC           → 立即通知安全(P1)
3. payment/customer 的 COPY  → 立即通知安全(P1)
4. 运维账号 20:00-09:00 登录 → 次日晨会前通知(P2)
5. 认证失败 > 10 次/分钟/来源 → 自动封禁 + 通知(P2)
6. AUDIT 日志量 = 0 持续 5m  → 立即通知 DBA(P1)

第四步:保留与访问控制

text
本地日志:保留 14 天(采集端确认入库后即可滚动)
Loki:在线 90 天,对象存储归档 365 天(WORM 桶)
访问:Loki 审计流只读权限 → 安全组;管理权限 → 双人授权

第五步:季度演练(按「演练与验证」五步走,问题清单闭环)

示例二:覆盖度核对脚本 ​

sql
-- 1. 对象审计授权是否覆盖了分级清单里的敏感表
SELECT table_schema || '.' || table_name AS audited_table,
       string_agg(privilege_type, ', ') AS privileges
FROM information_schema.role_table_grants
WHERE grantee = 'auditor'
GROUP BY 1
ORDER BY 1;
text
 audited_table     |             privileges
-------------------+--------------------------------------
 public.customer   | SELECT
 public.payment    | INSERT, SELECT, UPDATE, DELETE
sql
-- 2. 各角色的审计挡位现状
SELECT rolname, rolconfig
FROM pg_roles
WHERE rolconfig IS NOT NULL
  AND rolconfig::text LIKE '%pgaudit%'
ORDER BY rolname;
text
  rolname  |         rolconfig
-----------+----------------------------
 ops_admin | {pgaudit.log=all}
sql
-- 3. 全局挡位与连接审计是否在位
SELECT name, setting FROM pg_settings
WHERE name IN ('pgaudit.log','pgaudit.role','log_connections',
               'log_disconnections','log_destination',
               'log_min_duration_statement');

把三个查询的结果导出附在策略文档后面,就是本季度的覆盖度证据。

示例三:一次取证回放 ​

场景:周五下午客户投诉「我的订单被取消了」,需要还原 orders 表上该行的变更历史。

bash
# 1. 在日志平台按对象 + 时间窗检索 AUDIT 记录(示意)
#    query: message=~"AUDIT:.*WRITE.*public.orders" AND @timestamp >= 2026-10-02 AND @timestamp <= 2026-10-03
text
2026-10-03 14:22:08 CST [34110] user=app_write@orders_svc AUDIT: OBJECT,7,1,WRITE,UPDATE,TABLE,public.orders,
    UPDATE public.orders SET status = 'cancelled' WHERE id = 88231,<not logged>
bash
# 2. 用日志行的 PID 追溯连接来源
#    query: message=~"connection authorized" AND pid=34110
text
2026-10-03 14:20:51 CST [34110] LOG:  connection authorized: user=app_write
    database=orders_svc application_name=order-service host=10.0.3.17
bash
# 3. 结论:order-service 应用在 14:22 发起了取消操作,来源跳板机网段正常;
#    参数未记录(log_parameter=off),转应用层日志按 request id 追溯具体用户请求。

这个例子也说明策略的边界:数据库审计能定位到「哪个应用账号、哪条 SQL、什么时间」,个体用户的归因需要应用层审计配合——策略文档里要写明这条接力关系,避免取证时才发现链路断在最后一环。

示例四:特权账号最后活动报表 ​

合规自查常要求「每个特权账号最近一次活动」。前提是审计事实已由采集端入库(示例三中的 audit 表),一条 SQL 出报表(表结构示意,字段随采集端 schema 调整):

sql
-- audit_events:由日志平台回灌的 AUDIT 事实表(示意结构)
-- (event_time, db_user, dbname, client_addr, audit_type, class, command, object_name, statement_text)

-- 1. 各特权账号的最近活动一览
SELECT a.db_user,
       max(a.event_time) FILTER (WHERE a.class = 'DDL')         AS last_ddl,
       max(a.event_time) FILTER (WHERE a.audit_type = 'OBJECT') AS last_object_op,
       max(a.event_time) FILTER (WHERE a.command = 'SELECT')    AS last_select,
       count(*) FILTER (WHERE a.event_time >= now() - interval '7 days') AS events_7d
FROM audit_events a
WHERE a.db_user IN ('postgres', 'dba_ops', 'ops_admin')
GROUP BY a.db_user
ORDER BY a.db_user;
text
  db_user  |       last_ddl        |    last_object_op     |      last_select      | events_7d
-----------+-----------------------+-----------------------+-----------------------+-----------
 dba_ops   | 2026-10-06 11:20:31   | 2026-10-07 09:41:02   | 2026-10-07 09:41:02   |      1421
 ops_admin | 2026-09-30 16:05:12   | 2026-10-05 14:33:47   | 2026-10-05 14:33:47   |        87
 postgres  | 2026-10-01 02:10:09   | 2026-10-07 03:12:55   | 2026-10-07 03:12:55   |       310
sql
-- 2. 超过 30 天无活动的特权账号(潜在休眠账号,进入复核清单)
SELECT a.db_user, max(a.event_time) AS last_activity
FROM audit_events a
WHERE a.db_user IN ('postgres', 'dba_ops', 'ops_admin')
GROUP BY a.db_user
HAVING max(a.event_time) < now() - interval '30 days';

报表的价值不在 SQL 本身,而在它把「特权账号应该只做可解释的事」变成一张每月必须有人签字的表格。休眠账号清单交给账号生命周期流程(见「与账号生命周期的联动」小节)处理。

示例五:告警阈值的基线采集与调参 ​

告警规则上线的第一步不是定阈值,而是采基线。流程(均基于审计事实表,示意):

sql
-- 1. 按小时统计近两周各类事件量
SELECT date_trunc('hour', event_time) AS hr,
       class,
       count(*)                       AS cnt
FROM audit_events
WHERE event_time >= now() - interval '14 days'
  AND class IN ('DDL', 'ROLE')
GROUP BY 1, 2
ORDER BY 1, 2;
sql
-- 2. 认证失败按来源 IP 统计(连接日志事实表 conn_events,示意)
SELECT client_addr, count(*) AS failures, max(event_time) AS last_seen
FROM conn_events
WHERE message LIKE '%password authentication failed%'
  AND event_time >= now() - interval '14 days'
GROUP BY client_addr
HAVING count(*) > 20
ORDER BY failures DESC;

调参方法:

  1. 取两周分布按小时聚合,计算每类的 p99 值;
  2. 初始阈值 = p99 × 2——宁可漏报,不可误报淹没值班;
  3. 观察一周告警命中率:零命中且业务无异常 → 阈值下调 20% 试探;高频误报 → 先查根因(多是采集重复或白名单缺失),再收紧;
  4. 每季度用最新两周数据重算一次;业务量变化超过 ±30% 时立即重算。

阈值不是常数,是随流量漂移的函数——把它绑进季度演练流程,而不是等告警风暴之后才想起。

注意事项 ​

审计策略不是配置清单。 只写「开了哪些参数」的策略无法通过任何合规检查;必须有矩阵、保留期、访问控制、告警与演练记录。配置只是策略的执行结果。

不要试图全量记录一切。 全量语句日志在 OLTP 规模下很快会让日志平台、磁盘预算与团队精力同时崩溃,最终演变成「告警全关、日志没人看」——这比记少了更危险,因为它制造了虚假的安全感。

审计功能自身的可用性要优先保障。 pgaudit 预加载丢失、采集器断流、日志分区写满,任何一项都会让整条审计链静默失效。「审计失效告警」必须存在并且有人值班响应。

对象清单要跟随数据分级更新。 新业务上线加了敏感表却没进审计清单,是最常见的覆盖度缺口。把「敏感表登记」做成变更流程的必选项,而不是季度演练时补录。

审计日志自身含敏感信息。 IP、账号、参数值、语句文本都是敏感数据副本;审计平台的权限模型、传输加密、保留销毁要与生产数据同级管理,否则审计系统会成为最大的数据泄露源。

不要把审计当作访问控制的替代品。 审计是事后追溯手段,事前防线靠权限、RLS、脱敏与网络隔离。发现「靠审计兜住越权」的设计要退回去修权限模型。

跨实例复制策略时要重做实测。 边缘库与主库的流量特征不同,照搬主库挡位要么浪费要么不够;每个实例的挡位都来自自己的日志量实测。

保留期不满足合规要求是硬伤。 在线存储成本压力下最先被砍的往往是保留期,但 90 天起步的留存是多数框架的底线;用冷归档分层而不是缩短保留期来省钱。

策略文档要有版本号与生效日期。 没有「版本 1.3、2026-09-01 生效、评审人是谁」这类信息的审计策略,在合规检查中会被认定为未受控文档;配置基线入库版本管理,与文档版本号互相引用。

「没有告警」不等于「没有事件」。 告警只覆盖规则清单里的场景;季度演练中的覆盖度核对与端到端回放,才是验证「规则之外是否还有盲区」的手段,两者不能互相替代。

第三方与外包账号要单独列策略行。 供应商账号的权限、活跃窗口、审计挡位与内部账号不同,混在一张矩阵里容易被「平均化」处理;单列一行并附合同期与访问范围,续约与退场时才查得到依据。

审计矩阵要与数据分级的 owner 联名评审。 「分级变了审计没跟上」与「审计加了分级没记录」都源于两张清单各自演化;把「分级登记变更触发审计矩阵复核」写进流程,两张清单才有共同的变更节拍。

演练记录本身要可追溯。 演练的执行证据(核对 SQL 的输出、回放日志、问题清单)与策略文档一起归档——「我们每季度演练」这句话,最终要靠归档的记录来兑现。

策略文档要有一页「一句话版本」。 给业务方与新人看的摘要(记什么、留多久、谁能看、怎么告警)降低沟通成本,安全问卷与合规检查也能直接引用。

边缘实例的审计同样要有人认领。 边缘库、临时项目库最容易变成「没配审计、没人看日志」的飞地;策略里明确每个实例的 owner,把无 owner 实例列为清理对象。

最佳实践 ​

  1. 先写策略文档,再动配置。 四要素(对象 / 范围 / 保留 / 访问控制)逐项落字,随架构评审;配置变更必须回写文档。
  2. 用 L1→L4 分级模型控制成本。 全量 DDL + ROLE 是无条件底线;敏感表对象审计跟随数据分级清单;管理角色全审计;业务流量只抽样。
  3. 对象审计以「读」为重。 泄露主通道是查询与导出;SELECT / COPY / 大结果集的审计优先级高于 DML。
  4. 审计账号最小权限、职责分离。 auditor 用 NOLOGIN 声明角色;查阅与日志管理权限分属不同角色与团队;对 auditor 的 GRANT/REVOKE 本身被 L1 捕获。
  5. 日志集中收集 + 防篡改存储 + 异地归档。 本地文件只是中转;平台侧用 WORM / 不可变存储,保留期由合规倒推并分层(在线 + 冷归档)。
  6. 告警按威胁场景设计并设值班归属。 高危 DDL、权限放大、批量导出、爆破、越轨时段登录、审计失效六类规则起步;先观察基线再收紧阈值。
  7. 与 RLS / 脱敏形成纵深。 事前控制缩小审计面、降低审计日志的敏感度;脱敏与 RLS 的变更自身纳入 L1 审计。
  8. 每个实例的挡位由实测决定。 调整前后用 pg_stat_statements 与日志量对比,把结论写回策略文档;拒绝「抄别的库的配置」。
  9. 季度演练制度化。 覆盖度核对 SQL、端到端回放、取证演练、失效演练四件套,差异进问题清单闭环。
  10. 监控「AUDIT 日志量」本身。 它是审计链路的心跳指标:骤降为零说明预加载/配置/采集任一环断了,优先级高于一般性能告警。
  11. 保留期与访问控制写入安全基线并纳入巡检。 与磁盘告警、采集器存活、Loki 配额一起构成审计基础设施的日常检查项。
  12. 明确数据库审计与应用审计的接力边界。 数据库侧回答「哪个账号、哪条 SQL」,应用侧回答「哪个终端用户」;策略文档写清两侧的关联键(如 request id),取证时不掉链。

下一节

继续学习:透明数据加密(TDE)概述

参考链接 ​