数据库基础体系 · 第 111/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Oracle 安全治理:用户、角色、Profile、TDE、审计和最小权限

Oracle 的安全治理不是给数据库“加一层密码”这么简单,而是同时处理以下问题:

  1. 谁可以连接数据库:用户身份认证与账户状态。
  2. 连接后可以做什么:系统权限、对象权限和角色。
  3. 权限如何被约束:最小权限、授权链和存储程序执行权限。
  4. 账户如何被限制:Profile 对密码、登录失败和资源的控制。
  5. 数据如何防止介质泄露:Transparent Data Encryption,简称 TDE。
  6. 谁在什么时候做了什么:统一审计、细粒度审计和审计数据保护。
  7. 发生异常时如何判断边界:权限错误、账户锁定、钱包未打开、审计策略未命中等。

这些机制的保护对象并不相同。TDE 主要保护数据库文件、备份或其他静态介质;审计负责记录行为;角色负责组织授权;Profile 负责账户和部分资源约束;最小权限负责从整体上限制操作集合。把其中任意一个机制当成其他机制的替代品,都会产生安全盲区。


一、先建立 Oracle 权限模型

1. 用户、Schema 与账户

在 Oracle 中,**用户(user)**是可以认证、拥有权限和建立会话的数据库主体;Schema 是与用户同名的对象命名空间以及该用户拥有的数据库对象集合。

例如:

CREATE USER app_user
  IDENTIFIED BY "Strong-Password-Example-1"
  DEFAULT TABLESPACE app_data
  TEMPORARY TABLESPACE temp
  QUOTA 100M ON app_data;

GRANT CREATE SESSION TO app_user;

这里有三个不同层次:

  • app_user 是账户主体;
  • app_user 通常也是名为 APP_USER 的 Schema;
  • CREATE SESSION 只允许建立数据库会话,并不表示可以访问业务表;
  • QUOTA 规定该用户在指定表空间中最多可以使用多少空间。

如果没有对象权限,用户虽然可以登录,但执行查询仍可能失败:

SELECT * FROM app_owner.orders;

可能返回:

ORA-00942: table or view does not exist

这个错误不一定表示表不存在,也可能是当前用户没有访问该对象的权限。

Oracle 的对象所有权和权限是分离的。对象属于创建它的 Schema,而其他用户需要显式获得对象权限:

GRANT SELECT ON app_owner.orders TO app_user;

对于业务用户,通常不应让其成为业务表的所有者。更常见的分工是:

  • APP_OWNER:拥有表、索引、序列、存储程序;
  • APP_RUNTIME:应用运行账户,只获得运行时需要的权限;
  • APP_MIGRATION:发布或迁移账户,获得受控的 DDL 权限;
  • DBA 或运维账户:承担管理任务,但不作为应用连接账户。

这样做的原因是:应用账户一旦被注入或凭据泄露,攻击者取得的是运行时权限,而不是整个业务 Schema 的所有权。

2. 系统权限、对象权限和角色

Oracle 中常见的授权类型如下。

系统权限

系统权限描述数据库级能力,例如:

GRANT CREATE SESSION TO app_user;
GRANT CREATE TABLE TO developer_user;

CREATE TABLE 不仅表示“能执行一条 CREATE TABLE 语句”,还涉及用户在目标表空间中的配额。没有配额时,即使拥有创建表的系统权限,也可能因为无法分配空间而失败。

类似地,CREATE ANY TABLE 的范围远大于 CREATE TABLEANY 类系统权限通常允许用户在其他 Schema 中创建或操作对象,授权风险显著更高。

对象权限

对象权限作用于特定对象:

GRANT SELECT ON app_owner.orders TO app_runtime;
GRANT INSERT, UPDATE ON app_owner.orders TO app_runtime;
GRANT EXECUTE ON app_owner.order_api TO app_runtime;

应优先授予明确的对象权限,而不是使用范围过大的系统权限。例如,应用只需要读取一个表时,不应授予:

GRANT SELECT ANY TABLE TO app_runtime;

角色

角色是权限集合,可以包含系统权限、对象权限,也可以包含其他角色:

CREATE ROLE app_read_role;
GRANT SELECT ON app_owner.orders TO app_read_role;
GRANT app_read_role TO app_runtime;

角色的价值不只是减少 GRANT 语句数量,更重要的是把“岗位或应用身份”和“权限集合”分离:

  • 权限变化时修改角色;
  • 人员变化时修改角色成员;
  • 审查时查看角色定义,而不是逐个用户分析。

用户获得角色后,角色可以是默认角色,也可以只在特定会话中启用:

ALTER USER app_runtime DEFAULT ROLE app_read_role;

查看授权关系:

SELECT grantee, granted_role, default_role
FROM dba_role_privs
WHERE grantee IN ('APP_RUNTIME', 'APP_OWNER');

SELECT grantee, privilege
FROM dba_sys_privs
WHERE grantee = 'APP_READ_ROLE';

SELECT grantee, owner, table_name, privilege
FROM dba_tab_privs
WHERE grantee = 'APP_READ_ROLE';

DBA_* 视图通常需要较高权限。普通用户可以查看的范围取决于其数据字典访问权限。

3. 授权选项会改变权限边界

以下两种授权尤其需要谨慎:

GRANT CREATE SESSION TO developer_user WITH ADMIN OPTION;
GRANT SELECT ON app_owner.orders TO reporting_user WITH GRANT OPTION;
  • WITH ADMIN OPTION 允许被授权者把系统权限或角色继续授予其他主体,也可以撤销自己授予的权限。
  • WITH GRANT OPTION 允许被授权者继续授予对象权限。

如果 reporting_userSELECT ON app_owner.orders 授予 external_user,之后撤销 reporting_user 的权限,并不应简单假定授权链会自动按业务意图收敛。实际影响取决于授权关系和被授予方式,因此生产授权应避免无必要的转授权。

可将授权关系看成一个有向图:

G=(V,E)G=(V,E)

其中:

  • VV 是用户和角色;
  • EE 是“授予角色”“授予系统权限”或“授予对象权限”的边。

当允许转授权时,一个节点不仅拥有权限,还拥有增加图中边的能力。权限审查因此必须检查两件事:

  1. 当前主体实际能执行哪些操作;
  2. 当前主体能否把这些操作继续传播给其他主体。

二、最小权限:从口号变成可验证条件

1. 最小权限的形式化定义

设某个业务功能 FF 在完整执行过程中实际需要的权限集合为:

R(F)={p1,p2,,pn}R(F)=\{p_1,p_2,\ldots,p_n\}

某用户通过直接授权、角色、公共权限以及存储程序执行上下文最终获得的有效权限集合为:

E(u)E(u)

最小权限的基本条件是:

R(F)E(u)R(F)\subseteq E(u)

同时,理想状态是:

E(u)R(F)=E(u)\setminus R(F)=\varnothing

也就是说:

  • 缺少 R(F)R(F) 中任何权限,功能会失败;
  • 多出不属于 R(F)R(F) 的权限,攻击面会扩大。

实际工程中,往往不能做到完全相等,因为同一账户可能运行多个功能。此时应按职责拆分账户或角色,使每个主体的权限集合更接近单一业务功能。

2. 一个完整的授权算例

假设订单应用只需要:

  • 读取 APP_OWNER.PRODUCTS
  • 新增订单;
  • 通过存储程序取消订单;
  • 不允许直接更新订单金额。

可以这样设计:

CREATE ROLE order_runtime_role;

GRANT SELECT ON app_owner.products
  TO order_runtime_role;

GRANT INSERT ON app_owner.orders
  TO order_runtime_role;

GRANT EXECUTE ON app_owner.order_api
  TO order_runtime_role;

CREATE USER order_runtime
  IDENTIFIED BY "Another-Strong-Password-1"
  DEFAULT TABLESPACE app_data
  TEMPORARY TABLESPACE temp
  QUOTA 0 ON app_data;

GRANT CREATE SESSION TO order_runtime;
GRANT order_runtime_role TO order_runtime;

这里的 QUOTA 0 表示该用户不能在 APP_DATA 中创建需要占用配额的对象。它仍然可以访问被授予的对象,但不能因为意外获得 CREATE TABLE 后在业务表空间中任意建表。

如果应用还需要调用 ORDER_API.CANCEL_ORDER,可以把取消操作封装为存储程序:

CREATE OR REPLACE PACKAGE app_owner.order_api
AUTHID DEFINER
AS
  PROCEDURE cancel_order(p_order_id NUMBER);
END;
/

CREATE OR REPLACE PACKAGE BODY app_owner.order_api
AS
  PROCEDURE cancel_order(p_order_id NUMBER)
  AS
  BEGIN
    UPDATE app_owner.orders
       SET status = 'CANCELLED',
           cancelled_at = SYSTIMESTAMP
     WHERE order_id = p_order_id
       AND status = 'PAID';

    IF SQL%ROWCOUNT = 0 THEN
      RAISE_APPLICATION_ERROR(
        -20001,
        'Order does not exist or is not cancellable'
      );
    END IF;
  END;
END;
/

AUTHID DEFINER 表示默认的定义者权限模型:程序主要使用程序所有者的直接权限执行。随后只授予运行账户执行权限:

GRANT EXECUTE ON app_owner.order_api TO order_runtime;

这样,运行账户可以取消符合业务条件的订单,但不需要直接获得:

GRANT UPDATE ON app_owner.orders TO order_runtime;

这比直接授予整张表的 UPDATE 更接近最小权限。

3. 角色权限与存储程序的常见陷阱

在定义者权限的 PL/SQL 单元中,角色授予的权限通常不能替代所需的直接权限。也就是说,下面这种设计可能导致程序编译或运行失败:

GRANT SELECT ON app_owner.orders TO app_runtime_role;
GRANT app_runtime_role TO app_owner;

APP_OWNER 作为程序所有者通过角色得到权限,但定义者权限程序需要的对象权限应直接授予 APP_OWNER

GRANT SELECT ON app_owner.orders TO app_owner;

这不是角色失效,而是存储程序权限解析与普通 SQL 会话权限解析存在差异。排查 ORA-01031: insufficient privileges 时,应分别检查:

SELECT grantee, owner, table_name, privilege
FROM dba_tab_privs
WHERE grantee = 'APP_OWNER';

SELECT grantee, granted_role
FROM dba_role_privs
WHERE grantee = 'APP_OWNER';

此外,动态 SQL、AUTHID CURRENT_USER、对象所有者、同义词和跨 Schema 引用会改变实际权限路径。权限审计不能只看“用户拥有哪个角色”,还要看具体 SQL 和执行上下文。

4. 最小权限的验证方法

先用业务账户测试允许的操作:

CONNECT order_runtime/"Another-Strong-Password-1"

SELECT product_id, product_name
FROM app_owner.products;

INSERT INTO app_owner.orders(order_id, product_id, quantity, status)
VALUES (1001, 10, 2, 'NEW');

UPDATE app_owner.orders
SET amount = 0
WHERE order_id = 1001;

预期结果:

  • SELECT 成功;
  • INSERT 成功,前提是约束和序列等依赖已正确配置;
  • 直接 UPDATE 失败,通常为 ORA-01031

再测试存储程序:

BEGIN
  app_owner.order_api.cancel_order(1001);
END;
/

如果程序成功而直接更新失败,说明接口边界确实生效。若程序失败,应沿以下路径排查:

  1. ORDER_RUNTIME 是否拥有 EXECUTE
  2. APP_OWNER 是否直接拥有程序访问的对象权限;
  3. 程序中的对象是否使用了正确的 Schema;
  4. 是否存在动态 SQL;
  5. 数据行是否满足状态条件;
  6. 是否因并发更新导致 SQL%ROWCOUNT = 0

三、Profile:账户策略,不是权限集合

1. Profile 的职责

Profile 是分配给用户的一组资源限制和密码管理参数。它不授予 SELECTUPDATECREATE SESSION,也不能代替角色。

创建用户时,默认使用 DEFAULT Profile:

CREATE PROFILE app_profile LIMIT
  FAILED_LOGIN_ATTEMPTS 5
  PASSWORD_LOCK_TIME 1/24
  PASSWORD_LIFE_TIME 90
  PASSWORD_REUSE_TIME 365
  PASSWORD_REUSE_MAX 10
  PASSWORD_VERIFY_FUNCTION ora12c_strong_verify_function;

然后分配给用户:

ALTER USER order_runtime PROFILE app_profile;

查询配置:

SELECT profile, resource_name, resource_type, limit
FROM dba_profiles
WHERE profile = 'APP_PROFILE'
ORDER BY resource_type, resource_name;

Profile 的常见参数包括:

  • FAILED_LOGIN_ATTEMPTS:连续认证失败达到多少次后锁定账户;
  • PASSWORD_LOCK_TIME:账户锁定后的锁定时长;
  • PASSWORD_LIFE_TIME:密码有效期;
  • PASSWORD_REUSE_TIMEPASSWORD_REUSE_MAX:密码历史复用约束;
  • PASSWORD_VERIFY_FUNCTION:密码复杂度校验函数;
  • SESSIONS_PER_USER:每个用户允许的并发会话数;
  • IDLE_TIME:会话空闲时间限制;
  • CONNECT_TIME:单次会话连接时长限制;
  • CPU_PER_SESSIONCPU_PER_CALL:CPU 使用限制。

密码参数与资源参数不是同一类控制。一个 Profile 可以要求复杂密码,但不限制并发会话;也可以限制会话数,但不代表密码足够安全。

2. Profile 的状态变化

当用户连续输入错误密码时,账户可能经历:

OPEN
  -- 达到 FAILED_LOGIN_ATTEMPTS -->
LOCKED(TIMED)
  -- 等待 PASSWORD_LOCK_TIME -->
OPEN

管理员也可以手工锁定:

ALTER USER order_runtime ACCOUNT LOCK;

解锁:

ALTER USER order_runtime ACCOUNT UNLOCK;

查询状态:

SELECT username,
       account_status,
       lock_date,
       expiry_date,
       profile
FROM dba_users
WHERE username = 'ORDER_RUNTIME';

常见状态包括:

  • OPEN:可正常认证;
  • LOCKED:被显式锁定;
  • LOCKED(TIMED):因失败次数被临时锁定;
  • EXPIRED:密码过期;
  • EXPIRED & LOCKED:密码过期且账户被锁定。

典型失败表现:

  • ORA-28000: the account is locked:账户被锁定;
  • ORA-28001: the password has expired:密码过期;
  • ORA-01017: invalid username/password; logon denied:认证信息错误,也可能由认证方式或连接到错误容器引起。

解锁并重置密码:

ALTER USER order_runtime
  IDENTIFIED BY "New-Strong-Password-2"
  ACCOUNT UNLOCK;

生产环境中不能只执行 ACCOUNT UNLOCK 而忘记密码状态。如果账户已经过期,仍可能无法登录。

3. Profile 的边界与风险

Profile 不是防暴力破解系统

FAILED_LOGIN_ATTEMPTS 可以限制单个数据库账户的连续失败次数,但无法完全阻止:

  • 攻击者轮换多个账户;
  • 从连接池或中间层发起请求;
  • 使用已泄露的正确密码;
  • 在数据库前置网络层持续发送连接请求。

所以它是数据库账户层面的控制,不是完整的身份安全系统。

IDLE_TIME 不等于请求超时

IDLE_TIME 主要针对数据库会话空闲状态。应用连接池可能长期保持连接,却频繁执行轻量 SQL;此时不能把 IDLE_TIME 当作接口级超时或事务超时。

资源限制需要确认实例语义

传统资源限制参数是否生效,与实例参数和部署架构有关。应检查:

SHOW PARAMETER resource_limit;

在需要启用资源限制的环境中:

ALTER SYSTEM SET resource_limit = TRUE;

不同版本、CDB/PDB 架构和参数管理方式可能存在差异,不能仅凭 Profile 中出现了参数就断定运行时一定受限。修改后要用实际会话和监控视图验证,而不是只查看配置文本。

4. CDB、PDB 与用户范围

在多租户架构中,用户可能是:

  • 本地用户:只存在于某个 PDB;
  • 公共用户:在 CDB 范围创建,名称通常带 C## 前缀,可跨容器存在。

例如,业务应用通常应连接到目标 PDB,并在该 PDB 中创建本地用户:

ALTER SESSION SET CONTAINER = app_pdb;

CREATE USER app_runtime
  IDENTIFIED BY "Pdb-Password-Example-1";

GRANT CREATE SESSION TO app_runtime;

如果在错误的容器中执行 CREATE USER,可能出现:

  • 用户实际创建在 CDB Root,而不是业务 PDB;
  • 连接到 PDB 时找不到用户;
  • 授权对象属于另一个容器;
  • 查询 DBA_USERS 时观察到的结果与应用连接端不同。

因此排查认证和授权问题时,必须同时确认:

SHOW CON_NAME;
SELECT SYS_CONTEXT('USERENV', 'CON_NAME') FROM dual;
SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM dual;

四、TDE:保护静态数据,不是保护查询结果

1. TDE 解决什么问题

Transparent Data Encryption,透明数据加密,用于加密数据库中的静态数据,例如:

  • 数据文件;
  • 表空间;
  • 某些列;
  • 备份或复制出的数据库文件,在相应配置下受到密钥保护。

“透明”是指应用通常不需要修改 SQL:

SELECT card_token FROM app_owner.payment;

应用看到的是正常数据,Oracle 在底层完成加密和解密。

TDE 主要防范的是:

  • 直接窃取数据文件;
  • 盗取部分备份介质;
  • 未经授权读取存储层文件。

TDE 不直接防范:

  • 已登录且有 SELECT 权限的用户读取明文;
  • 被注入的应用执行合法查询;
  • 拥有足够数据库管理权限的人通过数据库接口读取数据;
  • 应用日志、导出文件、缓存或屏幕中出现的明文;
  • 弱授权导致的数据外泄。

因此:

TDE访问控制\text{TDE} \neq \text{访问控制}

更准确地说,TDE 保护的是“存储介质上的表示”,而最小权限保护的是“数据库操作能力”。

2. TDE 的密钥层次

TDE 通常涉及以下概念:

  • Keystore/Wallet:保存密钥材料的密钥库;
  • 主密钥:用于保护数据库或表空间密钥;
  • 表空间密钥或列密钥:用于实际数据加密;
  • 加密数据:落盘时以密文形式保存。

可以简化为:

C=EKd(P)C = E_{K_d}(P)

其中:

  • PP 是明文数据;
  • KdK_d 是数据加密密钥;
  • CC 是落盘密文。

而数据密钥本身还需要由密钥库中的更高层密钥保护。这样做的目的,是在不重新加密全部数据的情况下完成密钥轮换。

3. 配置 TDE 的关键状态

TDE 不是执行一条 ALTER TABLESPACE ... ENCRYPTION 就结束了。至少要确认:

  1. 密钥库已创建;
  2. 密钥库已经打开;
  3. 主密钥已经设置;
  4. 目标表空间或列已完成加密;
  5. 重启、备份和恢复流程能够重新获得密钥;
  6. CDB/PDB 的密钥库范围符合部署设计。

下面是一个典型的管理流程。路径和密码必须替换为组织自己的安全值,示例中的值不能直接用于生产:

-- 在具备 TDE 管理权限的管理会话中执行
ADMINISTER KEY MANAGEMENT CREATE KEYSTORE
  '/opt/oracle/admin/DB1/tde'
  IDENTIFIED BY "Keystore-Password-Example-1";

ADMINISTER KEY MANAGEMENT SET KEYSTORE OPEN
  IDENTIFIED BY "Keystore-Password-Example-1";

ADMINISTER KEY MANAGEMENT SET KEY
  IDENTIFIED BY "Keystore-Password-Example-1"
  WITH BACKUP USING 'initial_tde_key_backup';

这些语句的具体可用性取决于数据库版本、密钥库类型、CDB/PDB 配置以及部署方式。生产执行前应以目标版本 SQL Language Reference 和 Advanced Security 相关文档为准。尤其不能把文件系统密码写入脚本、命令历史或普通配置仓库。

查看状态:

SELECT wallet_type,
       status,
       wallet_order,
       keystore_mode
FROM v$encryption_wallet;

在支持相应视图列的版本中,还可以查询加密表空间:

SELECT tablespace_name,
       encrypted
FROM dba_tablespaces;

部分版本也提供:

SELECT tablespace_name,
       encrypted,
       encryptionalg
FROM v$encrypted_tablespaces;

如果密钥库没有打开,常见表现是:

ORA-28365: wallet is not open

数据库可能仍然可以启动,但访问依赖 TDE 密钥的数据时失败。这个故障不是通过给业务用户加权限解决的,而是要检查:

  1. 数据库实例实际使用的 keystore 配置;
  2. wallet 文件或外部密钥管理服务是否可访问;
  3. 密钥库密码或自动登录配置是否正确;
  4. CDB Root 和目标 PDB 的密钥库状态;
  5. 数据库重启后的自动打开策略。

4. 表空间加密与列加密

表空间加密适合保护一组表及其相关段,运维边界比较清晰:

CREATE TABLESPACE app_secure
  DATAFILE '/opt/oracle/oradata/DB1/app_secure01.dbf'
  SIZE 1G
  ENCRYPTION USING 'AES256'
  DEFAULT STORAGE (ENCRYPT);

也可以对已有表空间进行加密,具体语法和在线操作限制取决于版本及存储环境。执行前要确认:

  • 是否需要额外空间;
  • 是否会产生较大 I/O;
  • 是否影响备份窗口;
  • 是否存在无法在线完成的对象;
  • 加密后的表空间是否已纳入恢复演练。

列级 TDE 适合只保护少数敏感列,例如身份证号或支付标识。它的边界更细,但可能带来:

  • 索引和查询行为变化;
  • 等值查询、排序或连接能力受限;
  • 应用改造或密文索引设计;
  • 列迁移和密钥轮换复杂度增加。

不要把“列已加密”理解成“数据库中所有出现过的敏感数据都安全”。同一数据可能还存在于:

  • 非加密临时表;
  • 非加密表空间;
  • 导出文件;
  • 审计内容;
  • 应用日志;
  • 备份或归档副本。

5. TDE 的备份与恢复边界

备份数据库文件而不备份相应密钥材料,恢复出的文件可能无法解密。密钥库备份必须具备:

  • 独立的访问控制;
  • 与数据库备份不同的保护边界;
  • 可验证的恢复流程;
  • 明确的密钥轮换记录;
  • 防止单人同时获得数据库备份和密钥库的控制措施。

但把密钥库和数据库备份放在同一个普通目录,并不能形成有效隔离。攻击者如果同时得到数据文件和密钥材料,TDE 的介质保护目标就被破坏。

TDE 还涉及许可证和版本差异。Oracle 不同版本、版本包和部署形态对 TDE 功能、外部密钥管理以及授权要求可能不同,生产采购和许可判断必须以目标版本的官方许可文档为准,不能依据旧版本经验推断。


五、审计:记录行为,但不自动阻止行为

1. 审计的定义和数据流

**审计(auditing)**是把安全相关事件记录为审计记录。一个审计记录通常至少需要表达:

  • 谁执行了操作;
  • 什么时候执行;
  • 从哪里执行;
  • 执行了什么动作;
  • 作用于哪个对象;
  • 是否成功;
  • 可能的 SQL 文本、绑定信息或返回码。

可以把一次审计事件抽象为:

A=(u,t,s,o,a,r)A=(u,t,s,o,a,r)

其中:

  • uu:执行主体;
  • tt:时间;
  • ss:会话或来源信息;
  • oo:对象;
  • aa:动作;
  • rr:结果。

审计的核心是可追溯性,不是实时阻断。即使审计记录显示某用户执行了危险 SQL,该 SQL 通常已经执行完成;要阻止操作,应使用权限、视图、VPD、Database Vault、网络控制或应用层策略等机制。

2. 统一审计与传统审计

现代 Oracle 数据库提供统一审计(Unified Auditing),将多个审计来源统一到统一审计轨迹。不同版本可能处于:

  • 混合模式:传统审计能力与统一审计并存;
  • 纯统一审计:统一审计功能完全启用。

可先查看相关配置和轨迹:

SELECT policy_name,
       enabled_option,
       entity_name,
       success,
       failure
FROM audit_unified_enabled_policies;

不同版本视图列可能有所差异,查询前应对照目标版本数据字典文档。

审计记录通常可以从:

SELECT event_timestamp,
       dbusername,
       action_name,
       object_schema,
       object_name,
       return_code,
       unified_audit_policies
FROM unified_audit_trail
WHERE event_timestamp >= SYSTIMESTAMP - INTERVAL '1' HOUR
ORDER BY event_timestamp DESC;

查看。普通业务用户通常不能直接读取完整统一审计轨迹,审计查询应由独立的审计或安全管理角色完成。

3. 创建和启用统一审计策略

例如,只审计运行账户对订单表的修改:

CREATE AUDIT POLICY order_change_audit
  ACTIONS
    INSERT ON app_owner.orders,
    UPDATE ON app_owner.orders,
    DELETE ON app_owner.orders;

AUDIT POLICY order_change_audit
  BY order_runtime
  WHENEVER NOT SUCCESSFUL;

这里的语义是:

  • 策略定义要关注的动作;
  • BY order_runtime 将策略范围限制到指定主体;
  • WHENEVER NOT SUCCESSFUL 只记录失败操作。

如果要同时记录成功和失败,可以按目标版本支持的语法启用相应策略。审计策略设计应明确“要回答什么问题”,而不是无差别记录所有 SQL。例如:

  • 谁修改了支付状态?
  • 谁尝试读取敏感表但失败?
  • 谁创建、删除或修改了数据库用户?
  • 谁改变了 TDE 密钥或审计策略?
  • 哪个应用账户在短时间内产生大量失败访问?

取消策略:

NOAUDIT POLICY order_change_audit
  BY order_runtime;

删除策略:

DROP AUDIT POLICY order_change_audit;

删除前需要先确认该策略没有被依赖或被组织的变更流程引用。

4. 审计策略的命中验证

仅执行 CREATE AUDIT POLICY 不足以证明审计生效。应执行端到端验证:

CONNECT order_runtime/"Another-Strong-Password-1"

UPDATE app_owner.orders
SET status = 'PAID'
WHERE order_id = 1001;

然后由审计管理员查询:

SELECT event_timestamp,
       dbusername,
       action_name,
       object_schema,
       object_name,
       return_code
FROM unified_audit_trail
WHERE dbusername = 'ORDER_RUNTIME'
  AND object_schema = 'APP_OWNER'
  AND object_name = 'ORDERS'
ORDER BY event_timestamp DESC;

如果更新成功,预期可以看到对应的成功事件;如果用没有权限的账户执行失败操作,并且策略配置了失败审计,则应看到非零 RETURN_CODE 的记录。若查不到记录,应依次排查:

  1. 当前连接的是不是目标 PDB;
  2. 策略是否启用在正确的用户和容器;
  3. 审计条件是成功、失败还是两者;
  4. 查询时间范围和时区是否正确;
  5. 审计记录是否尚未刷新或已被归档;
  6. 当前查询者是否有读取审计轨迹的权限;
  7. 实际执行的是不是预期对象,是否经过了同义词或视图。

5. 细粒度审计

统一审计可以记录对象和动作,但某些需求需要根据行条件或访问上下文审计。例如,只在读取高价值订单、特定列或特定客户端模块时审计,可以使用 Fine-Grained Auditing,通常通过 DBMS_FGA 配置。

概念示例:

BEGIN
  DBMS_FGA.ADD_POLICY(
    object_schema   => 'APP_OWNER',
    object_name     => 'PAYMENT',
    policy_name     => 'PAYMENT_SENSITIVE_READ',
    audit_condition => 'payment_status = ''PAID''',
    audit_column    => 'CARD_TOKEN',
    statement_types => 'SELECT'
  );
END;
/

该调用的精确参数和行为受数据库版本影响,部署前必须按目标版本的 DBMS_FGA 文档验证。细粒度审计不是“加上条件就一定记录每一次逻辑访问”,还要考虑:

  • 查询是否实际访问了指定列;
  • 优化器是否消除了某些列访问;
  • 条件是否为真或为 NULL
  • 视图、同义词和底层表的对象边界;
  • 审计记录中的 SQL 是否包含敏感值。

6. 审计本身也需要保护

审计轨迹可能快速增长,并占用数据库空间。应监控:

  • 审计表所在表空间;
  • 审计记录增长速度;
  • 清理与归档策略;
  • 审计管理员的访问权限;
  • 审计记录是否转发到独立日志平台;
  • 数据库管理员是否能修改审计策略或删除记录。

审计数据不应只存在于被审计的同一个数据库中。否则攻击者取得高权限后,可能同时影响业务数据和审计证据。更稳妥的做法是定期抽取到受控的外部平台,并验证传输完整性、时间同步和保留周期。

审计失败时还要明确系统选择:

  • 是允许业务继续但记录可能丢失;
  • 还是在审计不可用时阻断关键操作。

两者没有普遍适用的答案。金融、支付等场景可能要求“审计不可用则拒绝关键操作”;普通业务可能优先保证可用性。这个取舍必须在设计阶段明确,而不是等审计表空间满后被动决定。


六、TDE、访问控制、脱敏和注入防护的边界

这些机制经常被混为一谈,但它们位于不同层次。

机制 主要保护对象 主要解决的问题
用户认证 身份 谁能建立会话
角色和权限 操作能力 能访问哪些对象、执行哪些动作
Profile 账户与部分资源 密码、锁定、并发和会话限制
TDE 静态介质 数据文件或备份被直接读取
审计 行为证据 谁在什么时候做了什么
数据脱敏 返回结果 某些主体看到部分或替换后的值
SQL 注入防护 输入到 SQL 的解析边界 防止输入改变 SQL 语义

例如,TDE 加密了 CARD_TOKEN,但拥有 SELECT 权限的应用仍会得到明文。若客服只应看到部分值,应使用数据脱敏、视图或专门 API:

CREATE OR REPLACE VIEW app_owner.payment_for_support AS
SELECT payment_id,
       CASE
         WHEN SYS_CONTEXT('USERENV', 'SESSION_USER') = 'SUPPORT_USER'
         THEN '****' || SUBSTR(card_token, -4)
         ELSE card_token
       END AS card_token_display
FROM app_owner.payment;

这个示例展示了思路,但在生产中不应仅依赖字符串判断用户。还应考虑:

  • 角色或应用上下文;
  • 视图是否被绕过;
  • 导出和报表路径;
  • 直接访问基表的权限;
  • 连接池复用会话造成的上下文残留。

SQL 注入则是另一类问题。下面的拼接方式具有风险:

String sql = "SELECT * FROM orders WHERE order_id = " + input;

应使用绑定变量:

PreparedStatement ps =
    connection.prepareStatement(
        "SELECT order_id, status FROM app_owner.orders WHERE order_id = ?");
ps.setLong(1, orderId);

绑定变量防止输入改变 SQL 结构,但它不替代最小权限。即使没有注入,应用账户如果拥有 SELECT ANY TABLE,凭据泄露后的后果仍然很大。


七、生产环境中的权限审查与诊断

1. 查看用户有效权限

常用视图包括:

SELECT username, account_status, profile, default_tablespace
FROM dba_users
WHERE username = 'ORDER_RUNTIME';

SELECT grantee, granted_role, default_role
FROM dba_role_privs
WHERE grantee = 'ORDER_RUNTIME';

SELECT grantee, privilege
FROM dba_sys_privs
WHERE grantee = 'ORDER_RUNTIME';

SELECT grantee, owner, table_name, privilege
FROM dba_tab_privs
WHERE grantee = 'ORDER_RUNTIME';

但这些查询仍可能漏掉几个来源:

  • PUBLIC 权限;
  • 角色嵌套;
  • 代理认证;
  • 定义者权限程序;
  • ANY 系统权限;
  • 通过视图、同义词和数据库链接间接访问;
  • 外部身份映射或企业用户权限。

因此,权限审查必须以实际业务动作测试为最终验证,而不能只生成一份角色清单。

2. 使用会话视图确认当前权限

在目标用户会话中:

SELECT * FROM session_roles;

SELECT privilege
FROM session_privs
ORDER BY privilege;

如果某角色不是默认角色,它可能不会出现在 SESSION_ROLES 中。可以在允许的前提下测试:

SET ROLE app_read_role;

但生产应用不应依赖人工 SET ROLE 来弥补错误的默认角色设计。角色启用策略必须和连接池生命周期一致。

3. 常见故障的因果关系

ORA-01031

可能原因:

  • 没有直接对象权限;
  • 只授予了角色,但代码运行在定义者权限程序中;
  • 当前角色未启用;
  • 访问了错误 Schema;
  • 缺少 EXECUTE 或底层依赖权限;
  • 在错误的 PDB 中执行。

恢复方法不是盲目授予 DBA,而是先定位失败语句和执行身份,再补充最窄权限。

ORA-28000

说明账户被锁定。应查询:

SELECT username, account_status, lock_date, profile
FROM dba_users
WHERE username = 'ORDER_RUNTIME';

恢复前还要查找为什么持续失败:

  • 连接池中仍保存旧密码;
  • Secret 已更新但实例未刷新;
  • 多个服务共享同一账户;
  • 健康检查使用了错误凭据;
  • 攻击者正在尝试密码。

只解锁不修复连接池,账户可能立即再次锁定。

ORA-28365

说明依赖的 wallet/keystore 未打开。应检查 V$ENCRYPTION_WALLET、数据库告警日志、文件权限、外部密钥服务和容器范围。不要通过关闭加密或绕过密钥管理来恢复业务,这会破坏原有安全边界。

4. 撤销权限后的真实效果

撤销权限通常影响后续权限检查,但已有会话、已打开游标、连接池和缓存会使观察结果复杂化:

  • 新 SQL 可能立即失败;
  • 已经执行的事务不会因为撤销而自动回滚;
  • 存储程序和对象依赖可能需要重新编译;
  • 连接池中的会话可能仍保留旧角色状态;
  • 应用中间层可能缓存授权结果。

因此,权限变更应包含:

  1. 变更前的授权快照;
  2. 变更脚本;
  3. 新旧会话验证;
  4. 应用连接池刷新策略;
  5. 失败时的精确回滚脚本。

八、一个可落地的分层设计

对于一个典型订单系统,可以将安全边界划分为:

身份层

  • 应用使用独立的运行账户;
  • 人员账户不直接供应用使用;
  • 管理员使用个人账户和可追溯认证;
  • 业务用户连接到正确的 PDB;
  • Profile 约束密码和账户状态。

授权层

  • 表由 APP_OWNER 所有;
  • 应用只获得必要对象权限;
  • 高风险修改通过存储程序封装;
  • 禁止无必要的 ANY 权限;
  • 禁止无必要的 ADMIN OPTIONGRANT OPTION
  • 定期检查角色嵌套和 PUBLIC 权限。

数据层

  • 敏感表空间使用 TDE;
  • 备份、密钥库和恢复流程分别保护;
  • 客服、报表等场景使用脱敏视图或 API;
  • 业务代码使用绑定变量。

证据层

  • 审计关键 DDL、敏感表访问和高风险修改;
  • 记录成功与失败的边界符合调查目标;
  • 审计管理员与数据库管理员职责分离;
  • 审计轨迹归档到独立平台;
  • 定期验证审计策略确实命中。

验证层

对每个业务角色建立“允许矩阵”和“禁止矩阵”:

主体 动作 对象 预期
ORDER_RUNTIME SELECT APP_OWNER.PRODUCTS 成功
ORDER_RUNTIME INSERT APP_OWNER.ORDERS 成功
ORDER_RUNTIME 直接 UPDATE amount APP_OWNER.ORDERS 失败
ORDER_RUNTIME EXECUTE APP_OWNER.ORDER_API 成功
SUPPORT_USER 读取完整支付标识 基表 失败或仅返回脱敏值
审计管理员 查询审计轨迹 统一审计视图 成功

这个矩阵的意义在于把“最小权限”转化为可重复的测试,而不是停留在配置意图上。


九、与 SQLite 的边界

Oracle 的用户、角色、Profile、TDE 和统一审计是数据库服务器安全模型的一部分。SQLite 是嵌入式数据库库,原生模型通常没有 Oracle 意义上的:

  • 数据库用户;
  • 服务器端角色;
  • Profile;
  • 统一审计轨迹;
  • 数据库进程级访问控制。

SQLite 的安全通常依赖:

  • 操作系统文件权限;
  • 应用进程隔离;
  • 加密扩展或外部加密方案;
  • 应用层认证与授权;
  • 事务和参数绑定;
  • 文件备份与密钥管理策略。

因此,不能把 Oracle 的:

CREATE ROLE ...
CREATE PROFILE ...
AUDIT POLICY ...

直接类比为 SQLite 的功能。两者都能执行 SQL,但安全边界不同:Oracle 主要在数据库服务端实施主体和权限控制;SQLite 很大程度上由宿主进程和文件系统承担控制责任。


Oracle 安全治理的核心不是某一个参数,而是让身份、权限、密钥、审计和数据访问边界相互对应:

  • 用户回答“是谁”;
  • 角色和权限回答“能做什么”;
  • Profile 回答“账户如何被限制”;
  • TDE 回答“文件被拿走后是否能直接读”;
  • 审计回答“发生过什么”;
  • 最小权限回答“为什么这个主体不应该拥有更多能力”。

只有把这些机制放在各自正确的边界内,并通过实际成功与失败测试验证,安全配置才真正成为可审查、可诊断、可恢复的治理体系。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。