7장을 한 문장으로 줄이면 **"디스크에 저장될 때만 암호화하고, 그 위(메모리·쿼리 처리)에서는 평문으로 다룬다"**가 된다. 이 방식을 **TDE(Transparent Data Encryption, 투명한 데이터 암호화)**라고 부르는데, 여기서 "투명"이 핵심이다 — 응용 프로그램도, 쿼리를 짜는 개발자도 데이터가 암호화돼 있다는 걸 전혀 신경 쓸 필요가 없다는 뜻이다. 그래서 인덱스도 그대로 다 쓸 수 있다. 이게 응용 프로그램이 직접 암호화하는 방식과 갈리는 결정적 지점이다.
# 7.1 MySQL 서버의 데이터 암호화
# "Data at Rest"만 암호화한다
MySQL의 암호화는 디스크와 InnoDB 버퍼 풀 사이의 I/O 레이어에서만 일어난다.
- 디스크 → 버퍼 풀로 읽을 때 복호화
- 버퍼 풀 → 디스크로 쓸 때 암호화
즉 버퍼 풀에 올라온 데이터, 쿼리가 처리되는 중간 과정은 전부 평문이다. 그래서 이걸 **"Data at Rest(저장된 데이터) 암호화"**라고 부른다. 반대말은 "Data in Transit(전송 중 데이터)"으로, 그건 네트워크 구간 암호화(SSL/TLS, 3장)의 영역이라 별개다.
이 구분이 왜 중요하냐면 — 암호화를 켜도 쿼리 성능이 거의 안 떨어지는 이유가 여기 있기 때문이다. 이미 버퍼 풀에 올라온 페이지를 읽는 쿼리는 복호화가 필요 없다. 디스크에서 새로 읽어야 할 때만 복호화 비용이 붙는다.
# 7.1.1 2단계 키 관리 — 마스터 키와 테이블스페이스 키
암호화 키가 딱 하나면 문제가 생긴다. 그 키를 바꾸려면 데이터 전체를 복호화했다가 새 키로 다시 암호화해야 한다. 테라바이트급 테이블이면 재앙이다. MySQL은 이걸 2단계(2-Tier) 키 관리로 푼다.
- 테이블스페이스 키(Tablespace Key): 실제 데이터를 암호화하는 키. 각 테이블마다 하나씩 있고, 암호화된 상태로 각 테이블 데이터 파일의 헤더에 저장된다. 이 키는 한 번 정해지면 절대 안 바뀐다.
- 마스터 키(Master Key): 그 테이블스페이스 키들을 암호화하는 키. MySQL 외부(keyring 파일이나 KMS)에 저장된다.
키를 교체할 때는 마스터 키만 바꾼다. 그러면 각 테이블의 테이블스페이스 키를 복호화 → 새 마스터 키로 재암호화만 하면 되고, 실제 데이터 파일은 전혀 안 건드린다. 이래서 키 교체가 순식간이다.
ALTER INSTANCE ROTATE INNODB MASTER KEY;
마스터 키를 관리하는 게 keyring 플러그인이다. 종류가 여럿인데 무료로 쓸 수 있는 건 커뮤니티 에디션의 keyring_file 하나뿐이고, 나머지(keyring_encrypted_file, KMIP 표준의 keyring_okv, keyring_aws)는 엔터프라이즈 에디션 전용이다.
암호화 알고리즘은 AES-256을 쓴다. 테이블스페이스 키는 AES-256-ECB, 실제 데이터는 AES-256-CBC.
# 7.1.2 암호화와 성능
앞서 말한 대로 버퍼 풀에 있는 데이터를 읽는 건 복호화가 필요 없어서 성능 차이가 없다. 비용은 디스크에서 새로 읽거나 디스크로 쓸 때만 붙는다. 책의 실측 기준으로:
- 읽기: 3~5% 정도 느려짐
- 쓰기: 5~6% 정도 느려짐
밀리초 단위로는 수치가 워낙 낮아서 크게 체감되지 않을 수 있다. AES 암호화는 평문 길이가 짧으면 결과가 오히려 커질 수 있지만, 데이터 페이지는 원본이 이미 커서(16KB) 암호화해도 크기가 그대로라 저장 공간 낭비도 없다.
# 7.1.3 암호화와 복제
복제(Replication)에서 주의할 점 하나. 마스터 키는 소스 서버와 레플리카가 각자 다른 걸 가진다(원칙적으로). 키 관리 솔루션을 공유하면 같을 수도 있지만, 원칙은 각자 관리다. 그런데 테이블스페이스 키는 데이터 파일에 담겨 그대로 복제되므로 소스와 레플리카가 동일하다. 결국 양쪽의 실제 데이터 파일 내용은 완전히 똑같다.
keyring 파일 백업을 잊지 마라
마스터 키가 담긴 keyring 파일을 잃어버리면 암호화된 데이터를 영영 못 읽는다. 데이터 파일만 백업하고 keyring 파일을 안 챙기면 복구가 불가능하다. 데이터 백업 방식과는 별개로 keyring 파일 백업 방식을 반드시 따로 마련해야 한다. 게다가 마스터 키는 주기적으로 교체(rotate)되므로, 백업에도 최신 keyring 파일이 반영돼야 한다.
# 7.2 keyring_file 플러그인 설치
무료로 쓸 수 있는 keyring_file 플러그인은 TDE의 마스터 키를 로컬 디스크의 파일에 저장한다. 설정은 my.cnf에 두 줄이다.
early-plugin-load = keyring_file.so
keyring_file_data = /very/secure/directory/tde_master.key
early-plugin-load인 이유는, TDE 플러그인은 MySQL 서버가 시작되는 가장 이른 단계에서 초기화돼야 하기 때문이다(다른 플러그인보다 먼저).
keyring_file의 근본적 한계
keyring_file은 마스터 키를 평문 그대로 로컬 디스크에 저장한다. 그래서 이 키 파일이 데이터 파일과 같은 디스크에 있으면 암호화의 의미가 없다 — 디스크째로 털리면 데이터도 키도 함께 넘어간다. 반드시 데이터와 물리적으로 분리된 안전한 디스크에 둬야 한다. 그래서 실무·상용 환경에선 HashiCorp Vault 등과 연동하는 keyring_vault(Percona) 같은 대안이 권장된다.
# 7.3 테이블 암호화
# 7.3.1 테이블 생성
테이블 생성 구문에 ENCRYPTION='Y' 옵션만 붙이면 된다.
CREATE TABLE tab_encrypted (
id INT,
data VARCHAR(100),
PRIMARY KEY(id)
) ENCRYPTION='Y';
암호화된 테이블은 조회해도 평범하게 평문으로 나온다(투명하니까). 서버 전체에 기본 적용하려면 default_table_encryption=ON으로 두면 된다. 암호화된 테이블 목록은 information_schema.TABLES의 CREATE_OPTIONS에서 확인한다.
# 7.3.2 응용 프로그램 암호화와의 비교 — 7장에서 제일 중요한 대목
"어차피 암호화할 거면 응용 프로그램에서 직접 암호화해서 넣으면 되지 않나?"라는 의문이 자연스럽다. 그런데 결정적 차이가 있다.
응용 프로그램이 값을 직접 암호화해서 저장하면, MySQL 입장에선 그게 암호화된 값인지 알 방법이 없다. 그냥 이상한 바이너리 문자열일 뿐이다. 그래서 그 컬럼에 인덱스를 걸어도 인덱스 기능을 100% 못 쓴다.
-- 응용 프로그램이 birth_year를 암호화해서 넣은 컬럼(enc_birth_year)에 인덱스가 있어도...
SELECT * FROM app_user WHERE enc_birth_year = #{encryptedYear}; -- 이건 됨 (정확히 일치)
SELECT * FROM app_user WHERE enc_birth_year BETWEEN #{min} AND #{max}; -- 범위 검색 불가!
SELECT * FROM app_user ORDER BY enc_birth_year; -- 정렬 불가!
암호문은 원본의 대소 관계·순서를 보존하지 않으므로, 범위 검색(BETWEEN, <, >)과 정렬(ORDER BY)이 완전히 깨진다. = 완전 일치만 겨우 된다.
반면 TDE는 인덱스 관련 작업을 전부 다 처리한 후, 디스크에 저장하기 직전에만 암호화한다. 그래서 인덱스가 평문 기준으로 만들어지고, 범위 검색이든 정렬이든 아무 제약이 없다. 이게 TDE의 가장 큰 장점이다.
🐘 PostgreSQL 비교 — PostgreSQL은 코어에 TDE가 없다. 그래서 저장 데이터 암호화는 보통 디스크/파일 시스템 레벨(LUKS 등)이나 상용 포크(EDB Postgres Advanced Server)로 해결한다. PG의
pgcrypto확장으로 컬럼 단위 암호화는 가능하지만, 그건 정확히 위의 "응용 프로그램 암호화"에 해당해서 인덱스 제약(범위·정렬 불가)이 그대로 따라온다. MySQL의 TDE에 딱 대응하는 코어 기능은 PG엔 없다고 보면 된다.
# 7.3.3 테이블스페이스 이동
TDE 테이블은 원본 서버와 목적 서버의 마스터 키가 다르다. 그래서 데이터 파일을 그냥 복사하면 목적 서버에서 못 읽는다. FLUSH TABLES ... FOR EXPORT로 내보낼 때 생성되는 .cfg(구조 정보)와 .cfp(임시 마스터 키) 파일을 데이터 파일과 함께 복사해야 한다. .cfp가 없으면 복구 불가다.
# 7.4 언두 로그 및 리두 로그 암호화
여기 함정이 하나 있다. 테이블을 암호화해도, InnoDB의 언두 로그와 리두 로그에는 그 데이터가 평문으로 남을 수 있다. 테이블 데이터가 이 로그들을 거쳐 가는데 로그가 평문이면 암호화가 반쪽이 된다. 그래서 8.0.16부터 이 로그들도 따로 암호화할 수 있다.
SET GLOBAL innodb_undo_log_encrypt = ON;
SET GLOBAL innodb_redo_log_encrypt = ON;
켠 시점 이후의 로그만 암호화된다
이 변수를 켜면 그 이후에 새로 기록되는 리두/언두 로그만 암호화된다. 켜기 전에 이미 쓰인 로그는 평문으로 남는다. 반대로 껐을 때도 이미 암호화된 로그는 그대로 암호화 상태로 남아, 복호화 키가 여전히 필요하다.
# 7.5 바이너리 로그 암호화
리두·언두 로그와 같은 논리로, 바이너리 로그(binlog)와 릴레이 로그(relay log)도 기본은 평문이다. 바이너리 로그에는 시간에 따른 데이터 변경 내역이 다 담기니 민감할 수 있고, 증분 백업(Incremental Backup)에 쓰이기도 해서 암호화 중요도가 높다.
# 7.5.1 키 관리 & 7.5.2 키 변경
바이너리 로그 암호화도 TDE와 똑같은 2단계 키 관리다 — File Key로 각 로그 파일을 암호화하고, **바이너리 로그 암호화 키(마스터)**로 그 File Key를 암호화한다. 키 교체는 역시 마스터만 바꾼다.
ALTER INSTANCE ROTATE BINLOG MASTER KEY;
SHOW BINARY LOGS; -- Encrypted 컬럼으로 암호화 여부 확인
# 7.5.3 mysqlbinlog 도구의 제약
바이너리 로그를 사람이 읽을 땐 mysqlbinlog 도구를 쓰는데, 암호화된 로그 파일은 이 도구로 직접 못 읽는다.
$ mysqlbinlog -vvv mysql-bin.000011
# ERROR: Reading encrypted log files directly is not supported.
암호화 키는 그 로그를 만든 MySQL 서버가 갖고 있어서, 파일만 떼어와선 복호화가 안 되기 때문이다. 대신 --read-from-remote-server 옵션으로 서버에 접속해서 읽으면 서버가 복호화해서 넘겨준다.
$ mysqlbinlog --read-from-remote-server --host=... mysql-bin.000011
# 정리
7장에서 실무로 들고 갈 것.
- **MySQL 암호화 = TDE = "저장될 때만 암호화(Data at Rest)"**다. 버퍼 풀·쿼리 처리 중엔 평문이라, 이미 버퍼 풀에 있는 데이터를 읽는 쿼리는 성능 저하가 없다(읽기 3~5%, 쓰기 5~6%는 디스크 I/O가 일어날 때만).
- 응용 프로그램에서 직접 암호화하지 말고 TDE를 써라. 앱이 암호화하면 그 컬럼은 인덱스로 범위 검색·정렬이 불가능해진다. TDE는 인덱스를 다 처리한 뒤 저장 직전에만 암호화해서 이 제약이 없다.
- keyring 파일(마스터 키)을 반드시 별도로 백업하라. 잃어버리면 데이터를 영영 못 읽는다. 그리고
keyring_file은 키를 평문 저장하니 데이터와 다른 디스크에 둬라. - **키 교체는
ALTER INSTANCE ROTATE ... MASTER KEY**로 순식간에 끝난다(마스터 키만 재암호화, 데이터 파일은 안 건드림). 2단계 키 관리 덕분이다. - 테이블만 암호화하면 반쪽이다. 언두·리두 로그(
innodb_undo/redo_log_encrypt)와 바이너리 로그도 같이 암호화해야 데이터가 평문으로 새지 않는다. 단, 켠 시점 이후 로그만 적용된다. - 암호화된 바이너리 로그는
mysqlbinlog로 직접 못 읽는다.--read-from-remote-server로 서버를 거쳐야 한다.