Oracle TDE 表領域暗号化②|守れる範囲・守れない範囲:ユーザー表領域が暗号化済みなのに平文が残る場所はどこか

【連載】Oracle TDE 表領域暗号化
  1. 設計と基本構築
  2. 守れる範囲・守れない範囲(この記事)
  3. 鍵の管理
  4. SYSTEM・SYSAUX・UNDO・TEMPを暗号化

ユーザーの表を暗号化した。これで「ディスクを盗まれても読めない」は達成です(第1回で実証しました)。

でも、それで全部でしょうか。

データ本体を暗号化しても、Oracleは内部で UNDO(変更前データ)・TEMP(ソート溢れ)・統計(オプティマイザ)・AWR(SQL履歴) という“波及先”にデータを書きます。 この波及先に平文が残っていれば、本体をいくら暗号化しても意味は半減します。

「UNDOやTEMPはどうなるのか」「統計やAWRは平文のままでは」——気になる波及先を、第2回では1つずつ実機で strings | grep して、ユーザー表領域を暗号化して守れる範囲と守れない範囲を切り分けます。結果は、直感とは半分ずれていました。


この記事について

本記事は、Oracle Database 19c の TDE 表領域暗号化を実機で検証する連載の第2回です。 第1回(設計と基本構築)で「データ本体は守れる」ことを実証しました。本記事は、その守れる範囲と守れない範囲——暗号化が及ぶ所と及ばない所——を実機で確かめます。

連載の全体マップ:

記事タイトル内容
第1回設計と基本構築——盗まれたデータファイルを読ませない脅威モデル・方式設計・盗難実証(before)・ウォレット/鍵構築・守られる実証(after)
第2回(本記事)守れる範囲・守れない範囲——ユーザー表領域が暗号化済みなのに平文が残る場所はどこかUNDO/TEMP・統計(SYSTEM)・AWR(SYSAUX)への平文コピーの検証
第3回鍵の管理——失えばデータは二度と戻らない既存表領域のオンライン暗号化移行・ウォレットファイルはいつ必要か・鍵バックアップとDR
第4回SYSTEM・SYSAUX・UNDO・TEMPを暗号化——統計・AWR経由の平文漏洩を実機で塞ぐ統計・AWRの平文漏洩を防ぐbefore/after・UNDO/TEMPの暗号化・REDOに平文が残る検証

想定読者:

  • TDEを導入した/導入したいが、「表を暗号化すれば本当に安心なのか」が気になる方
  • UNDO/TEMP・統計・AWR まで含めて、暗号化が及ぶ範囲を具体的に確かめたい方
  • TDEの守備範囲と運用上の穴を具体的に押さえたい方

前提知識:

  • 第1回の内容(表領域暗号化の基本・strings | grep での覗き方)
  • SQL*Plus・Linux の基本操作

第1回がまだの方は、先に「設計と基本構築」を読むと、本記事の検証がつながります。


検証環境

項目
VMdb-node(Oracle Linux 8.9)
DBバージョンOracle Database 19c Enterprise Edition 19.28.0.0.0
構成非CDB(non-multitenant)
ORACLE_SIDorcl19u

第1回で構築した環境をそのまま使います。暗号化表領域 enc_ts(表 hr_secret_enc)と、平文表領域 plain_ts を比較用に使います。

ライセンス前提: TDE(表領域暗号化)は Oracle Enterprise Edition + Advanced Security オプションの機能です。Standard Edition (SE2) では利用できません。本連載はこの前提で構築しています。


1. UNDO/TEMP に平文は残るか — 実は自動で守られる

まず確かめるのは、暗号化したユーザー表領域から波及するデータです。

更新で生まれる UNDO(変更前イメージ) と、ソート溢れで書かれる TEMP に、平文のまま残らないか——実機で確認します。

進め方はシンプルです。まず非暗号化の plain_ts で「平文が UNDO/TEMP に残る」ことを見せ、次に暗号化の enc_ts で同じ操作をして、消えるかを見ます。

UNDO — 更新前イメージに平文は残るか

UNDO に入る量は操作で違います。INSERT は行の値を UNDO に残さず、UPDATE は変更した列の旧値だけ。対して DELETE は行を丸ごと(全列の値ごと)UNDO へ退避します——ロールバックで行をそっくり戻せるように。つまり、平文が漏れるなら DELETE が最も出やすい。検査にはこれを使います。

① 平文表 hr_secret_plainplain_ts)で。 目印を2000行入れて削除し、UNDO(undotbs01.dbf)を grep します。

SQL> INSERT INTO hr_secret_plain SELECT LEVEL,'UNDO-PLAIN-0615','4111-0000-0000-0000'
  2    FROM dual CONNECT BY LEVEL<=2000;
SQL> COMMIT;
SQL> DELETE FROM hr_secret_plain WHERE name='UNDO-PLAIN-0615';   -- 2000行まるごとUNDOへ
SQL> COMMIT;
SQL> ALTER SYSTEM FLUSH BUFFER_CACHE;
SQL> ALTER SYSTEM CHECKPOINT;
$ strings /u01/app/oracle/oradata/ORCL19U/undotbs01.dbf | grep -c -E 'UNDO-PLAIN-0615|4111-0000'
2000

2000件ヒット。 平文表のデータは、削除すると UNDO にそのまま平文で残ります。これが「UNDO から漏れる」の正体です(非暗号化ならこうなる)。

② 暗号表 hr_secret_encenc_ts)で、まったく同じ操作を。 同じ undotbs01.dbfgrep すると——

SQL> INSERT INTO hr_secret_enc SELECT LEVEL,'UNDO-ENC-0615','4299-0000-0000-0000'
  2    FROM dual CONNECT BY LEVEL<=2000;
SQL> COMMIT;
SQL> DELETE FROM hr_secret_enc WHERE name='UNDO-ENC-0615';
SQL> COMMIT;
SQL> ALTER SYSTEM FLUSH BUFFER_CACHE;
SQL> ALTER SYSTEM CHECKPOINT;
$ strings /u01/app/oracle/oradata/ORCL19U/undotbs01.dbf | grep -c -E 'UNDO-ENC-0615|4299-0000'
0

0件。 同じ UNDO 表領域なのに、暗号化表のデータは平文で残りません。UNDO の暗号化はソース表領域に従う——暗号化表の更新なら、その UNDO も暗号化される、ということです。

TEMP — ソート溢れに平文は残るか

TEMP を見るには、まずソートを確実にディスクへ溢れさせる必要があります。既定(workarea_size_policy=AUTO)では PGA が自動調整され、この程度のデータはメモリ内で収まり、TEMP に書かれません。そこで MANUAL に切り替え(AUTO のままだと sort_area_size の指定は無視されます)、sort_area_size を 64KB に絞って、1回のソートに使える作業領域をわざと足りなくします

① 平文表 hr_secret_plain で。 目印を2000行入れてソート溢れさせ、TEMP(temp01.dbf)を grep します。

SQL> INSERT INTO hr_secret_plain SELECT 3000+LEVEL,'TEMP-PLAIN-0615','4111-7777-7777-7777'
  2    FROM dual CONNECT BY LEVEL<=2000;
SQL> COMMIT;
SQL> ALTER SESSION SET workarea_size_policy=MANUAL;
SQL> ALTER SESSION SET sort_area_size=65536;
SQL> SELECT name FROM hr_secret_plain ORDER BY name;   -- 大量ソートでTEMPへ溢れさせる
$ strings /u01/app/oracle/oradata/ORCL19U/temp01.dbf | grep -E 'TEMP-PLAIN-0615|4111-7777' | head
TEMP-PLAIN-0615   4111-7777-7777-7777
TEMP-PLAIN-0615   4111-7777-7777-7777
TEMP-PLAIN-0615   4111-7777-7777-7777
...(多数)

多数ヒット。 ソート溢れで TEMP に書き出された平文が、そのまま読めます。

② 暗号表 hr_secret_enc で、まったく同じ操作を。 同じ temp01.dbfgrep すると——

SQL> INSERT INTO hr_secret_enc SELECT 3000+LEVEL,'TEMPLEAK0615ABC','4277-0000-0000-0000'
  2    FROM dual CONNECT BY LEVEL<=2000;
SQL> COMMIT;
SQL> ALTER SESSION SET workarea_size_policy=MANUAL;
SQL> ALTER SESSION SET sort_area_size=65536;
SQL> SELECT name FROM hr_secret_enc ORDER BY name;
$ strings /u01/app/oracle/oradata/ORCL19U/temp01.dbf | grep -c -E 'TEMPLEAK0615ABC|4277-0000'
0

0件。 平文側では多数ヒットしたのと同じ操作なのに、暗号化表のデータは TEMP に平文を残しません。

補足(ソートが本当に TEMP へ溢れたか)SELECT n.name,m.value FROM v$mystat m JOIN v$statname n USING(statistic#) WHERE n.name IN ('sorts (memory)','sorts (disk)');sorts (disk) を見ると 0 でない=ディスクを使ったソートがある、と分かります(セッション累計なので目安)。確実に「TEMP へ書かれた」ことは、上の平文側で多数ヒットした事実が示しています。

結論:UNDO/TEMP は守られる

平文表では UNDO に 2000件・TEMP に 多数の平文が残り、暗号化表では同じ操作でどちらも 0件。並べて見れば、「0 は暗号化が理由」だと確定します。

押さえどころ:19c の TDE“表領域”暗号化では、暗号化表に紐づく UNDO/REDO/TEMP も Oracle が自動で暗号化します。UNDO/TEMP から平文が漏れるのは、TDE“列”暗号化を使った場合や、機密が“非暗号化表領域”に置かれている場合の話。表領域ごと暗号化していれば、UNDO/TEMP も守られます。

これは公式ドキュメントとも一致します。

「暗号化された表領域の機密データから生成されたメタデータ、UNDO および TEMP は、すでに自動的に暗号化されています。そのため、UNDO および TEMP の暗号化はオプションです。」 (出典:Oracle Advanced Security Guide「Encryption Conversions for Tablespaces and Databases」)

つまり、at-rest を守る条件は「ソース表領域 か UNDO/TEMP表領域 のどちらかが暗号化」。両方とも非暗号のときだけ漏れます。

なお、REDOログへの書き込みも同様にソースの表領域に従うことを、第4回で実測しています(REDOはUNDO・TEMPと違い、書き先側を暗号化する対策が存在しません)。


2. 運用の穴:新しい表領域の“暗号化漏れ”を塞ぐ — ENCRYPT_NEW_TABLESPACES

UNDO/TEMP は自動で守られる。では、運用でいちばん起きやすい漏れは何か——それは「新しい表領域を作るとき、ENCRYPT 句を書き忘れる」ことです。

これを防ぐのが ENCRYPT_NEW_TABLESPACES=ALWAYS書き忘れても自動で暗号化してくれます。実機で確かめます。

SQL> SHOW PARAMETER encrypt_new_tablespaces
NAME                     TYPE   VALUE
------------------------ ------ ----------
encrypt_new_tablespaces  string CLOUD_ONLY

SQL> ALTER SYSTEM SET encrypt_new_tablespaces=ALWAYS SCOPE=BOTH;

SQL> -- ★ENCRYPT句を「あえて書かず」に表領域を作成(=書き忘れを再現)
SQL> CREATE TABLESPACE noenc_ts DATAFILE '/u01/app/oracle/oradata/ORCL19U/noenc_ts01.dbf' SIZE 50M;
SQL> SELECT tablespace_name, encrypted FROM dba_tablespaces WHERE tablespace_name='NOENC_TS';

TABLESPACE_NAME ENC
--------------- ---
NOENC_TS        YES

ENCRYPT 句を書いていないのに ENCRYPTED=YES書き忘れても暗号化される=穴を塞げました。

自動暗号化の既定は AES128 — AES256は明示でそろえる

ここで暗号アルゴリズム(鍵長 AES128 か AES256 か)に注目します。ALWAYS の自動暗号化は、既定が AES128。第1回で「AES256を明示する」と決めたのに、自動だと128で作られてしまいます。

SQL> SELECT ts#, encryptionalg FROM v$encrypted_tablespaces ORDER BY ts#;

       TS# ENCRYPTIONALG
---------- -------------
         8 AES256          ← enc_ts(USING 'AES256' 明示)
         9 AES128          ← noenc_ts(ALWAYS の自動・既定128)
        10 AES256          ← enc256_ts(USING 'AES256' 明示)

noenc_ts(自動)だけ AES128USING 'AES256' を明示した表領域は256です。

既定を256に変えるパラメータ TABLESPACE_ENCRYPTION_DEFAULT_ALGORITHM を探しましたが、19.28 には存在しませんでしたSHOW PARAMETER で出力なし)。

設計の二段構えENCRYPT_NEW_TABLESPACES=ALWAYS で「明示忘れでも平文にはしない」安全網を張りつつ、強度は USING 'AES256' の明示を運用ルール化する。19.28では既定アルゴリズムを256へ変える手段がないので、この二段構えが現実解です。


3. 暗号化が及ばない場所① — 統計(SYSTEM)に列の実値が平文で残る

UNDO/TEMP は守られた。では、ユーザー表領域の暗号化が及ばない波及先はどこか。 1つ目はオプティマイザ統計です。

暗号化表でも、DBMS_STATS でヒストグラムを取ると、列の実値(low/high値・ヒストグラムの境界値)が、非暗号化の辞書(SYSTEM表領域)に平文でコピーされます。

ユーザー表領域の暗号化が及ばない経路です。

区別しやすい機密値を入れて、統計を取ります。

SQL> INSERT INTO hr_secret_enc VALUES (101,'STATLEAK-TANAKA','4811-1111-1111-1111');
SQL> INSERT INTO hr_secret_enc VALUES (102,'STATLEAK-SUZUKI','4822-2222-2222-2222');
SQL> INSERT INTO hr_secret_enc VALUES (103,'STATLEAK-SATO','4833-3333-3333-3333');
SQL> COMMIT;
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER,'HR_SECRET_ENC',method_opt=>'FOR ALL COLUMNS SIZE 254');
SQL> SELECT column_name, endpoint_actual_value FROM user_tab_histograms
  2    WHERE table_name='HR_SECRET_ENC' AND endpoint_actual_value IS NOT NULL;

COLUMN_NAME ENDPOINT_ACTUAL_VALUE
----------- ---------------------
NAME        ENCRYPTED-SAFE-0614
NAME        STATLEAK-SATO
NAME        STATLEAK-SUZUKI
NAME        STATLEAK-TANAKA

ヒストグラムに、暗号化表の NAME 列の実値がそのまま入っています。これは辞書(SYSTEM)に書かれます。ディスクを覗くと——

$ strings /u01/app/oracle/oradata/ORCL19U/system01.dbf | grep -c -E 'STATLEAK|4811-1111|4822-2222|4833-3333'
5

SYSTEM(system01.dbf)に 5 件ヒット。 表を暗号化したのに、統計経由で実値が平文で残りましたFOR ALL COLUMNS で全列の統計を取ったので、NAME 列のヒストグラム実値に加えて、CARD 列の最小・最大値もカード番号として SYSTEM に残ります(grep がカード番号も拾うのはこのため)。UNDO/TEMP(自動保護)とは対照的に、統計は守られません

補足:初回 gather では SYSAUX(統計履歴)は 0 でしたが、2回目以降の gather では旧統計が SYSAUX の履歴(WRI$_OPTSTAT)へ移り、そこにも実値が残ります


4. 暗号化が及ばない場所② — AWRのSQLテキスト(SYSAUX)にリテラルが残る

2つ目の波及先は AWR(自動ワークロードリポジトリ)。SQLに機密値をリテラルで埋め込むと、その SQL 文が AWR の履歴(SYSAUX表領域の DBA_HIST_SQLTEXT)に全文で保存され、平文で残ります。

カード番号をリテラルに含む SQL に /* leaktest9 */ という目印のコメントを付けて実行し、AWRに記録させます(/*+ ... */ のヒントとは違い、ただのコメントです。SQL文はコメント込みで保存されるため、後から自分の検証SQLだけを拾う目印になります)。

SQL> SELECT /* leaktest9 */ COUNT(*) FROM hr_secret_enc WHERE card='4811-1111-1111-1111';
SQL> -- 軽い一過性SQLはAWRに載りにくいので ADD_COLORED_SQL +手動スナップショットで強制的に記録
SQL> SELECT sql_id, SUBSTR(sql_text,1,80) AS txt FROM dba_hist_sqltext WHERE sql_text LIKE '%leaktest9%';

SQL_ID        TXT
------------- --------------------------------------------------------------
xxxxxxxxxxxxx SELECT /* leaktest9 */ COUNT(*) FROM hr_secret_enc WHERE card='4811-1111-1111-1111'

AWRの履歴に、カード番号入りのSQL文がそのまま保存されました。ディスクを覗くと——

$ strings /u01/app/oracle/oradata/ORCL19U/sysaux01.dbf | grep -c -E '4811-1111'
1

SYSAUX(sysaux01.dbf)にカード番号がヒット。 暗号化表のデータが、SQL文の形で平文で残りました。

守りの本命:バインド変数

ここが運用の急所です。機密値はリテラルで書かず、バインド変数を使う——これで、SQL文(V$SQL・共有プール・AWRの DBA_HIST_SQLTEXT)にカード番号が残りません。リテラルSQLの最大の弱点を、これ一つで塞げます。

ただし完全に痕跡ゼロではありません:バインド値そのものは、V$SQL_BIND_CAPTURE や AWR の DBA_HIST_SQLBIND(bindキャプチャ)にサンプリングで記録され得ます。それでも、SQLテキストに必ず焼き込まれるリテラルより、漏れ口は格段に小さい——だからバインド変数が第一の対策です。

-- ❌ リテラル(SQL文に値が残る)
SELECT COUNT(*) FROM hr_secret_enc WHERE card='4811-1111-1111-1111';

-- ✅ バインド変数(SQL文に値が残らない)
SELECT COUNT(*) FROM hr_secret_enc WHERE card=:card;

注意:AWRへの記録は top-N(負荷の高いSQL)依存で、軽い一過性のSQLは通常は載りません(本検証は ADD_COLORED_SQL で強制的に記録させました)。「AWRに載れば漏れる」が正確です。そしてリテラルSQL + AWR(Diagnostics Pack) が危険な組み合わせ。バインド変数がいちばん効く対策です。


まとめ — 守れる範囲・守れない範囲

第2回で実機検証した、ユーザー表領域を暗号化したときに守れる範囲・守れない範囲の一覧です。

項目結果一次情報
UNDO🟢 守る(自動暗号化)非暗号ソース=2000 / 暗号ソース=0
TEMP🟢 守る(自動暗号化)非暗号=大量 / 暗号=0(ソート溢れ確認済み)
新規表領域の書き忘れ🟢 塞げる(ただし既定AES128)ALWAYSで自動暗号化/USING 'AES256'明示が必須
統計(SYSTEM)🔴 守らないヒストグラムに実値 → system01.dbf に 5 件
AWR(SYSAUX)🔴 守らないリテラルSQL → sysaux01.dbf にヒット

ユーザー表領域の暗号化は“データ本体”を守りますが、“実値のコピー”が出ていく波及先(統計・AWR)は守りません。 こうして並べると、対策の打ち所が見えます。

  • UNDO/TEMP:表領域ごと暗号化していれば自動で守られる(多層防御として暗号化UNDO/TEMPも選べる)
  • 新規表領域ENCRYPT_NEW_TABLESPACES=ALWAYSUSING 'AES256'明示
  • 統計(SYSTEM):SYSTEM/SYSAUXの暗号化(第4回で実施)、または機密列にヒストグラムを作らない(method_opt調整)
  • AWR(SYSAUX)機密値はバインド変数(リテラルを残さない)が最優先

なお、SYSTEM/SYSAUX 自体を暗号化すれば、統計・AWR も at-rest では守れます(「書き先の表領域が暗号化なら平文は残らない」——「UNDO/TEMP に平文は残るか」の節と同じ理屈です。ただし SYSTEM/SYSAUX 暗号化そのものは本記事では未実施)。SYSTEM の暗号化はDB全体に影響が及ぶ変更なので、十分な検証が要ります。

この続きは第4回で扱っています。SYSTEM・SYSAUX・UNDO・TEMP を実際に暗号化し、統計・AWRの平文が消えるbefore/afterと、REDOログに平文が残る検証まで実測しました。

第3回は、運用フェーズの核心——既存DBを止めずに暗号化する移行ウォレットファイルはいつ必要か鍵を失っても戻せるか(DR)——を実機で検証します。→ 鍵の管理:失えばデータは二度と戻らない