删除未加密数据的虚副本
Oracle 和底层主机操作系统使用优化的算法来更新数据块中的数据,目的是最大程度地减少降低性能的磁盘 I/O。在对现有列数据进行加密的特定情况下,通常发生的一件事是 Oracle 将加密的列数据写入到新数据块并且只是将之前未加密的值占用的空间标记为未使用。换句话说,Oracle 不会尝试清除较旧的未加密数据。只要所讨论的系统持续遇到大量更新活动,您就有理由确信当重用块空间时 Oracle 将最终覆盖较旧的未加密数据。但是考虑到我的客户正在准备进行合规性审计,我必须确保在加密过程之后立即擦除未加密的敏感数据。
进行了很多研究之后,我在 Oracle 技术网上发现了一个 FAQ 以及一个网志,该网志确认这一特定问题并提供了有关解决该问题的一些基本想法。一般的想法是将包含之前未加密数据的所有段移动到新的表空间以及数据文件),然后使用一个操作系统实用程序删除旧数据文件。但该做法听起来容易,做起来难。事实是,在安全删除旧表空间及其数据文件之前,您很有可能需要移动大量段以及包含敏感数据的段。
为了使这一可能费时费力且易于出错的过程自动进行,我将一些脚本放在一起,以帮助我构建完成这一切所需的 DDL 语句。这里,我要向 Tom Kyte 表示谢意,因为此处的一些工作是修改我在 Asktom 站点找到的内容查询。列表 3 显示了我使用的整个过程的一个示例。
Enter password:
Connected.
SQL> -- Create new tablespaces for data and index segments
SQL> CREATE TABLESPACE data_002 DATAFILE SIZE 1G;
Tablespace created.
SQL> CREATE TABLESPACE indx_002 DATAFILE SIZE 500M;
Tablespace created.
SQL> -- Generate a script to move existing segments to new tablespaces
SQL> COL ORDER_COL1 NOPRINT;
SQL> COL ORDER_COL2 NOPRINT;
SQL> SET HEADING OFF;
SQL> SET VERIFY OFF;
SQL> SET ECHO OFF;
SQL> SELECT DECODE( segment_type, 'TABLE' , segment_name, table_name ) order_col1,
2 DECODE( segment_type, 'TABLE', 1, 2 ) order_col2,
3 'ALTER ' || segment_type || ' ' || LOWER(owner) || '.' || LOWER(segment_name) ||
4 DECODE( segment_type, 'TABLE', ' MOVE ', ' REBUILD ' ) ||
5 'TABLESPACE ' || LOWER(DECODE( segment_type, 'TABLE' , '&&NEW_DATA_TBS' , '&&NEW_INDX_TBS' )) || ';'
6 FROM dba_segments,
7 (SELECT table_name, index_name FROM dba_indexes WHERE tablespace_name = UPPER('&&OLD_INDX_TBS'))
8 WHERE segment_type in ( 'TABLE', 'INDEX' )
9 AND segment_name = index_name (+)
10 AND tablespace_name IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'))
11 AND owner = UPPER('&&OWNER')
12 ORDER BY 1, 2;
Enter value for new_data_tbs: data_002
Enter value for new_indx_tbs: indx_002
Enter value for old_indx_tbs: indx_001
Enter value for old_data_tbs: data_001
Enter value for owner: app_001
ALTER TABLE app_001.transactions MOVE TABLESPACE data_002;
ALTER INDEX app_001.transactions_pk REBUILD TABLESPACE indx_002;
ALTER INDEX app_001.transactions_ndx1 REBUILD TABLESPACE indx_002;
SQL> SET HEADING ON;
SQL> SET VERIFY ON;
SQL> SET ECHO ON;
SQL> -- execute script output
SQL> ALTER TABLE app_001.transactions MOVE TABLESPACE data_002;
Table altered.
SQL> ALTER INDEX app_001.transactions_pk REBUILD TABLESPACE indx_002;
Index altered.
SQL> ALTER INDEX app_001.transactions_ndx1 REBUILD TABLESPACE indx_002;
Index altered.
SQL> -- Check for any unusable indexes
SQL> SELECT owner, index_name, tablespace_name
2 FROM dba_indexes
3 WHERE STATUS = 'UNUSABLE';
no rows selected
SQL> -- Gather new schema stats
SQL> EXEC dbms_stats.gather_schema_stats('app_001');
PL/SQL procedure successfully completed.
SQL> -- Check for remaining segments in old tablespaces
SQL> SELECT distinct owner
2 FROM dba_segments
3 WHERE tablespace_name IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'));
old 3: WHERE tablespace_name IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'))
new 3: WHERE tablespace_name IN (UPPER('data_001'), UPPER('indx_001'))
no rows selected
SQL> -- Check for users assigned to old tablespaces
SQL> SELECT username, default_tablespace FROM dba_users
2 WHERE default_tablespace IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'));
old 2: WHERE default_tablespace IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'))
new 2: WHERE default_tablespace IN (UPPER('data_001'), UPPER('indx_001'))
USERNAME DEFAULT_TABLESPACE
------------------------------ ------------------------------
APP_001 DATA_001
SQL> -- Assign new default tablespaces for users, as necessary
SQL> ALTER USER app_001
2 DEFAULT TABLESPACE data_002;
User altered.
SQL> -- List the data file names of old tablespaces
SQL> COL tablespace_name FOR A15;
SQL> COL file_name FOR A70;
SQL> SET LINES 100;
SQL> SELECT tablespace_name, file_name
2 FROM dba_data_files
3 WHERE tablespace_name IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'));
old 3: WHERE tablespace_name IN (UPPER('&&OLD_DATA_TBS'), UPPER('&&OLD_INDX_TBS'))
new 3: WHERE tablespace_name IN (UPPER('data_001'), UPPER('indx_001'))
TABLESPACE_NAME FILE_NAME
--------------- ----------------------------------------------------------------------
DATA_001 /data01/oracle/db001/datafile/o1_mf_data_001_4m081w7m_.dbf
INDX_001 /data01/oracle/db001/datafile/o1_mf_indx_001_4m082l4q_.dbf
SQL> -- Drop old tablespaces, but keep data files in place
SQL> DROP TABLESPACE data_001
2 INCLUDING CONTENTS KEEP DATAFILES;
Tablespace dropped.
SQL> DROP TABLESPACE indx_001
2 INCLUDING CONTENTS KEEP DATAFILES;
Tablespace dropped.
SQL> -- Shred/remove old data files
SQL> HOST shred -n 200 -z -u /data01/oracle/db001/datafile/o1_mf_data_001_4m081w7m_.dbf
SQL> HOST shred -n 200 -z -u /data01/oracle/db001/datafile/o1_mf_indx_001_4m082l4q_.dbf
列表 3 删除剩余敏感数据的未加密虚副本需要一系列步骤才能解决很多相关性。
列表 3 中的最后一步针对特定的操作系统。在本例中,我演示了 Linux/Unix shred 实用程序的用法。其他您可能想探究的实用程序是 Linux/Unix wipe、scrub 和 srm 程序。
准备可能的重新生成密钥操作
我的客户还想做些准备以应对将来可能需要重新生成密钥的情况。为现有数据重新生成密钥意味着您要使用新密钥对现有数据重新加密。当您怀疑某人已经获得对现有表密钥的访问权限并且可能会在您的控制之外破解敏感数据时,可能需要执行该操作。
完成重新生成密钥操作的步骤与最初加密现有数据的步骤类似:删除目标列上的索引,重新生成该列数据的密钥,然后重建删除的索引。或者,如果您担心与使用以前的密钥加密的数据相对应的虚副本,可以重复将段移动到新表空间、删除旧表空间、然后清除旧表空间的数据文件的过程。
注:以我的经验,PCI 审计员会对重新生成万能加密密钥非常满意,因为该过程不需要访问任何数据,并且 PCI 标准不包含对 2 层密钥体系结构如 Oracle 的体系结构)的建议。从符合 PCI 的角度看,重新生成万能加密密钥应该足够了,而且 PCI 审计员无法强制具有几十亿行的公司将其业务关闭几天,仅仅只是为了重新生成密钥。
确定可能的查询计划更改
Oracle 文档、若干文章以及我阅读过的一些论坛帖子几乎都提到了有关加密列的数据后现有查询执行计划可能发生更改的概要信息以及某些具体信息。一般来说,相对于没有索引的列,在加密具有索引的列时,您必须注意执行 SQL 语句所发生的情况。当 Oracle 加密具有索引的列时,Oracle 还将加密相应的索引值。如果您花点时间考虑这个问题,就会清楚地发现以具有索引的数据为目标的相等谓词应该继续利用索引,但由于该索引值存储在索引中的方式,加密值的随机性质使得加密索引的范围扫描成本过高。列表 4 演示了这些详细描述的基本情况。
SQL> CONNECT app_001
Enter password:
Connected.
SQL> -- Create a plan table
SQL> @?/rdbms/admin/utlxplan.sql;
Table created.
SQL> -- Disable encryption of the credit card column
SQL> ALTER TABLE app_001.transactions
2 MODIFY (credit_card DECRYPT);
Table altered.
SQL> -- Ensure schema stats are current
SQL> EXEC dbms_stats.gather_schema_stats('app_001');
PL/SQL procedure successfully completed.
SQL> -- Display some representative data
SQL> COL credit_card FOR 9999999999999999;
SQL> SELECT * FROM app_001.transactions
2 WHERE rownum < 5;
TRANS_ID CREDIT_CARD
---------- -----------------
389 3469681098409570
390 3441050723354352
391 3485598407754404
392 3485458104610650
SQL> -- Enable tracing and explain plan output
SQL> SET AUTOTRACE ON EXPLAIN;
SQL> -- Demonstrate an equality predicate targeting the
SQL> -- encrypted column
SQL> SELECT * FROM app_001.transactions
2 WHERE credit_card = 3485458104610650;
TRANS_ID CREDIT_CARD
---------- -----------------
392 3485458104610650
Execution Plan
----------------------------------------------------------
Plan hash value: 32329967
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 14 | 5 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| TRANSACTIONS | 1 | 14 | 5 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | TRANSACTIONS_NDX1 | 1 | | 3 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("CREDIT_CARD"=3485458104610650)
SQL> -- Demonstrate a range predicate targeting the
SQL> -- encrypted column
SQL> SELECT * FROM app_001.transactions
2 WHERE credit_card BETWEEN 3499990000000000 AND 3499999999999999;
TRANS_ID CREDIT_CARD
---------- -----------------
4629 3499990987277941
18597 3499993250694089
13364 3499996558049599
79326 3499996616476145
60420 3499997873591732
24392 3499998608513414
97433 3499999831086288
72183 3499999977925392
8 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 32329967
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 42 | 6 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| TRANSACTIONS | 3 | 42 | 6 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | TRANSACTIONS_NDX1 | 3 | | 3 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("CREDIT_CARD">=3499990000000000 AND "CREDIT_CARD"<=3499999999999999)
SQL> -- Disable tracing and explain plan output
SQL> SET AUTOTRACE OFF;
SQL> -- Encrypt the column (and indexes)
SQL> ALTER TABLE app_001.transactions
2 MODIFY (credit_card ENCRYPT NO SALT);
Table altered.
SQL> -- Ensure schema stats are current
SQL> EXEC dbms_stats.gather_schema_stats('app_001');
PL/SQL procedure successfully completed.
SQL> -- Enable tracing and explain plan output
SQL> SET AUTOTRACE ON EXPLAIN;
SQL> -- Rerun previous queries, compare execution plans
SQL> SELECT * FROM app_001.transactions
2 WHERE credit_card = 3485458104610650;
TRANS_ID CREDIT_CARD
---------- -----------------
392 3485458104610650
Execution Plan
----------------------------------------------------------
Plan hash value: 32329967
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 14 | 5 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| TRANSACTIONS | 1 | 14 | 5 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | TRANSACTIONS_NDX1 | 1 | | 3 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("CREDIT_CARD"=3485458104610650)
SQL> SELECT * FROM app_001.transactions
2 WHERE credit_card BETWEEN 3499990000000000 AND 3499999999999999;
TRANS_ID CREDIT_CARD
---------- -----------------
60420 3499997873591732
4629 3499990987277941
18597 3499993250694089
13364 3499996558049599
24392 3499998608513414
79326 3499996616476145
72183 3499999977925392
97433 3499999831086288
8 rows selected.
Execution Plan
----------------------------------------------------------
Plan hash value: 1321366336
----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1250 | 17500 | 914 (2)| 00:00:11 |
|* 1 | TABLE ACCESS FULL| TRANSACTIONS | 1250 | 17500 | 914 (2)| 00:00:11 |
----------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(INTERNAL_FUNCTION("CREDIT_CARD")>=3499990000000000 AND
INTERNAL_FUNCTION("CREDIT_CARD")<=3499999999999999)
SQL> -- Disable tracing and explain plan output
SQL> SET AUTOTRACE OFF;
列表 4 仔细识别引用加密列数据的 SQL 语句,然后比较加密前后这些语句的执行计划,以查找是否存在任何更改。
我还想知道加密的开销是否会改变计划成本以及优化程序的选择,即使在文档建议不要这样做的情况下。为了确保我确切知道进行生产时关键任务应用程序所发生的情况,我在沙箱环境中进行了一些额外的工作。首先,我从各种自动负载信息库 (AWR) 快照中收集了一个使用频繁的 SQL 语句CPU、Gets 和 I/O)列表。然后,我比较了加密列前后每个 SQL 语句的查询执行计划。我的研究转向对多个基于相等的条件使用一个谓词的复杂查询,其中一个条件是以将要进行加密的列为目标。让我吃惊的是,在对列进行加密之后,该查询的执行计划发生了改变。遗憾的是,我无法在我的测试实验室中为本文复制这些结果,我仍然无法完全确定查询计划发生改变的原因。但我之所以在此处提到这种情况是想指出,在对生产系统进行更改之前,最好在测试环境中研究生产应用程序密钥查询的执行计划。如果我假设没有任何使用频繁的查询会发生改变,那么我们将对生产系统进行更改并且不得不勉强拼凑一个解决方案。
此处的教训是,在进行更改之前,您应该始终对这些事项进行测试,无论您在文档和其他来源中阅读了什么内容都是如此。
结论
使用 Oracle 的 TDE 特性加密新表与没有任何数据的表中的列或者现有表中的新列非常简单,原因是不存在任何需要担心的相关性。相反,加密现有列数据需要仔细研究并在您的沙箱环境中进行测试,然后才能在实际生产系统中实施您的计划,因为加密可能会影响很多相关性。
Steve Bobrowski 自 Oracle 数据库版本 5 开始一直使用该软件,曾就职于 Oracle;他还是 The Database Domain (dbdomain.com) 的创始人以及五本 Oracle 出版社出版的书籍的作者包括《Oracle 数据库 10g 快捷版上机操作》系列)。最近,Steve 担任着几个大型公司如 Computer Sciences Corporation、BEA Systems 和 Salesforce.com)的 SaaS 顾问和 SaaS 首席技术官。
- 深入解析Oracle数据库安全策略
- 网络中最基本的安全技术:数据加密技术
- 简析基于本地的Oracle安全扫描