(KO) CUBRID TDE — 마스터 키로 래핑된 DEK 위에서 동작하는 투명 페이지 단위 암호화
목차
학술적 배경
섹션 제목: “학술적 배경”페이지 단위 스토리지 엔진이 디스크에 한 바이트라도 쓰기 전에 파일 시스템 접근권을 가진 공격자로부터 데이터를 방어하려면 네 가지 질문에 먼저 답해야 한다 — 어떤 cipher 인가, 어떤 키 인가, 어떤 nonce 인가, 키는 어디에 사는가. 각 답은 다른 모 든 계층 — 버퍼 풀, WAL, 복구 코드, 백업 도구, DBA 운영 런북 — 까지 그대로 새어 나간다. 그래서 멀리서 보는 것보다 설계 공 간은 훨씬 좁다.
위협 모델 (threat model). Encryption-at-rest 는 데이터 파 일, WAL, 백업, 디스크 이미지를 복사할 수 있는 공격자를 막아 주 지만, 실행 중인 프로세스의 메모리를 읽을 수 있는 공격자는 막 지 못한다. 악의적인 DBA, 호스트에서 동작 중인 메모리 스크래핑 멀웨어, cipher 구현의 사이드 채널 같은 위협은 이 모델 밖이다. Database Internals (Petrov, 4장 “Transaction Processing and Recovery”, 그리고 마지막 장의 encryption-at-rest 단상) 는 TDE 를 파일 시스템 단위 의 방어를 페이지 단위로 다시 짠 것으로 정리한다. 엔진이 실제로 필요한 페이지만 복호화할 수 있게 하기 위해서다.
Cipher 선택. AES (FIPS 197) 가 어디서나 기본인 이유는 하 드웨어 지원이다. x86 의 AES-NI, ARMv8 의 Crypto Extensions, 그리고 OpenSSL 안의 대응 intrinsic 이 페이지당 오버헤드를 한 바이트당 몇 사이클 수준으로 떨어뜨린다. 운영 모드 (mode of operation) 선택은 cipher 선택만큼이나 중요하다. NIST SP 800-38A 가 옵션을 정리한다.
- CBC 는 블록을 chaining 한다. 마지막 블록이 직전 블록 모 두에 의존한다. 확산 (diffusion) 은 좋지만 페이지 안에서 암 호화 경로가 순차적이고, 암호문 한 비트가 뒤집히면 복호화 시 한 블록 전체가 쓰레기 평문으로 번진다. 페이지가 자체 checksum 을 가지므로 그것 자체는 괜찮다. 다만 페이지 일부만 다시 쓰 는 동작이 있어야 한다면 좋지 않다.
- CTR 은
nonce || counter를 암호화한 키 스트림을 평문에 XOR 한다. 페이지 안에서 암호화는 완전히 병렬이고, 암호문 길 이가 평문과 같다 (padding 이 없다). in-place 부분 재기록도 자명하게 지원된다. 단 한 가지 절대 규칙은 (key, nonce) 쌍 이 절대 반복되어선 안 된다 는 것이다. 같은 쌍에서 만들어진 두 암호문을 XOR 하면 두 평문의 XOR 이 그대로 드러나기 때문이 다. CUBRID 는 CTR 을 골랐다.
키 계층 (key hierarchy). 모든 페이지를 사용자의 마스터 키 하나로 직접 암호화하는 평면 구조는 키 회전 (rotation) 에 적대 적이다. 키를 회전한다는 것이 데이터베이스의 모든 페이지를 다 시 암호화한다는 의미가 되기 때문이다. 교재의 해법은 Oracle TDE 가 대중화한 두 단계 계층 이다.
- Master Key (MK). DBA 가 시작 시 제공하거나, 키 관리 서 버가 공급한다. 서버 동작 중에는 휘발성 메모리에만 산다.
- 하나 이상의 Data Encryption Key (DEK). 암호화 용도별로 하나씩 둔다 (테이블 데이터, 임시 영역, WAL). 데이터베이스 를 만들 때 한 번 생성되며, 디스크에는 항상 MK 로 래핑 (wrapped) — 즉 암호화 — 된 형태로만 저장된다.
이 구조 위에서 MK 를 회전한다는 것은, 래핑된 DEK 를 옛 MK 로 풀고 새 MK 로 다시 래핑해서 그 결과 blob 을 디스크에 다시 쓰 는 일이 된다. 데이터 페이지 자체에는 손이 가지 않는 것이 핵심 이다. DEK 자체를 회전한다는 것은 그 DEK 에 의존하는 모든 페 이지를 다시 암호화한다는 뜻이 되므로, 엔진은 정말 어쩔 수 없 는 경우가 아니면 DEK 회전은 피한다.
페이지별 nonce. CTR 모드에서 nonce 유일성 invariant 는 키 의 입자성을 결정하는 단단한 제약이다. 단일 DEK 가 모든 페이지 를 암호화한다면, nonce 는 데이터베이스 전체에서 전역적으로 유 일해야 한다. 자연스러운 선택지는 다음과 같다.
- LSN / LSA — 페이지의 가장 최근 로그 시퀀스 번호. 영구 페이지에서는 모든 수정이 페이지 flush 전에 새 로그 레코드를 쓰기 때문에 (WAL) 항상 새로운 값이 된다.
- 페이지 식별자 —
(volid, pageid)는 공간상으로는 유일하 지만, 같은 페이지가 다시 쓰여도 변하지 않는다. 키가 고정된 상태에서 이 식별자만 nonce 로 쓰면, 다른 내용으로 다시 쓰는 순간 유일성 invariant 가 깨진다. - 단조 증가 카운터 — LSA 가 의미가 없는 페이지 (temp 파일 은 LSA 가 sentinel 값으로 고정되어 있다) 에서는 사용 가능하 지만, 원자 증분이 있어야 한다.
- WAL 의 논리 페이지 ID — 데이터베이스마다 단조 증가하는 로그 페이지 번호.
CUBRID 의 선택은 영구 페이지에는 LSA, 임시 페이지에는 원자 카운터, 로그 페이지에는 논리 페이지 ID 다. 세 종류의 DEK 위 에 세 가지 정책을 얹은 셈이다.
래핑된 DEK 는 어디에 사는가. 두 진영이 있다.
- 데이터베이스 내부. PostgreSQL 의 개발 중인 TDE, MySQL InnoDB TDE, 그리고 CUBRID 는 모두 래핑된 DEK 를 데이터베이 스 내부의 시스템 테이블이나 전용 heap 에 저장한다. trade-off 는 데이터베이스를 백업하면 래핑된 DEK 도 함께 백업된다는 점 이지만, 그것을 푸는 데에는 여전히 마스터 키가 필요하다는 점 이 안전 장치다.
- 데이터베이스 옆. 마스터 키 파일 자체는 어딘가에 있어야 한
다. 하드웨어 모듈 (HSM / KMS) 이거나, DBA 소유의 OS 파일이
거나, 어쨌든 데이터베이스 파일과는 분리된 자리다. CUBRID 는
OS 파일
<db>_keys를 쓴다. 기본 위치는 데이터베이스 파일 과 같은 디렉터리이고,tde_keys_file_path로 변경할 수 있 다.
성능 예산 (performance budget). AES-NI 를 쓰는 AES-256-CTR 은 코어당 1 GB/s 정도의 처리량을 낸다. 16 KB 페이 지 한 장의 암호화 또는 복호화가 대략 16 마이크로초의 CPU 시간 을 가져간다. 버퍼 풀 hit 가 지배적인 워크로드에서는 사실상 보 이지 않고, 디스크에 매번 flush 하는 쓰기 위주 워크로드에서는 전체 쓰기 경로의 5–10 % 정도가 더 든다. AES-NI 가 없거나 페이 지가 더 작으면 비용이 빠르게 나빠진다. 비용은 전적으로 I/O 경 로에 떨어진다. 이미 복호화되어 있는 페이지에 대한 버퍼 풀 hit 는 무료다.
DBMS 공통 설계 패턴 (Common DBMS Design)
섹션 제목: “DBMS 공통 설계 패턴 (Common DBMS Design)”다섯 개 엔진 (Oracle, SQL Server, MySQL InnoDB, PostgreSQL, CUBRID) 이 같은 이유로 폭넓게 비슷한 선택을 한다. 모두 OS 파 일 시스템과 고정 크기 페이지 추상 사이에 끼어 있어서, 암호화 경계가 자연스럽게 “버퍼 풀과 디스크의 frontier 를 가로지르는 바이트” 위에 놓인다.
Oracle TDE — 두 단계 계층을 2005 년에 처음 도입했다. 각 테이블스페이스 가 자기 DEK 를 가지며, 모든 DEK 는 Oracle Wallet (PKCS #12 파일) 또는 HSM 에 저장된 마스터 키로 래핑된 다. CTR 모드 선택은 더 나중에 들어왔고, 초기 버전은 CBC 를 썼 다. 입자성은 테이블스페이스 단위라, 표 단위 암호화는 native 하게 지원되지 않는다. 민감한 표를 별도 테이블스페이스로 분리 하는 식의 운영으로 보통 해결한다.
SQL Server TDE — 세 단계 계층이다. Service Master Key (SMK, 머신 종속) → Database Master Key (DMK, 데이터베이스별) → Database Encryption Key (DEK) 로 래핑이 이어진다. 알고리즘 은 AES-256 이다 (별도 기능인 Always Encrypted 는 클라이언트 드라이버 안에서 컬럼 단위로 암호화한다). 입자성은 데이터베이 스 단위 — DB 전체 혹은 무 (nothing) 다.
MySQL InnoDB TDE — 테이블스페이스별 DEK 를 keyring 플러그 인 (file, AWS KMS, HashiCorp Vault 등) 이 관리하는 마스터 키 가 래핑한다. 기본 알고리즘은 AES-256-CBC, 페이지별 IV 는 테이 블스페이스 ID 와 페이지 번호에서 유도된다. 래핑된 DEK 는 테이 블스페이스 헤더에 산다. 마스터 키를 회전해도 테이블스페이스별 키는 다시 래핑될 뿐 페이지 데이터는 손대지 않는다.
PostgreSQL TDE — 본 트리에서는 역사적으로 지원되지 않았 다. Cybertec / EnterpriseDB 의 TDE Patch 가 클러스터 단위 암호화를 목표로 진행 중이며, passphrase 에서 유도된 단일 DEK 와 WAL 용 별도 cluster file encryption key (CFEK) 를 둔다. 2026 년 초 시점에서도 upstream 설계는 여전히 유동적이다. Percona, Cybertec 등의 fork 가 프리릴리스 TDE 를 먼저 제공한 다.
CUBRID 는 MySQL InnoDB 나 Oracle 과 같은 공간에 자리한다.
DB 별로 세 개의 DEK (perm / temp / log), 데이터베이스마다 마
스터 키 하나, AES-256-CTR (또는 한국 컴플라이언스용
ARIA-256-CTR), 그리고 마스터 키 파일은 데이터베이스 트리 밖
에 둔다는 선택이다. 입자성 다이얼은 파일 단위 — 즉 heap 단
위, B-tree 단위 — 로 설정되며 SQL CREATE TABLE ... ENCRYPT=AES 로 노출된다. 구현은 tde.c 약 1700 줄과 페이지
버퍼, 로그 페이지 버퍼, 파일 매니저, 부트 코드의 얇은 후크들
로 이루어져 있다.
CUBRID의 구현
섹션 제목: “CUBRID의 구현”키 계층
섹션 제목: “키 계층”CUBRID 는 두 단계를 쓴다. 마스터 키 하나가 Data Key Set 을 래 핑하고, 두 번째 단계는 세 개의 독립적인 DEK 로 다시 갈라진다.
// TDE_DATA_KEY_SET — src/storage/tde.htypedef struct tde_data_key_set{ unsigned char perm_key[TDE_DATA_KEY_LENGTH]; // permanent data pages unsigned char temp_key[TDE_DATA_KEY_LENGTH]; // temporary file pages unsigned char log_key[TDE_DATA_KEY_LENGTH]; // WAL pages} TDE_DATA_KEY_SET;세 DEK 모두 256 비트 길이 (TDE_DATA_KEY_LENGTH = 32) 이며,
마스터 키도 마찬가지다 (TDE_MASTER_KEY_LENGTH = 32). 데이터
베이스 생성 시점에 tde_create_dk() 가 OpenSSL 의
RAND_bytes() 를 직접 호출해 한 번 생성한다. 유도
(derivation) 나 passphrase 는 없다. DEK 를 셋으로 쪼개는 이유
는 nonce 정책 때문이다. 페이지 종류마다 nonce 로 쓸 수 있
는 식별자가 다르고, 종류별로 별개의 DEK 를 부여하면 가상의
nonce 재사용 버그가 한 종류의 페이지로 격리된다.
이 계층을 디스크 위에 구체화하는 레코드가 TDE_KEYINFO 다.
// TDE_KEYINFO — src/storage/tde.htypedef struct tde_keyinfo{ int mk_index; // index in master-key file time_t created_time; // of the master key time_t set_time; // last MK change on this DB unsigned char mk_hash[TDE_MASTER_KEY_LENGTH]; // SHA-256(MK), for validation unsigned char dk_perm[TDE_DATA_KEY_LENGTH]; // wrapped: enc_MK(perm_key) unsigned char dk_temp[TDE_DATA_KEY_LENGTH]; // wrapped: enc_MK(temp_key) unsigned char dk_log[TDE_DATA_KEY_LENGTH]; // wrapped: enc_MK(log_key)} TDE_KEYINFO;이 레이아웃에서 짚을 점이 셋이다.
- DEK 는 래핑된 형태로만 저장된다. 푸는 데에는 MK 가 필요 하다. 평문 형태로는 절대 디스크에 닿지 않는다.
- MK 는 해시 로만 저장된다. 후보 MK 가 들어왔을 때 “이 MK 가 이 DEK 들을 래핑한 그 MK 와 일치하는가?” 를 검증하기에 해시면 충분하며, MK 자체를 저장할 필요가 없다. 이것이 데이 터베이스를 여는 흐름에서 잘못된 MK 를 거절하는 근거다.
mk_index는 마스터 키 파일<db>_keys안의 실제 MK 바이 트 스트림을 가리키는 역포인터다.
TDE_KEYINFO blob 자체는 데이터베이스 내부의 작은 CUBRID heap
에 산다. MVCC 조정을 피하려고 손으로 빚은 형태다
(tde_initialize() 는 vacuum_rv_check_at_undo() 의 UNDO 도
중 레코드 조정을 막기 위해 일부러 더미 repid_and_flag_bits
int 를 레코드 앞에 prepend 한다). 그 heap 의 HFID 는
boot_Db_parm.tde_keyinfo_hfid 에 보관되어 재시작 시
tde_cipher_initialize() 로 전달된다.
마스터 키 파일은 기본적으로 <db_full_name>_keys 위치에
(tde_keys_file_path 가 설정되어 있다면 그 경로 아래에) 자리
한다. 모드 0600 으로 열리며, 매직 헤더 다음에 최대 128 개의
고정 크기 슬롯이 이어진다.
// TDE_MK_FILE_ITEM — src/storage/tde.htypedef struct tde_mk_file_item{ time_t created_time; // -1 if slot is invalid unsigned char master_key[TDE_MASTER_KEY_LENGTH];} TDE_MK_FILE_ITEM;
#define TDE_MK_FILE_ITEM_COUNT_MAX 128슬롯 0 은 매직을 건너뛴 CUBRID_MAGIC_MAX_LENGTH offset 에서
시작한다. created_time == -1 인 슬롯은 삭제된 것으로 취급된
다. tde_delete_mk() 는 timestamp 를 -1 로 덮어쓰지만 파일을
줄이지는 않는다. tde_add_mk() 는 첫 번째로 만나는 삭제된 슬
롯을 채우거나, 빈 슬롯이 없으면 끝에 append 한다.
graph TD
DBA[DBA passphrase / KMS] -- 기록 --> MKF[<db>_keys 파일<br/>모드 0600<br/>최대 128 MK 슬롯]
MKF -- 슬롯 mk_index의 32B MK --> MK[메모리 안의 Master Key<br/>32 바이트]
MK -- AES-256-CTR / ARIA-256-CTR --> WRAP{wrap}
PERM[perm_key 32B] -.DB 생성 시 생성.-> WRAP
TEMP[temp_key 32B] -.DB 생성 시 생성.-> WRAP
LOG[log_key 32B] -.DB 생성 시 생성.-> WRAP
WRAP --> KI[TDE_KEYINFO heap 레코드<br/>데이터베이스 내부]
KI -- mk_index --> MKF
KI -- mk_hash --> MK
MK -- 재시작 시 unwrap --> CIPHER[tde_Cipher.data_keys<br/>평문 DEK<br/>프로세스 메모리 한정]
CIPHER --> ENC[페이지 암호화 / 복호화]
암호화 모드와 IV 유도
섹션 제목: “암호화 모드와 IV 유도”cipher 선택은 tde_encrypt_internal() 안의 알고리즘별 스위치
한 번으로 끝난다.
// tde_encrypt_internal — src/storage/tde.cswitch (tde_algo) { case TDE_ALGORITHM_AES: cipher_type = EVP_aes_256_ctr (); break; case TDE_ALGORITHM_ARIA: cipher_type = EVP_aria_256_ctr (); break; case TDE_ALGORITHM_NONE: default: assert (false); goto cleanup; }두 알고리즘 모두 256 비트 키와 CTR 모드를 쓴다. ARIA 는 한국
표준 (KS X 1213) cipher 로, 한국의 금융·정부 환경에서 KISA 컴
플라이언스를 만족시키기 위해 함께 제공된다. tde.h 상단의
TDE_DK_ALGORITHM 매크로는 DEK 래핑용 알고리즘을 AES 로 못
박아 둔다 (데이터 페이지 알고리즘이 무엇이든 마스터 키는 항상
AES-256-CTR 로 DEK 를 래핑한다).
코드 전반에서 nonce 라고 부르는 IV 는 페이지 종류별로 다르
게 구성되는 16 바이트 버퍼다.
| 페이지 종류 | DEK | nonce 출처 | 길이 |
|---|---|---|---|
| 영구 데이터 페이지 | perm_key | iopage_plain->prv.lsa (8B) zero-pad | 16 B |
| 임시 데이터 페이지 | temp_key | tde_Cipher.temp_write_counter, ATOMIC_INC_64, 8B zero-pad | 16 B |
| WAL 페이지 | log_key | logpage_plain->hdr.logical_pageid (8B) zero-pad | 16 B |
| 래핑된 DEK | (master key) | tde_dk_nonce(dk_type) — 종류별 0/1/2 고정 패턴 | 16 B |
영구 페이지의 경우 LSA 는 WAL 덕분에 매 수정마다 자동으로 증
가한다 — undo / redo 로그가 append 될 때마다 새 LSA 가 만들어
지고 그 값이 페이지에 stamp 된다. (perm_key, lsa) 의 결합은
(페이지, 버전) 전역에 걸쳐 유일하므로 CTR invariant 가 충족된
다. nonce 는 암호화된 페이지 안의 prv.tde_nonce 에 박혀 디
스크로 함께 가고, reader 가 그 값을 그대로 읽어 복호화에 쓴
다.
임시 페이지에서는 LSA 가 sentinel 값
(pgbuf_init_temp_page_lsa) 으로 고정되어 있어 nonce 역할을
못한다. 대신 tde_Cipher 의 64 비트 원자 카운터가 매 암호화
마다 증가하고, 그 값이 prv.tde_nonce 에 기록되었다가 복호화
시점에 읽힌다. 카운터는 서버 시작 시 0 으로 초기화된다. 임시
파일은 재시작 시 다시 만들어지므로 우려는 서버 한 라이프타임
안의 wrap-around 뿐이다. 페이지 쓰기당 1 씩 증가하는 속도로는
2⁶⁴ 가 수천 년 걸리니 사실상 무관하다.
로그 페이지는 WAL 자체의 단조 증가 페이지 번호인 logical
pageid 를 그대로 쓴다. 전용 log_key 와 결합하므로, 어떤 로
그 페이지도 다른 로그 페이지와 (key, nonce) 쌍을 공유하지 않
는다.
DEK 자체에 대한 nonce 는 tde_dk_nonce() 가 정한 고정 패턴이
다.
// tde_dk_nonce — src/storage/tde.ccase TDE_DATA_KEY_TYPE_PERM: memset (dk_nonce, 0, ...); break;case TDE_DATA_KEY_TYPE_TEMP: memset (dk_nonce, 1, ...); break;case TDE_DATA_KEY_TYPE_LOG: memset (dk_nonce, 2, ...); break;이것이 안전한 이유는 마스터 키가 DEK 가 다시 쓰이는 빈도보다 더 자주 회전하기 때문이다. 각 (mk, dk_type) 쌍은 DEK 의 일생 동안 정확히 두 번만 암호화된다 — DB 생성 시 한 번, 그리고 매 MK 변경 시 한 번씩. 마스터 키가 두 번 쓰기 사이에 바뀌므로 종류별로 고정 nonce 를 쓰더라도 CTR 유일성은 깨지지 않는다. 동일한 MK 가 동일한 DEK 를 두 번 래핑하면 결과 바이트가 동일 해진다는 점은 (같은 key + nonce 는 같은 키 스트림을 만든다) 허용된다.
페이지 암호화 경계
섹션 제목: “페이지 암호화 경계”CUBRID 의 I/O 페이지는 고정 크기 struct 인 FILEIO_PAGE 다.
// FILEIO_PAGE — src/storage/file_io.hstruct fileio_page_reserved{ LOG_LSA lsa; // page LSN INT32 pageid; INT16 volid; unsigned char ptype; unsigned char pflag; // bit 0x1 = AES, bit 0x2 = ARIA, mask 0x3 INT32 p_reserve_1; INT32 p_reserve_2; INT64 tde_nonce; // counter (temp) or LSA copy (perm)};struct fileio_page{ FILEIO_PAGE_RESERVED prv; // header — NEVER encrypted char page[1]; // body — encrypted on-disk FILEIO_PAGE_WATERMARK prv2; // tail-LSA — NEVER encrypted};암호화 경계는 매크로 두 개로 정의된다.
#define TDE_DATA_PAGE_ENC_OFFSET sizeof (FILEIO_PAGE_RESERVED)#define TDE_DATA_PAGE_ENC_LENGTH DB_PAGESIZE디스크 위 평문 부분은 머리의 FILEIO_PAGE_RESERVED 와 꼬리의
FILEIO_PAGE_WATERMARK 다. 암호문 부분은 그 사이의 모든 바이
트, 길이는 DB_PAGESIZE 다. 이렇게 갈라야 하는 이유가 셋이다.
- 헤더 는 평문이어야 한다. 버퍼 매니저가 페이지를 복호화할
지 말지를 결정하기 전에 헤더를 읽기 때문이다.
pflag자 체가 이 페이지는 암호화되어 있는가 를 알려 주는 플래그이 므로,pflag를 키 상태의 일부로 두고pflag를 암호화하 는 것은 순환 의존이 된다. - 꼬리의 watermark 도 평문이어야 한다. 이 필드 (LSA 의 복
사본) 는 애초에 torn-write 검출 을 위해 도입된 자리다.
CUBRID 는
prv.lsa == prv2.lsa비교로 페이지의 부분 쓰기를 감지하는데, 이 검사는 암호화된 형태에서도 동작해야 한다. - (
prv안의)tde_nonce필드는 IV 그 자체이고, 암호학 적 doctrine 에 따라 IV 는 공개되어야 한다.
로그 페이지에도 비슷한 분할이 있다.
#define TDE_LOG_PAGE_ENC_OFFSET sizeof (LOG_HDRPAGE)#define TDE_LOG_PAGE_ENC_LENGTH ((LOG_PAGESIZE) - (TDE_LOG_PAGE_ENC_OFFSET))LOG_HDRPAGE (16 바이트: logical pageid, offset, flags,
checksum) 는 평문으로 남는다. 그 안의 flags 필드가 암호화 마
커를 들고 다닌다.
#define LOG_HDRPAGE_FLAG_ENCRYPTED_AES 0x1#define LOG_HDRPAGE_FLAG_ENCRYPTED_ARIA 0x2#define LOG_IS_PAGE_TDE_ENCRYPTED(p) \ ((p)->hdr.flags & LOG_HDRPAGE_FLAG_ENCRYPTED_AES \ || (p)->hdr.flags & LOG_HDRPAGE_FLAG_ENCRYPTED_ARIA)이 플래그가 데이터 페이지의 pflag 와 같은 역할을 하지만, 자
리가 로그 페이지 헤더라는 점이 다를 뿐이다.
파일 단위 입자성과 테이블스페이스 다이얼
섹션 제목: “파일 단위 입자성과 테이블스페이스 다이얼”암호화는 파일 단위 다 — 전역이 아니다. 각 FILE_HEADER
가 자기 TDE_ALGORITHM 을 들고 다니며,
file_set_tde_algorithm() 으로 조작된다. 페이지가 파일에 처
음 할당될 때 file_alloc() 이 헤더의 알고리즘을 읽어 새 페이
지의 pflag 에 stamp 한다.
// file_alloc — src/storage/file_manager.ctde_algo = file_get_tde_algorithm_internal (fhead);// ... debug logging ...pgbuf_set_tde_algorithm (thread_p, page_alloc, tde_algo, FILE_IS_TEMPORARY (fhead));이미 존재하는 파일에 암호화를 켜는 동작은
file_apply_tde_algorithm() 이다. 파일 헤더에 알고리즘을 set
한 뒤, 해당 파일의 모든 사용자 페이지를 무조건 write 모드로
latch 하면서 walk 하며 각 페이지의 pflag 를 set 한다. 모든
페이지를 다시 flag 해야 (그래서 다음 flush 에서 다시 암호화되
어야) 하므로 이 동작은 O (파일 페이지 수) 이고, 동시 접근이
시작되기 전에 끝나야 한다.
SQL 표면은 CREATE TABLE ... ENCRYPT=AES 다. 이 구문은 그 표
의 heap 파일과 모든 overflow 파일을
file_apply_tde_algorithm() 을 부르는 형태로 번역된다
(heap_create_internal 시점의 호출은 file_manager.c 의
12401, 12410 줄에 보인다). 사용자에게 노출되는 다이얼은 표 단
위지만, 내부 메커니즘은 파일 단위다.
어떤 파일에도 속하지 않는 페이지 — 가장 두드러지게 볼륨 헤더
와 파일 매니저 자체의 file-table 페이지 — 는 절대로 암호화
되지 않는다. 그 페이지들의 pflag 는 0 인 채로 남는다. 즉 데
이터 파일을 손에 넣은 공격자는 볼륨 레이아웃, sector bitmap,
file allocation table 까지는 여전히 볼 수 있다. 방어가 미치는
범위는 사용자 데이터 — heap 페이지, overflow 페이지, B-tree
페이지 — 와 WAL 뿐이다.
Encrypt-on-flush — 데이터 페이지
섹션 제목: “Encrypt-on-flush — 데이터 페이지”dirty 데이터 페이지는 디스크로 가는 길에 모두
pgbuf_bcb_flush_with_wal() 을 통과한다. 암호화 후크는 “페이
지를 밖으로 복사한다” 시퀀스의 가장 위에 놓여 있다.
// pgbuf_bcb_flush_with_wal — src/storage/page_buffer.ciopage = (FILEIO_PAGE *) PTR_ALIGN (page_buf, MAX_ALIGNMENT);CAST_BFPTR_TO_PGPTR (pgptr, bufptr);tde_algo = pgbuf_get_tde_algorithm (pgptr);if (tde_algo != TDE_ALGORITHM_NONE) { error = tde_encrypt_data_page (&bufptr->iopage_buffer->iopage, tde_algo, is_temp, iopage); // ... }else { memcpy (iopage, &bufptr->iopage_buffer->iopage, IO_PAGESIZE); }if (uses_dwb) { error = dwb_set_data_on_next_slot (thread_p, iopage, ..., &dwb_slot); // ... }흐름을 따라가 보면 다음과 같다.
IO_MAX_PAGE_SIZE크기의 스택 정렬 스크래치 버퍼iopage를 확보한다.pflag에서 페이지별 TDE 알고리즘을 읽는다. 0 이면 메모리 상의 페이지를 스크래치 버퍼로memcpy하고 진행한다.- 0 이 아니면
tde_encrypt_data_page()를 부른다. 이 함수가 헤더와 watermark 를 평문으로 복사하고, nonce 를 계산하고 (영구는 LSA, 임시는 원자 카운터), nonce 를iopage->prv.tde_nonce에 쓰고, 본체를 CTR 로 암호화해 스 크래치 버퍼에 담는다. - DWB (double-write buffer) 가 활성화되어 있으면 암호화된 페이지를 받아 자기 슬롯에 둔다. DWB 는 순수하게 torn-write 방어 장치이고, 자기가 보는 바이트는 이미 암호화된 상태다.
- DWB 가 제어를 돌려준 뒤
fileio_write가 암호화된 페이지 를 데이터 볼륨에 쓴다.
sequenceDiagram
participant T as 워커 스레드
participant PB as 페이지 버퍼
participant TDE as tde_encrypt_data_page
participant DWB as Double-write 버퍼
participant FIO as fileio_write
participant DSK as 데이터 볼륨
T->>PB: pgbuf_bcb_flush_with_wal
PB->>PB: pflag 읽기 → tde_algo
alt tde_algo != NONE
PB->>TDE: encrypt(plain, algo, is_temp, cipher)
TDE->>TDE: nonce = LSA (perm) 또는 atomic++ (temp)
TDE->>TDE: 헤더 + watermark 평문 복사
TDE->>TDE: AES-256-CTR 본체 → cipher 버퍼
TDE-->>PB: cipher 버퍼
else tde_algo == NONE
PB->>PB: memcpy plain → cipher
end
PB->>DWB: dwb_set_data_on_next_slot(cipher)
DWB->>FIO: fileio_write(cipher)
FIO->>DSK: 페이지 기록
Decrypt-on-read — 데이터 페이지
섹션 제목: “Decrypt-on-read — 데이터 페이지”거울상은 pgbuf_claim_bcb_for_fix() 안에 있다. 페이지 miss
가 디스크 read 를 트리거하고, 그 직후 in-place 복호화가 일어
난다.
// pgbuf_claim_bcb_for_fix — src/storage/page_buffer.cif (dwb_read_page (thread_p, vpid, &bufptr->iopage_buffer->iopage, &success) != NO_ERROR) { ... }else if (success == true) { /* copied from DWB */ }else if (fileio_read (thread_p, ..., &bufptr->iopage_buffer->iopage, vpid->pageid, IO_PAGESIZE) == NULL) { ... }
CAST_IOPGPTR_TO_PGPTR (pgptr, &bufptr->iopage_buffer->iopage);tde_algo = pgbuf_get_tde_algorithm (pgptr);if (tde_algo != TDE_ALGORITHM_NONE) { if (tde_decrypt_data_page (&bufptr->iopage_buffer->iopage, tde_algo, pgbuf_is_temporary_volume (vpid->volid), &bufptr->iopage_buffer->iopage) != NO_ERROR) { // ... } }복호화는 in-place 다. tde_decrypt_data_page() 는 같은
bufptr->iopage_buffer->iopage 버퍼에서 읽고 같은 버퍼에 쓴
다. CTR 이 stream cipher 라서, 암호화의 역연산은 같은 키 스
트림과의 두 번째 XOR — 즉 정확히 같은 연산 — 이다.
DWB 는 가장 먼저 조회된다. 이 VPID 의 쓰기가 현재 DWB 에 묶여
있다면 dwb_read_page() 가 DWB 의 사본 (암호문 — DWB 는 암
호화된 페이지를 들고 있다) 을 돌려 준다. 그 후 복호화 후크가
그대로 실행된다. 이것이 DWB 를 TDE 를 투명하게 만들어 준
다.
sequenceDiagram
participant T as 워커 스레드
participant PB as 페이지 버퍼
participant DWB as Double-write 버퍼
participant FIO as fileio_read
participant DSK as 데이터 볼륨
participant TDE as tde_decrypt_data_page
T->>PB: pgbuf_fix(vpid)
PB->>DWB: dwb_read_page(vpid)
alt DWB에 페이지 있음
DWB-->>PB: cipher (DWB 슬롯에서)
else DWB에 없음
PB->>FIO: fileio_read(vpid)
FIO->>DSK: 페이지 read
DSK-->>FIO: cipher 바이트
FIO-->>PB: cipher
end
PB->>PB: pgbuf_get_tde_algorithm(pflag)
alt tde_algo != NONE
PB->>TDE: decrypt(cipher, algo, is_temp, plain)
TDE->>TDE: nonce = prv.tde_nonce
TDE->>TDE: AES-256-CTR 본체 → 평문 (in place)
TDE-->>PB: 평문 페이지
end
PB-->>T: PAGE_PTR
로그 암호화
섹션 제목: “로그 암호화”로그 페이지는 이미 존재하는 WAL flush 경로를 그대로 따르며,
후크가 두 군데 추가된다. append 시점 에 페이지가 사용자 데
이터를 포함하는 레코드를 한 개라도 받았다면
logpb_set_tde_algorithm 으로 페이지 자체를 암호화 대상 으
로 표시하고, flush 시점 에 디스크 쓰기 직전에 암호화한다.
append 쪽 결정은 로그 레코드 단위에서 시작해 페이지로 promote
된다. prior_set_tde_encrypted() 가 log_prior_node 에 TDE
관련 플래그를 단다 (이 함수는 tde.h 의
LOG_MAY_CONTAIN_USER_DATA 매크로가 열거하는 모든 heap 및
B-tree user-data 복구 인덱스에 해당하는 레코드 종류를 호
출된다). 페이지가 마무리될 때 logpb_next_append_page() 와
logpb_start_append() 가
log_Gl.append.appending_page_tde_encrypted 를 검사하고
PRM_ID_TDE_DEFAULT_ALGORITHM 값을 새 페이지의 flag 에 stamp
한다.
// logpb_next_append_page — src/transaction/log_page_buffer.cif (log_Gl.append.appending_page_tde_encrypted) { TDE_ALGORITHM tde_algo = (TDE_ALGORITHM) prm_get_integer_value (PRM_ID_TDE_DEFAULT_ALGORITHM); logpb_set_tde_algorithm (thread_p, log_Gl.append.log_pgptr, tde_algo); logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); }flush 쪽 암호화는 logpb_writev_append_pages() 와
logpb_write_page_to_disk() 에 산다.
// logpb_writev_append_pages — src/transaction/log_page_buffer.clog_pgptr = to_flush[i];if (LOG_IS_PAGE_TDE_ENCRYPTED (log_pgptr)) { if (tde_encrypt_log_page (log_pgptr, logpb_get_tde_algorithm (log_pgptr), enc_pgptr) != NO_ERROR) { logpb_set_tde_algorithm (thread_p, log_pgptr, TDE_ALGORITHM_NONE); // ... raise ER_TDE_ENCRYPTION_LOGPAGE_ERORR_AND_OFF_TDE ... } else { log_pgptr = enc_pgptr; } }암호화 실패 시의 fallback 은 fail-open 이다 — flag 를 끄
고, 페이지를 평문으로 쓴 뒤, 에러를 raise 한다. 의도는
logpb_write_page_to_disk() 의 주석이 명시한 대로 “한 번 실
패하면 그 후로 페이지가 항상 사용자 데이터를 평문으로 흘리게
된다” 이다 [원문 그대로]. 즉 데이터베이스가 멈추는 것보다 프
라이버시 침해를 받아들이는 쪽을 택한 의도된 trade-off 다. 검
토 대상 후보다.
decrypt-on-read 거울상은 logpb_read_page_from_active_log()
와 logpb_read_page_from_file() 에 있다. 후자에는 흥미로운
비대칭이 있다. archive 에서 읽어 오는 페이지는 이미 평문이
다 (아래 설명대로 archive 로그 파일은 복호화된 내용을 저장한
다) 라서 복호화가 필요 없고, active 로그에서 읽어 오는 페이
지는 자기 flag 를 그대로 들고 있어서 in-place 로 복호화된다.
// logpb_read_page_from_file — src/transaction/log_page_buffer.cTDE_ALGORITHM tde_algo = logpb_get_tde_algorithm ((LOG_PAGE *) log_pgptr);if (tde_algo != TDE_ALGORITHM_NONE) { if (tde_decrypt_log_page ((LOG_PAGE *) log_pgptr, tde_algo, (LOG_PAGE *) log_pgptr) != NO_ERROR) { ... } }archive 를 평문으로 저장한다는 선택은 다소 이례적이다 —
archive 된 로그가 유휴 상태에서 암호화되지 않는다 는 의미가
되며, 암호화는 active 로그에 한정된다. 소스 주석은 그 이유를,
replication log 에 대해서는 TDE 가 비활성화되어 있기 때문이라
고 적는다 (UNSTABLE_TDE_FOR_REPLICATION_LOG). archive 파일
은 archival 과 replication 에 함께 쓰이는데, replication 쪽은
TDE 상태에 의존해서는 안 된다는 정책이다.
DWB 상호작용 — 패스스루
섹션 제목: “DWB 상호작용 — 패스스루”double-write buffer 는 페이지 버퍼가 암호화를 적용한 후의
페이지만을 본다. double_write_buffer.cpp 안에는 tde_* 호
출이 0 건이다. DWB 는 들어온 바이트를 그대로 자기 슬롯에 끼우
고, 그 바이트를 자기 checksum 을 계산하고, 나중 flush 때
두 번 쓴다 (DWB 볼륨에 먼저, 데이터 볼륨에 그 다음). 크래시
복구 시점에는 DWB 볼륨이 데이터 볼륨의 미완료 쓰기를 검
사되고, DWB 의 사본 (여전히 암호문이다) 이 데이터 볼륨에
replay 되며, 그 페이지는 다음 read 에서 페이지 버퍼 코드가 평
소대로 복호화한다.
깔끔한 분리다. DWB 는 torn write 에 신경 쓰고, 내용에는 신경 쓰지 않는다.
마스터 키 파일 라이프사이클
섹션 제목: “마스터 키 파일 라이프사이클”tde_initialize() 는 데이터베이스 생성 시점에 한 번 호출된다
(boot_create_volume_dirs). 다음 일을 한다.
tde_make_keys_file_fullname()으로 마스터 키 파일 경로를 만든다.tde_create_keys_file()을 부른다. 이 함수는O_CREAT | O_RDWR로 모드0600인 파일을 열고, offset 0 에CUBRID_MAGIC_KEYS를 쓰고,fsync한다. create + write 쌍을 둘러싸는 동안 (치명적인 것을 제외한) 시그널이 차단된 다.tde_create_mk()로 임의의 MK 를 생성한다 (RAND_bytes(default_mk, 32)+ 현재time(NULL)).tde_add_mk()가 키 파일의 슬롯 0 에 그 MK 를 쓴다.- 세 개의 DEK 를 생성한다 (
tde_create_dk()× 3, 각각RAND_bytes(32)). tde_generate_keyinfo()가 MK index, SHA-256(MK), 그리고 세 개의 래핑된 DEK 로TDE_KEYINFO를 빌드한다.TDE_KEYINFOblob 을 전용 TDE keyinfo heap 에 insert 한 다.
tde_cipher_initialize() 는 매 서버 재시작마다
boot_restart_server → tde_cipher_initialize 경로로 호출된
다 (호출 지점은 boot_sr.c:2324, boot_sr.c:5233).
<db>_keys를 mount 한다 (사용자 지정 경로 우선, 그 다음 기본 경로).- 매직 헤더를 검증한다.
- keyinfo heap 에서
TDE_KEYINFO를 읽는다. - 키 파일에서
keyinfo.mk_index위치의 MK 를 읽는다. - 그 MK 를 해시해서
keyinfo.mk_hash와 비교한다. 불일치이 면 거절한다 — 이것이 잘못된 MK 상황을 잡아 내는 메커니즘이 다. tde_load_dks()가 세 DEK 를 풀어tde_Cipher.data_keys에 둔다.tde_Cipher.temp_write_counter = 0으로 리셋한다.tde_Cipher.is_loaded = true로 설정한다.- 키 파일을 dismount 한다. MK 는 자기 일을 끝냈다. 이제 프 로세스 메모리에는 평문 DEK 들만 남는다.
tde_Cipher 는 TDE_CIPHER 타입의 단일 전역이다. 세 DEK 와
temp-counter 를 들고 있고, tde.h 에 extern 으로 선언되어
tde.c 에 정의된다. 모든 encrypt / decrypt 호출이 이 전역으로
읽고, 스레드별 사본은 없다.
마스터 키 회전
섹션 제목: “마스터 키 회전”tde_change_mk() (그리고 admin wrapper 인
xtde_change_mk_without_flock — cubrid tde --change-key=N
admin 명령으로 노출된다, util_admin.c 의 TDE_CHANGE_KEY_S,
util_cs.c:3941 참고) 가 마스터 키를 교체한다. 데이터는 단
한 페이지도 다시 암호화되지 않는다.
- 키 파일에서 새 MK 를 index 로 찾는다.
- 이미 로드되어 있는 키와 같은 키가 아닌지 확인한다.
- 이전 키가 파일에 여전히 존재하는지 확인한다 (이래야 DBA 가 풀 수 없는 키 경로로 데이터베이스를 좌초시키는 사태를 막는다).
- 새
mk_index, 새mk_hash, 그리고 다시 래핑된 DEK 들로 새TDE_KEYINFO를 만든다 (메모리 안의 평문 DEK 가 새 MK 로 다시 암호화된다). heap_update_logical로 옛TDE_KEYINFO레코드를 덮어쓴 다.heap_flush()가 필수다. 이게 빠지면 새 keyinfo 가 페 이지 버퍼에만 머문 상태에서 크래시가 일어날 수 있다. 그러 면 메모리상 keyinfo 는 새 MK 를 가리키는데 디스크상 keyinfo 는 옛 MK 를 가리키는 상태로 데이터베이스가 남게 된다. flush 가 끝난 뒤에는 DBA 가 키 파일에서 이전 MK 를 지워도 데이터 베이스가 정상적으로 열림이 보장된다.
이 명령이 회전하지 않는 것은 다음과 같다.
- DEK 자체 — 같은 바이트 값을 유지한 채 다시 래핑될 뿐이다. 디스크 위 모든 페이지는 이전과 같은 DEK 로 계속 암호화된 상 태로 남는다.
- 어떤 데이터 페이지도 — 그 페이지를 만든 DEK 가 변하지 않았 으므로 기존 암호문은 그대로 유효하다.
DEK 회전은 그 DEK 를 쓰는 모든 페이지를 다시 암호화해야 한 다. 현재 소스에는 그런 동작이 없다. 향후 작업의 후보다.
성능과 AES-NI
섹션 제목: “성능과 AES-NI”암호화 두 경로 모두 OpenSSL 의 EVP_* 인터페이스를 통과한다.
이 인터페이스는 x86 에서 AES-NI 를, ARM 에서는 ARM Crypto
Extensions 를 자동으로 선택한다. 핫 루프는 페이지당
EVP_EncryptUpdate 한 번이다 — CTR 모드의 16 KB 페이지라면
16 KB / 16 B = 1024 AES 블록 연산이고, AES-NI 하드웨어에서는
각 블록이 약 1 사이클이다. 최신 하드웨어에서 페이지당 오버헤
드는 마이크로초 미만이다. CTR 모드는 다중 페이지 flush 를 코
어 단위로 자명하게 병렬화할 수 있게 해 주지만, CUBRID 는 이를
활용하지 않는다 — 암호화가 flush 스레드 위에서 inline 으로
동작한다.
비용은 다음에 떨어진다.
- 페이지 버퍼 miss 로 인한 디스크 read: miss 된 페이지당 복호화 1 회.
- 버퍼 풀 flush: flush 된 dirty 페이지당 암호화 1 회.
- WAL flush: 사용자 데이터를 담은 로그 페이지당 암호화 1 회.
버퍼 풀 hit 는 무료다. 워킹 셋이 버퍼 풀에 들어가는 워크로드 는 TDE 비용을 log-flush 경로에서만 본다. 버퍼 풀을 thrash 하 는 OLTP 워크로드는 매 flush 와 매 miss 마다 비용을 지불한다.
graph LR
subgraph Hot["핫 패스 — 버퍼 풀 hit"]
Q1[Query] --> H1[pgbuf_fix]
H1 --> M1[메모리 안의 평문]
end
subgraph Miss["콜드 패스 — 버퍼 풀 miss"]
Q2[Query] --> H2[pgbuf_fix]
H2 --> R2[fileio_read cipher]
R2 --> D2[tde_decrypt_data_page]
D2 --> M2[메모리 안의 평문]
end
subgraph Flush["flush 경로 — 암호화"]
F1[pgbuf_bcb_flush_with_wal] --> E1[tde_encrypt_data_page]
E1 --> W1[fileio_write cipher]
end
subgraph WAL["WAL 경로 — 로그 암호화"]
L1[logpb_writev_append_pages] --> EL[tde_encrypt_log_page]
EL --> WL[fileio_write log cipher]
end
소스 코드 가이드
섹션 제목: “소스 코드 가이드”심볼을 서브시스템 단위로 묶는다. 각 심볼의 역할은 두 줄 이하 로 요약하고, 전체 위치 힌트는 이 섹션 끝의 표에 모은다.
키 파일 관리
섹션 제목: “키 파일 관리”tde_create_keys_file(tde.c) — 모드 0600 의O_CREAT|O_RDWRopen,CUBRID_MAGIC_KEYSwrite, fsync, close. create 동안 시그널 차단.tde_validate_keys_file(tde.c) — 매직을 읽어CUBRID_MAGIC_KEYS와 비교한다. 재시작 시점, 그리고 키 파 일에서 read 하기 전마다 호출된다.tde_make_keys_file_fullname(tde.c) — 마스터 키 파일 경 로를 결정한다. 데이터베이스 파일과 같은 위치의<db>_keys또는 시스템 파라미터가 설정되어 있다면${tde_keys_file_path}/<base>_keys.tde_copy_keys_file(tde.c) — 키 파일을 블록 단위로 복사 한다 (cubrid copydb와 backup 시 사용).tde_add_mk(tde.c) — append 또는 첫 번째 삭제 슬롯 채우 기.TDE_MK_FILE_ITEM_COUNT_MAX = 128키가 데이터베이스당 상한이다.tde_find_mk(tde.c) — 슬롯으로 seek 해서 item 을 읽고created_time != -1임을 검증한다.tde_find_first_mk(tde.c) — 슬롯 0 부터 선형 스캔으로 처음 만나는 valid 항목을 반환한다. DB 생성 시ER_BO_VOLUME_EXISTS상황에서 기존 키를 집어드는 데 쓰인 다.tde_delete_mk(tde.c) —created_time을 -1 로 덮어쓴 다. in-place — 파일이 줄어들지 않는다.tde_dump_mks(tde.c) — admin 스캔. 모든 valid 슬롯의 index 와 생성 시각을 출력한다.tde_create_mk(tde.c) —RAND_bytes(master_key, 32)와time(NULL).tde_print_mk(tde.c) — MK 를 hex 로 출력한다. admin 이 사용한다.
Keyinfo (DB 내부의 래핑된 DEK 레코드)
섹션 제목: “Keyinfo (DB 내부의 래핑된 DEK 레코드)”tde_initialize(tde.c) — DB 생성 진입점. 키 파일, MK, 세 DEK, keyinfo 를 만들고 TDE keyinfo heap 에 insert 한다.tde_cipher_initialize(tde.c) — 서버 재시작 진입점. 키 파일 mount, keyinfo fetch, MK 검증, DEK 를tde_Cipher로 복호화.tde_get_keyinfo(tde.c) — TDE keyinfo heap 의 단일 레코 드를heap_first기반으로 fetch 한다.tde_update_keyinfo(tde.c) —UPDATE_INPLACE_CURRENT_MVCCID와 함께heap_update_logical.heap_scancache_check_with_hfid를 우회하려고class_oid를 약간 손본다.tde_generate_keyinfo(tde.c) —TDE_KEYINFO에 SHA-256(MK) 와 세 개의 암호화된 DEK 를 채운다.tde_change_mk(tde.c) — 새 MK 로 DEK 를 다시 래핑하고 새 keyinfo 를heap_flush로 강제 flush 한다. flush 누락 시 후속 크래시가 데이터베이스를 좌초시킬 수 있어서 강제한 다.xtde_get_mk_info(tde.c) — admin RPC. 로드된 keyinfo 의mk_index,created_time,set_time을 반환한다.xtde_change_mk_without_flock(tde.c) — admin RPC.fileio_open으로 키 파일을 읽고 (클라이언트 쪽이 fcntl lock 을 들고 있다), 검증한 뒤tde_change_mk를 부른다.
마스터 키 검증과 DEK wrap
섹션 제목: “마스터 키 검증과 DEK wrap”tde_load_mk(tde.c) — 주어진 index 로 키 파일에서 MK 를 읽고, 해시해서 keyinfo 의mk_hash와 비교한다.tde_validate_mk(tde.c) — 후보 MK 를 SHA-256 해서 저장 된 해시와memcmp. 상수 시간 (constant-time) 인가?memcmp라 아니다.tde_make_mk_hash(tde.c) — 32 바이트 MK 에 대한 SHA-256.SHA256_DIGEST_LENGTH == TDE_MASTER_KEY_LENGTH라 는 정적 단언이 함께 있다는 점에 주의.tde_load_dks(tde.c) — keyinfo 의 세 DEK 를 모두 풀어tde_Cipher.data_keys로 옮긴다.tde_create_dk(tde.c) —RAND_bytes(data_key, 32).tde_encrypt_dk(tde.c) —TDE_DK_ALGORITHM(= AES) 과 종류별 유도 nonce 로 DEK 를 MK 로 래핑한다.tde_decrypt_dk(tde.c) — unwrap.tde_dk_nonce(tde.c) —TDE_DATA_KEY_TYPE별 0/1/2 바 이트 fill 고정 패턴.
페이지 암호화 / 복호화 — 내부
섹션 제목: “페이지 암호화 / 복호화 — 내부”tde_encrypt_internal(tde.c) — 단일 OpenSSL EVP 경로:EVP_CIPHER_CTX_new→EVP_EncryptInit_ex→EncryptUpdate→EncryptFinal_ex→EVP_CIPHER_CTX_free. CTR 이 stream cipher 라서cipher_len == length를 assert 한다.tde_decrypt_internal(tde.c) — 거울상.
페이지 암호화 / 복호화 — 공개
섹션 제목: “페이지 암호화 / 복호화 — 공개”tde_encrypt_data_page(tde.c) — 헤더 (32 바이트FILEIO_PAGE_RESERVED) 와 watermark 를 평문으로 복사하고, nonce (영구는 LSA, 임시는 원자 카운터) 를 유도해서prv.tde_nonce에 stamp 하고, 본체를 CTR 로 암호화한다.tde_decrypt_data_page(tde.c) — 거울상. nonce 를prv.tde_nonce에서 읽는다.tde_encrypt_log_page(tde.c) — 비슷하지만 offset 이sizeof(LOG_HDRPAGE)이고 nonce 가hdr.logical_pageid다.tde_decrypt_log_page(tde.c) — 거울상.tde_get_algorithm_name(tde.c) — 로그 라인용 문자열:NONE,AES,ARIA.tde_is_loaded(tde.c) —tde_Cipher.is_loaded의 getter.
페이지 버퍼 후크 (데이터 페이지)
섹션 제목: “페이지 버퍼 후크 (데이터 페이지)”pgbuf_set_tde_algorithm(page_buffer.c) —pflag비트 를 쓰고,skip_logging이 false 라면 (temp 페이지는 true)RVPGBUF_SET_TDE_ALGORITHM로그 레코드를 emit 한다.pgbuf_get_tde_algorithm(page_buffer.c) —pflag비트 를TDE_ALGORITHM_AES/_ARIA/_NONE으로 다시 읽는 다.pgbuf_rv_set_tde_algorithm(page_buffer.c) — 복구 경로:RVPGBUF_SET_TDE_ALGORITHM로그 레코드를 페이지에 replay 한다.pgbuf_bcb_flush_with_wal(page_buffer.c) — flush 핫 패 스. DWB 이전에 encrypt-on-flush 후크를 들고 있다.pgbuf_claim_bcb_for_fix(page_buffer.c) — fix 콜드 패스. DWB / fileio_read 이후에 decrypt-on-read 후크를 들고 있다.pgbuf_copy_from_area(page_buffer.c) — 페이지 영역에 직 접 쓰는 특수 경로.TDE_ALGORITHM인자를 받아서 새 페이지 에 적용한다.
파일 매니저 후크 (파일 단위 입자성)
섹션 제목: “파일 매니저 후크 (파일 단위 입자성)”file_set_tde_algorithm(file_manager.c) — 알고리즘을FILE_HEADER에 쓰고RVFL_FHEAD_SET_TDE_ALGORITHM로그 레코드를 emit 한다 (temp 는 제외).file_get_tde_algorithm_internal(file_manager.c) — 다시 읽기.file_set_tde_algorithm_internal(file_manager.c) — 로깅 없이 쓰기 (복구가 사용한다).file_get_tde_algorithm(file_manager.c) — 페이지 latch 와 함께 공개 read.file_apply_tde_algorithm(file_manager.c) — 파일 헤더에 set + 모든 사용자 페이지를 walk 해서pflag를 stamp 한다. 호출자가 동시 접근이 없음을 보장해야 한다 (PGBUF_UNCONDITIONAL_LATCH사용).file_alloc(file_manager.c) — 할당 시점에 파일의 알고리 즘을 새 페이지의pflag로 복사한다. 그 줄:pgbuf_set_tde_algorithm (thread_p, page_alloc, tde_algo, FILE_IS_TEMPORARY (fhead));.file_rv_set_tde_algorithm(file_manager.c) — 복구 replay.
로그 페이지 후크
섹션 제목: “로그 페이지 후크”logpb_set_tde_algorithm(log_page_buffer.c) — 로그 페이 지 헤더에LOG_HDRPAGE_FLAG_ENCRYPTED_*플래그를 set 한 다.logpb_get_tde_algorithm(log_page_buffer.c) — 다시 읽 기.LOG_IS_PAGE_TDE_ENCRYPTED(log_storage.hpp) — fast 매크 로 검사.prior_set_tde_encrypted(log_append.cpp) — prior node 에 mark.tde_is_loaded()가 false 면 거절한다.prior_is_tde_encrypted(log_append.cpp) — 다시 읽기.LOG_MAY_CONTAIN_USER_DATA(tde.h) — 페이지에 사용자 데 이터가 있음을 의미하는 복구 인덱스를 열거하는 매크로. 이것 이prior_set_tde_encrypted의 트리거다.logpb_next_append_page(log_page_buffer.c) — 새 append 페이지 할당 시appending_page_tde_encrypted를 페이지 flag 로 전파.logpb_start_append(log_page_buffer.c) — 페이지의 첫 레 코드에서 같은 동작.logpb_writev_append_pages(log_page_buffer.c) — active 로그의 flush 핫 패스. 각 TDE 표시된 페이지를 스크래치 버퍼 로 암호화한 뒤 write 한다.logpb_write_page_to_disk(log_page_buffer.c) — 단일 페 이지 flush 변형. 같은 암호화 로직.logpb_read_page_from_active_log(log_page_buffer.c) — 일괄 read. 페이지를 순회하며 TDE 표시된 것들을 in-place 로 복호화한다.logpb_read_page_from_file(log_page_buffer.c) — active / archive 분기와 함께 동작하는 단일 페이지 read. archive 페 이지는 이미 평문이다.
부트 통합
섹션 제목: “부트 통합”tde_initialize호출 지점 —boot_sr.c:5104,boot_create_volume_dirs안 (DB 생성 흐름).tde_cipher_initialize호출 지점 —boot_sr.c:2324(명시 적 MK 경로와 함께 재시작,restoredb가 사용) 와boot_sr.c:5233(기본 MK 경로의 일반 재시작).boot_Db_parm.tde_keyinfo_hfid— TDE keyinfo heap 의 HFID. DB control block 에 영속화된다.
Admin / 유틸리티
섹션 제목: “Admin / 유틸리티”TDE_CHANGE_KEY_S(util_admin.c) —cubrid tde --change-key=N의 짧은 옵션. 서버 측의xtde_change_mk_without_flock으로 전달된다.TDE_CHANGE_KEY_L(util_admin.c) — 긴 옵션.tdeadmin 명령 본체 —util_cs.c:3941이 옵션을 읽어 서 버에 RPC 한다.
상수와 타입
섹션 제목: “상수와 타입”TDE_ALGORITHMenum (tde.h) —NONE(0),AES(1),ARIA(2).TDE_DATA_KEY_TYPEenum (tde.h) —PERM,TEMP,LOG.TDE_DATA_KEY_SET(tde.h) — 세 개의 32 바이트 DEK 를 묶 은 struct.TDE_KEYINFO(tde.h) — 디스크 위 keyinfo blob.TDE_MK_FILE_ITEM(tde.h) — 디스크 위 마스터 키 파일 슬 롯.TDE_CIPHER(tde.h) — 메모리 안의 싱글톤.TDE_DATA_PAGE_ENC_OFFSET/_LENGTH(tde.h) — 데이터 페이지의 암호화 영역.TDE_LOG_PAGE_ENC_OFFSET/_LENGTH(tde.h) — 로그 페이 지의 암호화 영역.TDE_DATA_PAGE_NONCE_LENGTH= 16,TDE_LOG_PAGE_NONCE_LENGTH= 16,TDE_DK_NONCE_LENGTH= 16 (tde.h).TDE_MASTER_KEY_LENGTH= 32,TDE_DATA_KEY_LENGTH= 32 (tde.h).TDE_MK_FILE_ITEM_COUNT_MAX= 128 (tde.h).LOG_DBTDE_KEYS_VOLID(log_volids.hpp) — 마스터 키 파일 이fileio_mount로 mount 될 때 부여되는 합성 volid.FILEIO_PAGE_FLAG_ENCRYPTED_AES= 0x1,_ARIA= 0x2,_MASK= 0x3 (file_io.h).LOG_HDRPAGE_FLAG_ENCRYPTED_AES= 0x1,_ARIA= 0x2,_MASK= 0x3 (log_storage.hpp).PRM_ID_TDE_DEFAULT_ALGORITHM(system_parameter) —tde_default_algorithm. 로그 암호화에 쓰이는 알고리즘이다 (데이터 페이지는 표 단위로 설정된 파일 헤더의 알고리즘을 따 른다).PRM_ID_TDE_KEYS_FILE_PATH(system_parameter) —tde_keys_file_path.<db>_keys위치를 선택적으로 override 한다.FILEIO_SUFFIX_KEYS=_keys(file_io.h) — 기본 마스 터 키 파일 접미사.
위치 힌트 (2026-05-01 기준)
섹션 제목: “위치 힌트 (2026-05-01 기준)”| 심볼 | 파일 | 라인 |
|---|---|---|
tde_initialize | src/storage/tde.c | 107 |
tde_cipher_initialize | src/storage/tde.c | 233 |
tde_create_keys_file | src/storage/tde.c | 321 |
tde_validate_keys_file | src/storage/tde.c | 370 |
tde_copy_keys_file | src/storage/tde.c | 410 |
tde_make_keys_file_fullname | src/storage/tde.c | 504 |
tde_generate_keyinfo | src/storage/tde.c | 532 |
tde_get_keyinfo | src/storage/tde.c | 569 |
tde_update_keyinfo | src/storage/tde.c | 607 |
tde_change_mk | src/storage/tde.c | 661 |
tde_load_mk | src/storage/tde.c | 705 |
tde_load_dks | src/storage/tde.c | 742 |
tde_validate_mk | src/storage/tde.c | 774 |
tde_make_mk_hash | src/storage/tde.c | 794 |
tde_create_dk | src/storage/tde.c | 814 |
tde_encrypt_dk | src/storage/tde.c | 838 |
tde_decrypt_dk | src/storage/tde.c | 858 |
tde_dk_nonce | src/storage/tde.c | 875 |
tde_encrypt_data_page | src/storage/tde.c | 908 |
tde_decrypt_data_page | src/storage/tde.c | 961 |
tde_encrypt_log_page | src/storage/tde.c | 1009 |
tde_decrypt_log_page | src/storage/tde.c | 1039 |
tde_encrypt_internal | src/storage/tde.c | 1074 |
tde_decrypt_internal | src/storage/tde.c | 1153 |
xtde_get_mk_info | src/storage/tde.c | 1226 |
xtde_change_mk_without_flock | src/storage/tde.c | 1258 |
tde_create_mk | src/storage/tde.c | 1323 |
tde_add_mk | src/storage/tde.c | 1363 |
tde_find_mk | src/storage/tde.c | 1449 |
tde_find_first_mk | src/storage/tde.c | 1516 |
tde_delete_mk | src/storage/tde.c | 1581 |
tde_dump_mks | src/storage/tde.c | 1641 |
tde_get_algorithm_name | src/storage/tde.c | 1706 |
TDE_CIPHER (struct) | src/storage/tde.h | 148 |
TDE_KEYINFO (struct) | src/storage/tde.h | 160 |
TDE_MK_FILE_ITEM (struct) | src/storage/tde.h | 92 |
TDE_DATA_KEY_SET (struct) | src/storage/tde.h | 85 |
LOG_MAY_CONTAIN_USER_DATA (macro) | src/storage/tde.h | 107 |
FILEIO_PAGE_FLAG_ENCRYPTED_AES | src/storage/file_io.h | 63 |
FILEIO_PAGE_RESERVED (struct) | src/storage/file_io.h | 165 |
LOG_HDRPAGE_FLAG_ENCRYPTED_AES | src/transaction/log_storage.hpp | 42 |
LOG_IS_PAGE_TDE_ENCRYPTED | src/transaction/log_storage.hpp | 47 |
LOG_DBTDE_KEYS_VOLID | src/transaction/log_volids.hpp | 41 |
pgbuf_set_tde_algorithm | src/storage/page_buffer.c | 4880 |
pgbuf_rv_set_tde_algorithm | src/storage/page_buffer.c | 4933 |
pgbuf_get_tde_algorithm | src/storage/page_buffer.c | 4953 |
pgbuf_claim_bcb_for_fix (decrypt hook) | src/storage/page_buffer.c | 8277 |
pgbuf_bcb_flush_with_wal (encrypt hook) | src/storage/page_buffer.c | 10532 |
file_set_tde_algorithm | src/storage/file_manager.c | 5823 |
file_get_tde_algorithm | src/storage/file_manager.c | 5929 |
file_apply_tde_algorithm | src/storage/file_manager.c | 6003 |
file_alloc (TDE stamp) | src/storage/file_manager.c | 5503 |
prior_set_tde_encrypted | src/transaction/log_append.cpp | 1564 |
prior_is_tde_encrypted | src/transaction/log_append.cpp | 1580 |
logpb_get_tde_algorithm | src/transaction/log_page_buffer.c | 11564 |
logpb_set_tde_algorithm | src/transaction/log_page_buffer.c | 11592 |
logpb_writev_append_pages (encrypt hook) | src/transaction/log_page_buffer.c | 2819 |
logpb_write_page_to_disk (encrypt hook) | src/transaction/log_page_buffer.c | 2303 |
logpb_read_page_from_active_log (decrypt) | src/transaction/log_page_buffer.c | 2201 |
logpb_read_page_from_file (decrypt) | src/transaction/log_page_buffer.c | 2110 |
tde_initialize call site | src/transaction/boot_sr.c | 5104 |
tde_cipher_initialize call sites | src/transaction/boot_sr.c | 2324, 5233 |
교차 검증 노트
섹션 제목: “교차 검증 노트”cubrid-page-buffer-manager.md 와 비교. 페이지 버퍼 문서
는 LRU / AOUT 교체 정책과 pgbuf_bcb_flush_with_wal 의 dirty
플래그 회계는 다루지만, flush 시퀀스 가장 위에 자리한 TDE 후
크는 언급하지 않는다. 암호화 경로는 IO_MAX_PAGE_SIZE 만큼의
스택 정렬 스크래치 버퍼를 잡고, tde_encrypt_data_page 를 통
해 메모리 안의 평문을 그 버퍼로 복사한다 (이 함수는 내부적으
로 FILEIO_PAGE_RESERVED 헤더와 watermark 를 평문으로
memcpy 한 뒤 본체를 CTR 로 암호화한다). 그 이후 DWB 와
fileio_write 가 보는 것은 그 cipher 사본뿐이다. BCB 안의 페
이지는 절대로 수정되지 않는다 — RAM 에는 평문 그대로 남
고, 디스크로 가는 스크래치 사본만 암호화된다. 그래서 버퍼 풀
hit 는 TDE 비용을 하나도 지불하지 않는다.
cubrid-double-write-buffer.md 와 비교. DWB 문서는 내용
비의존성을 주장한다. TDE 관점에서 이 주장은 정확하다. DWB 는
pgbuf_bcb_flush_with_wal 이 만들어 준 이미 암호화된 바이트
만 받고, 그 바이트로 자기 checksum 을 계산한 뒤 두 번 쓴다.
복구 시점에는 DWB 가 (cipher) 사본을 데이터 볼륨에 replay 하
고, 그 페이지의 다음 read 는 pgbuf_claim_bcb_for_fix 의 복
호화 후크를 평소대로 통과한다. 이 점이 의미를 가지는 자리가
정확히 한 군데 있다 — DWB 볼륨 자체는 단위로 암호화되지 않지
만, DWB 슬롯에 들어 있는 모든 페이지는 그 페이지의 암호화된
형태다. DWB 파일을 읽는 공격자는 평문이 아니라 암호문을 본다.
cubrid-log-manager.md 와 비교 (예상). 로그 매니저 문서
가 나오면 다음을 기록해야 한다.
- TDE 플래그는 페이지 단위 속성
(
LOG_HDRPAGE_FLAG_ENCRYPTED_*) 이지 레코드 단위 속성이 아니다. - 암호화할지의 결정은 레코드 단위에서 일어나서 (사용자 데이
터를 다루는 레코드를
prior_set_tde_encrypted) 그 레 코드를 담은 페이지로 promote 된다. 페이지에 사용자 데이 터 레코드가 하나라도 들어가면 그 페이지 전체가 flush 시에 암호화된다. - archive 로그 파일은 평문 으로 저장된다. active 로그의
TDE 플래그는 페이지가 archive 로 옮겨지기 전에
logpb_archive_active_log에 의해 클리어된다 (해당 코드 경로는tde_decrypt_log_page를 호출하고 archive 에는 평 문 페이지를 emit 한다). replication log 를 TDE 가 현 재 비활성화 상태이고 (UNSTABLE_TDE_FOR_REPLICATION_LOG), archive 파일이 replication 에 다시 쓰이기 때문이다. - flush 경로에서 암호화 실패 시 fail-open 동작 —
ER_TDE_ENCRYPTION_LOGPAGE_ERORR_AND_OFF_TDE를 raise 하 고 플래그를 클리어 — 은 소스 주석에 명시된 알려진 trade-off 다.
cubrid-disk-manager.md 와 비교. 디스크 매니저 문서는 볼
륨 헤더, sector table, file allocation table 을 다룬다. 이 페
이지들은 어느 것도 TDE 하에서 암호화되지 않는다 — 그 페이지
들의 pflag 는 0 인 채로 남는다. TDE 경계는 file_alloc 으
로 할당되는 사용자 페이지에 적용되고, 새 페이지가 파일 헤더에
서 TDE 플래그를 물려받는다. 볼륨 레이아웃은 파일 시스템 단위
공격자에게 여전히 노출된다.
마스터 키 파일 위치. 마스터 키 파일이 데이터베이스와 같
은 호스트 에 있다는 암묵적 가정에 주의한다. 기본값이다 — 데
이터베이스와 colocate 된 <db>_keys. KMS 또는 HSM 통합은
tde_create_keys_file 과 파일 기반의 tde_find_mk /
tde_add_mk 를 키 매니저 호출로 바꿔 끼워야 한다. 현재 소스
에는 그런 통합이 없다.
Replication / HA 함의. 헤더가 비활성화된
UNSTABLE_TDE_FOR_REPLICATION_LOG 심볼을 언급한다. 이 상태에
서 HA 복제기는 평문 archive 로그를 받는다 — 즉 TDE 를 켠
primary 를 미러링하는 secondary 는 데이터를 암호화해 저장 (자
기 TDE 를 가진다) 하지만, 수신 하는 WAL 스트림은 네트워크
위에서 암호화되지 않는다. 코드 주석이 명시적으로 짚어 둔 횡단
관심사다. 워크어라운드는 네트워크 단의 암호화 (TLS) 다.
미해결 질문
섹션 제목: “미해결 질문”- HSM / KMS 통합.
tde_load_mk,tde_find_mk,tde_add_mk,tde_delete_mk가 모두 POSIX 파일을read/write/lseek로 직접 동작한다. 가장 깔끔한 확 장점은 이 네 함수를 가상화해서 플러그형 backend 로 바꾸는 것이다. 코드 경로의 다른 어느 곳도 마스터 키를 파일 시 스템 의미론을 가정하지 않는다는 점이 유리하다. 오늘은 그런 추상이 존재하지 않는다. - 표 단위 re-keying / 온라인 rekey.
tde_change_mk는 DEK 를 다시 래핑하기만 한다. 실제 DEK 회전은 모든 페이지 를 읽고 옛 DEK 로 복호화한 뒤 새 DEK 로 다시 써야 한다 — 알고리즘 플래그를file_apply_tde_algorithm이 하는 일과 비슷하지만, 본체를 완전히 다시 암호화하는 형태다. 현재 소스에는 그런 동작이 없으며, 쉽지 않은 설계 문제다. 페이지 LSA 가 nonce 역할을 하기 때문에 LSA 를 바꾸지 않고 in-place 로 다시 암호화하는 것은 CTR 유일성을 깨지 않고서는 불가능하 다. - 컬럼 / 행 단위 암호화. TDE 는 페이지 본체 전체를 암호화 한다. 속성 단위 암호화 (SQL Server Always Encrypted 와 비슷 한 형태) 는 클라이언트 측 지원이 필요한데, CUBRID 는 현재 그 표면을 노출하지 않는다. TDE 모듈은 순전히 페이지 단위 다.
- 상수 시간 MK 비교.
tde_validate_mk는 후보 MK 의 SHA-256 과 저장된 해시를 rawmemcmp로 비교한다. 타이밍 사이드 채널은 작지만 (32 바이트) 0 은 아니다. OpenSSL 의CRYPTO_memcmp가 이를 닫아 줄 것이다. - archive 로그 암호화. archive 로그에서 TDE 를 끈 것은 평 문 replication 을 허용하기 위한 의식적 선택이다. HA 링크의 암호화가 네트워크 단 (TLS) 에서 이뤄질 것을 전제하면 trade- off 는 받아들일 만하다. 그렇지 않다면 archive 시점에 다시 암호화하는 경로가 필요해진다.
- 키 파일 슬롯 회수.
tde_delete_mk는 슬롯을 invalid 로 표시하지만 파일을 compact 하지는 않는다. 키 회전이 여러 번 일어나면 파일이 128 슬롯 상한까지 선형으로 자란다. compaction 루틴은 존재하지 않는다. - 로그 암호화 실패 시 fail-open.
ER_TDE_ENCRYPTION_LOGPAGE_ERORR_AND_OFF_TDE경로는 암호화 가 실패하면 조용히 평문을 쓴다. 프라이버시 관점에서는 fail-closed (flush 를 거절하고 서버를 멈춘다) 가 더 안전할 수 있다. 현재 선택은 가용성을 우선한다. - keyinfo heap 자체에 대한 TDE.
TDE_KEYINFOblob 은 CUBRID heap 파일에 저장된다. 그 heap 파일은 TDE 플래그가 붙지 않은 상태여야 한다 (붙으면 순환이 된다 — 풀려면 DEK 가 필요한데 그것이 그 안에 래핑되어 있다). keyinfo HFID 의 파일 헤더가tde_algo == NONE임을 점검으로 확인할 가치 가 있다. 방어적 assertion 이 추가되면 좋다. - production 빌드의
tde_print_mk. 이 함수는 마스터 키 소재 (material) 를 stdout 으로 출력한다. admin 디버깅용으 로 노출 (tde_dump_mks+print_value) 되지만, admin 이 아닌 컨텍스트에서 우발적으로 호출되는 일을 가드하고 있는지 확인이 필요하다.
소비된 코드 경로:
src/storage/tde.hsrc/storage/tde.csrc/storage/page_buffer.c— encrypt-on-flush, decrypt-on-read,pflagaccessor, 복구 후크.src/storage/file_io.h—FILEIO_PAGE레이아웃,pflag상수, watermark / sanity helper.src/storage/file_io.c— TDE 접점이 단 하나 있다 (페이지 포맷 scrub 안의io_page->prv.tde_nonce = 0;).src/storage/file_manager.h,file_manager.c— 파일 단위 TDE 알고리즘. 페이지 할당이 이를 전파한다.src/storage/double_write_buffer.cpp— 확인됨: TDE 인지 코 드가 하나도 없다. DWB 는 설계상 내용 비의존이다.src/transaction/log_page_buffer.c— flush, read, archive 경계에서의 로그 페이지 encrypt / decrypt 후크.src/transaction/log_storage.hpp—LOG_HDRPAGE_FLAG_*,LOG_IS_PAGE_TDE_ENCRYPTED.src/transaction/log_append.cpp,log_append.hpp— prior node 의 TDE 플래그 (prior_set_tde_encrypted/prior_is_tde_encrypted).src/transaction/log_volids.hpp— 마스터 키 파일 mount 용 합성 volid.src/transaction/boot_sr.c— TDE 모듈로의 DB 생성 / 재시작 진입점.src/executables/util_admin.c,util_cs.c—cubrid tdeadmin 명령 파싱과 dispatch.
이론적 참고:
- FIPS 197 (AES 표준) — 블록 cipher.
- NIST SP 800-38A — 운영 모드. CUBRID 는 CTR 을 쓴다.
- KS X 1213 (ARIA) — 한국 표준 cipher. AES 와 함께 제공되는 대안 알고리즘.
- Database Internals (Petrov, 2019) — 스토리지 챕터의 encryption-at-rest 논의.
- 비교 엔진: Oracle TDE (테이블스페이스별 DEK + Wallet 마스 터), SQL Server TDE (세 단계 SMK / DMK / DEK 계층), MySQL InnoDB TDE (테이블스페이스별 DEK + keyring 플러그인), PostgreSQL TDE (개발 중, 클러스터 단위 DEK 와 선택적 별도 WAL 키).