筑牢安全防线!Oracle 26ai 统一审计与 SQL Firewall 实战详解

前阵子公司做了一次内部安全演练,第三方团队模拟攻击者拿到了一个应用的数据库账号,然后顺着 SQL 注入的漏洞往库里塞了条查询工资表的语句。那条语句被数据库拦下来了,但安全部门发现的时候还是冒了身冷汗——如果当时没有部署 SQL Firewall,后果是什么样谁都说不准。

后来复盘时我翻了一圈 Oracle 26ai 的安全能力清单,发现这两年在审计和防护上确实多了不少东西。这篇东西不念文档,只讲我亲手搭过的、踩过的、觉得值得记下来的东西。

一、审计这件事,26ai 不再给你第二种选择

先说审计。很多 DBA 对审计的第一印象是"开了太吃性能、不开又过不了合规"。Oracle 之前有两条腿走路:传统审计(Traditional Auditing)和统一审计(Unified Auditing)。传统审计靠 AUDIT 命令配置,记录落盘到 SYS.AUD$ 表,好处是简单,坏处是慢、灵活度低、多租户环境里支持得一塌糊涂。绝大部分老 DBA 都是"审计开了就会,查日志就烦"的状态。

26ai 把这条路堵死了: 传统审计在这个版本正式不再支持,统一审计成了唯一选择。简单说,如果再按老习惯写 AUDIT SELECT ON hr.employees BY ACCESS; 这种传统语法,到了 26ai 会直接报错——不是降级,是不认。如果你是从 19c 或 23c 迁过来的,升级前得先把传统审计策略全换成统一审计。

统一审计的好处是架构层面的事:审计记录写进 AUDSYS schema,性能比 AUD$ 好一个量级;策略用 CREATE AUDIT POLICY 语句声明式配置,语义清晰,不是那种改一个参数就全局跑偏的玩法;并且在 CDB/PDB 环境下,每层可以有自己的策略,不互相打架。我当时从传统审计迁移统一审计的时候,最直接的感受是——终于不用在应用高峰期猜"审计是不是又把系统表空间撑爆了"了。

二、列级审计:26ai 最让我眼前一亮的变化

统一审计本身不是新东西,23c 就有了。26ai 真正让我上心的是 列级对象审计

啥意思?以前你想审计"谁改了 SALARY 列",得把人整张表的 UPDATE 动作全记下来,然后自己翻 SQL_TEXT 字段去过滤,日志量大还不精确。26ai 直接在 CREATE AUDIT POLICYACTIONS 子句里支持指定列名:

CREATE AUDIT POLICY salary_audit
  ACTIONS UPDATE(SALARY);

这条策略的含义是:任何人(除了 SYS 级别用户)更新了 SALARY 列,不管在哪个表、哪个 schema,都会生成审计记录。如果你只想针对某一张表做,可以加上 ON 限定:

CREATE AUDIT POLICY hr_salary_audit
  ACTIONS UPDATE(SALARY)  ON hr.employees;

我们把这条策略应用在生产库的薪资表上之后,每笔薪资变更都有案可查,审计日志的量比以前"整表全记"少了大概七成。HR 系统的合规检查原本要靠业务日志来回比对,现在直接跑 UNIFIED_AUDIT_TRAIL 视图就能交差,省了不少事。

启用策略就是一条 AUDIT 命令:

AUDIT POLICY hr_salary_audit;

查记录也简单:

SELECT DBUSERNAME, ACTION_NAME, OBJECT_NAME, SQL_TEXT, EVENT_TIMESTAMPFROM UNIFIED_AUDIT_TRAILWHERE OBJECT_SCHEMA = 'HR'
  AND ACTION_NAME = 'UPDATE'
  AND EVENT_TIMESTAMP > SYSDATE - 7ORDER BY EVENT_TIMESTAMP DESC;

唯一需要注意的:你可以把策略设成 WHENEVER SUCCESSFULWHENEVER NOT SUCCESSFUL,分别管"成功操作"和"失败操作"——实际操作中,失败的 UPDATE 往往比成功的更值得盯。

三、SQL Firewall:不是"加个模块",是"长在数据库里"

聊完"记",再聊"防"。这是 26ai 在安全上最重的更新—— SQL Firewall 直接内置进数据库内核了

"内置进内核"这四个字不是Oracle 的营销用语,是实际架构决定的。传统数据库防火墙大多是中间件层或网络层的独立设备,SQL 到了数据库门口才被检测,如果配置不当或者设备故障,SQL 已经进去了。SQL Firewall 是 Oracle 数据库执行引擎的一部分,每一条 SQL、每一个存储过程调用、每一个 PL/SQL 块,在执行之前都会被它过一遍。它不能被绕过,因为它就在"必由之路"上。

它的原理说穿了不复杂:白名单。你先让一个数据库账号在正常业务下跑一段时间,SQL Firewall 把它的 SQL 活动全记下来;然后你"拍个快照"生成白名单;之后所有不在白名单里的 SQL,你让它记下来还是直接拦掉,你说了算。

整个流程走一遍比看文档清楚得多。第一步是启用这个功能:

EXEC DBMS_SQL_FIREWALL.ENABLE;

光这一步不会黑掉什么东西,它只是把 Firewall 引擎打开。然后选一个你要保护的用户——我们拿应用账号 APP 举例——开始捕获它的正常 SQL:

BEGIN
  DBMS_SQL_FIREWALL.CREATE_CAPTURE (
    username       => 'APP',
    top_level_only => TRUE,
    start_capture  => TRUE
  );END;/

top_level_only => TRUE 的意思是只捕获应用发过来的顶层 SQL,不深入追踪存储过程内部的每个语句,否则日志量会大很多。开始捕获之后,让 APP 用户跑一遍正常的业务流程——登录、查数据、跑报表、甚至跑定时任务。跑多长时间取决于你业务覆盖面的复杂度,我们当时测了三天,覆盖了周一到周三的完整业务周期。

确认覆盖充分了,停止捕获:

EXEC DBMS_SQL_FIREWALL.STOP_CAPTURE ('APP');

然后基于这些捕获日志生成白名单:

EXEC DBMS_SQL_FIREWALL.GENERATE_ALLOW_LIST ('APP');

这一步生成的其实是个"准入清单":哪些 SQL 文本是允许的、从哪里连接过来的。到这一步,白名单已经在库里了,但还没生效,你还可以再加一层控制: 上下文白名单

上下文白名单:不让 SQL 从奇怪的地址进来

光控 SQL 文本是不够的。如果攻击者拿到了 APP 的密码,然后用跳板机从境外 IP 连进来,发一条正常的 SELECT,文本在白名单里,它照样放行。所以 SQL Firewall 允许你同时限制这个用户的"访问来源":IP 地址、操作系统用户、程序名。

BEGIN
  DBMS_SQL_FIREWALL.ADD_ALLOWED_CONTEXT (
    username     => 'APP',
    context_type => DBMS_SQL_FIREWALL.IP_ADDRESS,    value        => '192.168.1.0/24'
  );
  DBMS_SQL_FIREWALL.ADD_ALLOWED_CONTEXT (
    username     => 'APP',
    context_type => DBMS_SQL_FIREWALL.OS_PROGRAM,    value        => 'myapp'
  );END;/

这样一来, APP 用户只有从 192.168.1.x 网段、通过名为 myapp 的程序连进来的会话,才会被允许执行白名单内的 SQL。别的来源一律"违规"。注意,上下文只在会话建立时检查一次,不是每次 SQL 都查——你不用担心每条 SELECT 都多一次 IP 匹配的开销。

允许、记录、还是阻断

上下文和 SQL 文本都配好之后,最后一步是"开闸"。你有两种力度的选择:

BEGIN
  DBMS_SQL_FIREWALL.ENABLE_ALLOW_LIST (
    username => 'APP',
    enforce  => DBMS_SQL_FIREWALL.ENFORCE_SQL,
    block    => FALSE
  );END;/

block => FALSE 的意思是:违规 SQL 放行,但记下来。这叫"观察模式",适合刚上线的时候用,先看看有没有误伤。 enforce 参数有三种取值: ENFORCE_CONTEXT(只拦上下文)、 ENFORCE_SQL(只拦 SQL)、 ENFORCE_ALL(两者都拦,默认)。我们刚开始用了两个星期的观察模式,每天都去查 DBA_SQL_FIREWALL_VIOLATIONS 确认违规记录里有没有正常业务,确认没了再切到 block => TRUE

BEGIN
  DBMS_SQL_FIREWALL.ENABLE_ALLOW_LIST (
    username => 'APP',
    enforce  => DBMS_SQL_FIREWALL.ENFORCE_ALL,
    block    => TRUE
  );END;/

阻断模式下不合规的 SQL 会直接抛 ORA-47605: SQL Firewall violation,应用那边该报错报错,该重试重试,你的数据库不再替恶意的查询买单。

四、SQL Firewall 上线后的日常活

白名单上了之后不是一劳永逸。应用发版、SQL 改写法、甚至查询优化器换了执行计划导致文本微变——都可能在白名单之外。所以日常有两件事要干。

第一,监控违规日志。主要看两个视图:

-- 违规记录SELECT USERNAME, VIOLATION_TYPE, SQL_TEXT, VIOLATION_TIMESTAMPFROM DBA_SQL_FIREWALL_VIOLATIONS;-- 捕获日志(如果还在训练期)SELECT USERNAME, SQL_TEXT, CAPTURE_TIMESTAMPFROM DBA_SQL_FIREWALL_CAPTURE_LOGS;

应用发版之后,头半天我基本会蹲在 DBA_SQL_FIREWALL_VIOLATIONS 前面,看有没有正常的 SQL 被误拦。发现了就 DBMS_SQL_FIREWALL.ADD_ALLOWED_SQL 加到白名单里,或者在捕获模式下重新过一遍。成熟的业务,发版后跑个几小时就能收工。

第二,定期清理日志。Firewall 的捕获日志和违规日志会随着时间涨,不是无限屯着就好。Oracle 给了 PURGE_LOG 来清理:

BEGIN
  DBMS_SQL_FIREWALL.PURGE_LOG (
    username    => 'APP',
    purge_time  => SYSTIMESTAMP - 90,
    log_type    => DBMS_SQL_FIREWALL.ALL_LOGS
  );END;/

我设了一个按月跑的定时任务,把 90 天前的违规日志清掉,不占空间也不影响查询速度。如果你的环境是在云上用的 Oracle Data Safe,它能自动每 7 天清一次,省了这一步。

多租户下怎么用

如果你的库是 CDB 架构,SQL Firewall 在根容器和各 PDB 里都可以独立配。根里配的策略管根自己,不往下透;各 PDB 自己建自己的白名单。一个很实用的场景是多租户环境下各租户的应用账户各自捕获、各自生成白名单,互不影响。我们 dev、uat、prod 各一个 PDB,dev 上用观察模式、prod 上阻断,配法一模一样,就改个 username 前缀。

五、Database Vault:防的不是外人,是管理员

SQL Firewall 防的是外部攻击和凭证滥用,但还有一种威胁它防不住—— 超级管理员自己。DBA 账户在传统模式下几乎无所不能,他可以绕过应用直接 SELECT * FROM hr.employees,而且审计日志照记不误——但问题是,DBA 自己就能删审计日志。

这不只是理论上的"坏人 DBA"问题。合规审计里有一种常见场景:第三方运维公司的人有生产库的 DBA 角色,他确实不会恶意做坏事,但万一他操作失误了呢?如果有数据泄露事件,你拿什么证明不是他干的?这时候你需要的是"想干但被阻止"——而不是"干了之后被记下来但已无法挽回"。

Oracle Database Vault 解决的就是这个。它给你三个东西:

  • 领域(Realm):把某几张表划成一个保护圈,即使是 DBA 用户在 realm 外也没权限访问里面的数据。只有 realm 授权的用户才能操作。我们当时把薪资表、客户身份信息表划进了一个 PROTECTED_DATA realm,所有非应用账号一律不准直接访问。
  • 命令规则(Command Rule):控制某类 SQL 能不能执行。我们设了一条:" DROP TABLE 只能从 DBA 专用跳板机执行,其他来源一律拒绝。"看似不起眼,但线上的 DROP TABLE 误操作,一年至少能拦一两起。
  • 多因子授权(Multi-Factor Authorization):让你给关键操作加一道审批。比如 ALTER SYSTEM 这种高危操作,不能一个人敲完就算,得再刷一次身份证或者走审批流。

Database Vault 和 SQL Firewall 的关系有点像"防内"和"防外"的分工。SQL Firewall 管的是"你这应用账户不能滥发 SQL",Vault 管的是"你 DBA 也不能乱碰不该碰的数据"。两个叠在一起,覆盖得就比较全了。

六、26ai 安全更新的边角料,也有几个值得提的

除了统一审计和 SQL Firewall,26ai 在安全层面还有一堆零散的更新,挑几个我印象深的说说。

传输层安全升到 TLS 1.3。 如果你还在用 TLS 1.2,甚至更老的 1.0/1.1,升级到 1.3 不光加密强度更高,握手次数从 2-RTT 降到了 1-RTT,加密链路的建连速度也有改善。不需要改应用代码,只需要把数据库和客户端的 TLS 配置调一下。

列加密和表空间加密算法换代。 列加密从 CBC 换到 GCM 模式,表空间密钥从 CFB 换到 XTS 模式。GCM 带认证加密,防篡改比纯 CBC 强。RMAN 完整性检查从 SHA1 换到 SHA512。这些东西你通常不需要主动去改,升级到 26ai 之后默认就是新的。

密码长度从 30 字节扩到 1024 字节。 别笑,之前 Oracle 数据库密码最长 30 个字符,在如今的安全策略面前简直不够用——你连个带特殊字符的复杂密码都可能超长。1024 字节之后,基本不怕密码复杂度被字长限死了。

Schema 级权限。 以前你想让一个用户能操作某个 schema 里的所有表,得一个个表授权,或者给一个宽泛的 ANY 系统权限。26ai 支持直接在 schema 粒度上授予权限。新角色 DB_DEVELOPER_ROLE 也很实用——它预配置了一组开发者需要的最小权限集合,不用自己拼。

MFA 支持 Cisco Duo 和 Oracle Mobile Authenticator。 用户名密码之外,再加一道推送通知或一次性密码。这个在高权限账户登录时很关键,我们给 DBA 角色开了 MFA,登录的时候手机弹个审批,不是本人操作的直接拒。

后量子加密。 这条是纯为未来准备的。26ai 支持了 NIST 刚标准化的 ML-KEM(密钥封装)和 ML-DSA(数字签名算法)——就算量子计算机哪天真的商用了,现在的加密链路也不会被一把解开。对大部分业务来说这步是用不到的,但它表明了 Oracle 对加密的长线态度。

七、三层防线怎么搭才不浪费

零零碎碎的东西说完了,最后聊一下搭配取舍。

我现在的安全基线是三件事叠在一起:

第一层是 SQL Firewall,放在最前面。管的是"这个应用账号能不能发这条 SQL、能不能从这个地址连进来"。它是最直接的攻击拦截面,SQL 注入、凭证滥用、异常连接路径,基本在这一层就能挡住。我上线新应用的标准流程是:先捕获一周正常负载、生成白名单、观察模式跑两周确认没有误伤、再切阻断模式。这套流程跑下来,目前没有出现过一次合理的业务被误拦的 case。

第二层是 统一审计,放在最后面。Firewall 挡不住的事——比如某个应用账号在允许的白名单内做了异常的查询量——靠审计来记。列级审计特别省钱,只记关键操作不记全表。审计不替代防火墙,防火墙也不替代审计,一个管"是不是违规",一个管"谁干了什么"。你不能因为有了白名单就不看日志,也不能因为记了日志就觉得安全了。

第三层是 Database Vault,放在中间偏"内"。防的是特权用户越权,跟 SQL Firewall 分工明确——Firewall 管应用用户,Vault 管 DBA 和运维用户。如果你们公司有外包运维,或者有严格的职责分离要求,这一层必须有。合规审计里很多"你能证明你的 DBA 没看过客户数据吗"的问题,靠 Vault 来证。

三层之间配合起来没什么冲突。SQL Firewall 和 Vault 可以同时启用,统一审计记两者的日志也都有对应的策略类型(有专门的 REALM VIOLATION 策略记 Vault 的事件,有专门的 Firewall 违规记入 UNIFIED_AUDIT_TRAIL 的配置)。不是说 A 上了 B 就得降级,它们各自管各自的维度,能共存。

最后讲个实际体验。那天演练结束后,安全部门问我们"你们那条 SQL 注入是谁拦住的",我说是 SQL Firewall——白名单里只有应用的标准 SQL,那条 SELECT salary FROM employees 不在列表里,直接被拦了。攻击者拿到的只有报错提示,没有数据。后来我去查审计日志,发现 Firewall 从启用到现在,替我们拦过的东西从 SQL 注入到半夜的执行计划探测都有,而统一审计那边的记录条数也没多到让人焦虑——因为列级审计帮我们把大量不重要的操作过滤掉了。那天晚上我关电脑的时候想了一件事:这三层东西,以前在 Oracle 上得装额外的产品、买额外的授权、对接额外的系统才能凑齐,现在 26ai 一个库里全塞进去了。## 八、从传统审计迁移到统一审计:我踩过的坑

如果你的库是从 19c 或 23c 迁移上来的,有一件事在升级前必须做:把传统审计策略换成统一审计。我接手这套库的时候,库里还挂着五六条老掉牙的 AUDIT 命令:

AUDIT SELECT ON hr.employees BY ACCESS;
AUDIT UPDATE ON hr.employees BY ACCESS;

这些东西在 19c 上跑得好好的,但 26ai 不再支持。升级到 26ai 之后,它们不会被自动迁移——直接失效。如果没人注意到,你以为审计还在跑,实际上一行记录都没写,等到合规审计来查才发现,那就是事故了。

迁移本身不复杂,但有个关键点:两边不能同时开着。你先把统一审计策略建好、测通,再把传统审计停掉,中间不能有"跑着传统、慢慢加统一"的过渡期——因为统一审计一旦启用,传统审计的记录就不再可靠,可能丢数据。标准的做法是:

第一,把每一行传统 AUDIT 命令翻译成 CREATE AUDIT POLICY。旧的 AUDIT SELECT ON hr.employees BY ACCESS 对应的是:

CREATE AUDIT POLICY hr_emp_select_pol
  ACTIONS SELECT ON hr.employees;

注意传统审计里的 BY ACCESS(每访问一次记一条)和 BY SESSION(每个会话记一条)在统一审计里不再需要指定,统一审计默认就是每动作记一条,粒度更细,但原则上不影响。

第二,建好策略之后一条条执行 AUDIT POLICY ...; 启用。用下面这条确认所有预期策略都已经生效:

SELECT policy_name, enabled_optFROM AUDIT_UNIFIED_ENABLED_POLICIES;

第三,确认统一审计跑起来正常之后,用 NOAUDIT 把传统策略停掉。如果你留着传统策略不管,它们不会导致错误,但也确实不再产生记录,相当于"沉默失效"——这才是最危险的状态。所以我的建议是:升级前先建好统一策略,升级完第一时间启用并验证,确认无误后立即 NOAUDIT 清掉老的。

还有一个坑: SYS.AUD$ 表里的历史审计数据要不要留着?26ai 下 AUD$ 表依然存在,只是不再被写入新记录。如果合规要求保留历史数据, AUD$ 可以留着不动;如果没这个要求,可以在 NOAUDIT 之后把 AUD$ 清掉,省点空间。我们当时把升级前 180 天的审计记录导出来存了一份归档,然后 TRUNCATE TABLE SYS.AUD$——反正也不会再有新数据写进去了。

九、SQL Firewall 误拦截事件处理实录

前面讲 SQL Firewall 的时候说"观察模式跑两周",这个两周不是随口说的。我们的教训是:训练期不够,上线就出事。

第一次上线 SQL Firewall 时,我设了三天捕获,第四天就生成了白名单,切了阻断模式。结果第二周应用发了个小版本,改了一条查询语句的写法——原来 SELECT a,b FROM t WHERE c = :1,新版本改成了 SELECT a,b FROM t WHERE c IN (SELECT x FROM ref) WHERE ...。SQL 文本变了,白名单没有这一条,直接报 ORA-47605。业务那边炸了十分钟,我这边紧急把 block 改成 FALSE 降级成观察模式才恢复。

复盘原因是:三天覆盖了业务操作周期,但没覆盖"应用发版周期"。我们的应用每两周发一次版,每次 SQL 都会有小改动。只捕获一个稳定期的 SQL,根本抓不到发版时的变化。后来我把训练期改成了两轮发版——两周加三天,确保新老版本的 SQL 都进过捕获日志。发版当天再单独启动一次短周期的捕获,把新版 SQL 补进去,再切回阻断。

还有一个案例跟 SQL 文本无关,是上下文的问题。有一次我们一个运维同事从跳板机连库执行一条 ALTER TABLESPACE 维护命令,结果也被拦了。原因很简单:跳板机的 IP 不在 APP 用户的 ALLOWED_CONTEXT 里。但他的运维账号跟应用账号不是同一个——我查了才发现,我给他的 DBA 账号没启 SQL Firewall,他是在 APP 用户下执行的。这不是 SQL Firewall 的问题,是操作习惯问题——运维就该用运维账号,别偷懒用应用账号登。后来我加了一条运维账号的 Firewall 策略,该补的上下文补上,就没再出事。

这两个 case 教训说穿了就两条:第一,训练期的时长取决于你业务的变更频率,不是"捕获三天"就够,得覆盖至少一个完整发版周期;第二,上下文白名单不只是配了好看的,IP 和程序名的限制是防火墙落地时最容易被忽略但最有效的防线——攻击者拿到密码不稀奇,拿到密码还从合法 IP 和合法程序连进来,门槛就高了一截。

安全这东西,越往内核里做,越不容易被绕过去——这是个朴素的道理,但从 19c 到 26ai,Oracle 确实是按这个思路在改。


请使用浏览器的分享功能分享到微信等