dm-crypt — 블록 레벨 투명 암호화
dm-crypt는 Linux Device Mapper 프레임워크 위에서 블록 레벨 투명 암호화를 제공합니다.
LUKS2 키 관리, AES-XTS/Adiantum 알고리즘, 커널 Crypto API 기반 암/복호화,
성능 최적화(no_read_workqueue, 하드웨어 가속), FIPS 140-2/3 준수,
그리고 dm-integrity와의 AEAD 결합까지 커널 내부 구현(drivers/md/dm-crypt.c)을 심층 분석합니다.
- dm-verity — Merkle Tree 기반 읽기 전용 블록 검증
- dm-integrity — 읽기/쓰기 무결성 보호, 저널링, AEAD 결합
- Device Mapper / LVM — DM 프레임워크 기본
핵심 요약
- Device Mapper — 블록 디바이스 위에 가상 매핑(Mapping) 레이어를 제공하는 커널 프레임워크
- dm-crypt — 블록 레벨 투명 암호화 (LUKS, AES-XTS, Adiantum)
- LUKS2 — JSON 메타데이터, Argon2id KDF, 최대 32개 키 슬롯, AEAD 지원
- 성능 최적화 — no_read/write_workqueue, submit_from_crypt_cpus, AES-NI/ICE 가속
- AEAD 결합 — dm-crypt + dm-integrity를 결합한 인증 암호화 (AES-GCM)
단계별 이해
- Device Mapper 레이어 이해
커널의 블록 I/O 스택에서 DM은 bio를 가로채 target 드라이버에 전달합니다. - dm-crypt로 암호화
섹터 단위로 AES-XTS 암호화를 적용하여 디스크 데이터를 보호합니다. - LUKS 키 관리
LUKS2는 Argon2id KDF, 다중 키 슬롯, TPM/FIDO2 토큰을 지원합니다. - 성능 최적화
워크큐 바이패스, 하드웨어 가속, NUMA 친화 설정으로 오버헤드를 최소화합니다.
Device Mapper 개요
Device Mapper(DM)는 Linux 커널의 블록 레이어에서 가상 블록 디바이스를 생성하는 프레임워크입니다.
drivers/md/dm.c에 구현된 DM 코어는 struct mapped_device로 가상 디바이스를 표현하고,
struct dm_table로 매핑 테이블을 관리합니다. 각 target 드라이버(struct target_type)는
특정 기능(암호화, 검증, 스냅샷 등)을 구현하며, DM 코어가 bio를 적절한 target에 라우팅(Routing)합니다.
DM은 원래 Heinz Mauelshagen이 LVM(Logical Volume Manager)을 위해 개발했으며,
현재는 dm-crypt, dm-verity, dm-integrity, dm-thin, dm-cache, dm-snapshot 등
다양한 target을 통해 리눅스 스토리지 스택의 핵심 계층으로 자리잡았습니다.
사용자 공간(User Space)에서는 dmsetup, cryptsetup, veritysetup,
integritysetup 등의 도구로 DM 디바이스를 관리합니다.
DM 코어의 핵심 자료 구조를 살펴봅니다:
/* include/linux/device-mapper.h */
struct target_type {
uint64_t features;
const char *name;
struct module *module;
int (*ctr)(struct dm_target *ti, unsigned argc, char **argv);
void (*dtr)(struct dm_target *ti);
int (*map)(struct dm_target *ti, struct bio *bio);
int (*end_io)(struct dm_target *ti, struct bio *bio,
blk_status_t *error);
void (*status)(struct dm_target *ti, status_type_t type,
unsigned status_flags, char *result,
unsigned maxlen);
int (*iterate_devices)(struct dm_target *ti,
iterate_devices_callout_fn fn, void *data);
};
struct dm_target {
struct dm_table *table;
struct target_type *type;
sector_t begin; /* 매핑 시작 섹터 */
sector_t len; /* 매핑 길이 (섹터) */
void *private; /* target 별 사적 데이터 */
unsigned num_discard_bios;
unsigned num_secure_erase_bios;
unsigned num_write_zeroes_bios;
};
DM의 I/O 경로는 다음과 같이 진행됩니다: 사용자 공간에서 가상 디바이스(/dev/dm-N)에 I/O를 발행하면,
dm_submit_bio()가 호출됩니다. 이 함수는 dm_table에서 bio가 속하는 target을 찾아
해당 target의 map() 콜백(Callback)을 호출합니다. target은 bio를 변환(remap, split, encrypt 등)하여
하위 디바이스로 전달합니다. map() 콜백의 반환값에 따라 DM 코어가 bio의 후속 처리를 결정합니다:
DM_MAPIO_REMAPPED는 bio가 하위 디바이스로 redirect되었음을,
DM_MAPIO_SUBMITTED는 target이 bio를 완전히 처리했음을 의미합니다.
| DM 구성 요소 | 역할 | 소스 파일 |
|---|---|---|
mapped_device | 가상 블록 디바이스 표현 | drivers/md/dm.c |
dm_table | target 매핑 테이블 | drivers/md/dm-table.c |
dm_target | 개별 매핑 세그먼트 | drivers/md/dm.h |
dm_io | I/O 요청 추적 | drivers/md/dm.c |
dm_ioctl | 사용자 공간 인터페이스 | drivers/md/dm-ioctl.c |
dm_bufio | 해시/메타데이터 블록 캐시(Cache) | drivers/md/dm-bufio.c |
시작섹터 길이 target_name [target_args...] 형식입니다.
예: 0 2097152 crypt aes-xts-plain64 KEY 0 /dev/sdb1 0
/* drivers/md/dm-ioctl.c - 주요 ioctl 명령 */
/* 사용자 공간 (dmsetup) <-> 커널 DM 인터페이스 */
/* DM_DEV_CREATE: 새 mapped_device 생성 */
/* DM_DEV_REMOVE: mapped_device 제거 */
/* DM_TABLE_LOAD: 매핑 테이블 로드 */
/* DM_DEV_SUSPEND: I/O 일시 중지 (테이블 교체 준비) */
/* DM_DEV_STATUS: 디바이스 상태 조회 */
/* DM_TABLE_STATUS: 테이블 상태 (dmsetup status) */
/* DM_TARGET_MSG: target에 메시지 전달 (dmsetup message) */
static int table_load(struct file *filp,
struct dm_ioctl *param,
size_t param_size)
{
struct dm_table *t;
struct mapped_device *md;
/* 1. 테이블 생성 */
r = dm_table_create(&t, get_mode(param), param->target_count, md);
/* 2. 각 target 파싱 및 추가 */
for (i = 0; i < param->target_count; i++) {
dm_table_add_target(t, type_name, start, len, params);
/* 내부에서 target_type->ctr() 호출 */
}
/* 3. 테이블 완료 (무결성 검증) */
r = dm_table_complete(t);
/* 4. 테이블을 mapped_device에 연결 */
dm_swap_table(md, t);
return 0;
}
/* bio 라우팅 - dm_submit_bio() 핵심 경로 */
static void dm_submit_bio(struct bio *bio)
{
struct mapped_device *md = bio->bi_bdev->bd_disk->private_data;
struct dm_table *map;
struct dm_target *ti;
map = dm_get_live_table(md, &srcu_idx);
/* bio가 속하는 target 찾기 */
ti = dm_table_find_target(map, bio->bi_iter.bi_sector);
/* target의 map() 콜백 호출 */
r = ti->type->map(ti, bio);
switch (r) {
case DM_MAPIO_SUBMITTED:
break; /* target이 처리 완료 */
case DM_MAPIO_REMAPPED:
submit_bio_noacct(bio); /* 하위 디바이스로 전달 */
break;
case DM_MAPIO_REQUEUE:
bio_io_error(bio); /* 재시도 */
break;
}
dm_put_live_table(md, srcu_idx);
}
물리 디바이스 -> dm-integrity -> dm-crypt -> ext4
이 경우 각 레이어의 map()이 순서대로 호출됩니다.
스택 깊이가 깊을수록 I/O 지연(Latency)이 증가하므로, AEAD 모드처럼
단일 레이어에서 암호화+무결성을 처리하는 것이 성능상 유리합니다.
dm-crypt 개요
dm-crypt는 블록 레벨 투명 암호화를 제공하는 Device Mapper target입니다.
drivers/md/dm-crypt.c에 구현되어 있으며, LUKS(Linux Unified Key Setup) 포맷과 함께
리눅스의 표준 디스크 암호화 솔루션으로 사용됩니다.
모든 읽기/쓰기 I/O가 dm-crypt를 통과하면서 투명하게 암/복호화(Decryption)됩니다.
LUKS2(Linux Unified Key Setup version 2)는 dm-crypt의 표준 온디스크 포맷입니다. LUKS1과 비교하여 JSON 메타데이터, Argon2id PBKDF, 최대 32개 키 슬롯, 큰 헤더 크기(최대 64MB), 인증 암호화(AEAD) 지원 등이 추가되었습니다.
| 속성 | LUKS1 | LUKS2 |
|---|---|---|
| 메타데이터 형식 | 바이너리 헤더 | JSON + 바이너리 |
| 키 슬롯 | 최대 8개 | 최대 32개 |
| PBKDF | PBKDF2 | Argon2id (기본), PBKDF2 |
| 헤더 크기 | 고정 2MB | 가변 (최대 64MB) |
| AEAD 지원 | 미지원 | 지원 (dm-integrity 결합) |
| 토큰 시스템 | 미지원 | systemd-cryptenroll 연동 |
| 온라인 키 변경 | 미지원 | 지원 |
| 무결성 보호 | 미지원 | HMAC/AEAD 지원 |
/* LUKS2 온디스크 헤더 구조 (간략화) */
struct luks2_hdr_disk {
char magic[6]; /* "LUKSº¾" */
uint16_t version; /* 2 */
uint64_t hdr_size; /* 전체 헤더 크기 */
uint64_t seqid; /* 시퀀스 ID (원자적 업데이트) */
char label[48]; /* 볼륨 라벨 */
char checksum_alg[32]; /* "sha256" */
uint8_t salt[64]; /* 헤더 salt */
char uuid[40]; /* UUID */
char subsystem[48]; /* 서브시스템 */
uint64_t hdr_offset; /* JSON 영역 시작 */
char _padding[184];
uint8_t csum[64]; /* 헤더 체크섬 */
/* 이후: JSON 메타데이터 영역 */
/* 이후: 키 슬롯 바이너리 영역 */
};
/* LUKS2 JSON 메타데이터 예시 (cryptsetup luksDump) */
/*
{
"keyslots": {
"0": {
"type": "luks2",
"key_size": 64,
"af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
"kdf": {
"type": "argon2id",
"time": 4,
"memory": 1048576,
"cpus": 4,
"salt": "base64..."
},
"area": {
"type": "raw",
"offset": "32768",
"size": "258048",
"encryption": "aes-xts-plain64",
"key_size": 64
}
}
},
"segments": {
"0": {
"type": "crypt",
"offset": "16777216",
"size": "dynamic",
"iv_tweak": "0",
"encryption": "aes-xts-plain64",
"sector_size": 512
}
}
}
*/
--pbkdf-memory 옵션으로 메모리를 줄일 수 있습니다.
/* drivers/md/dm-crypt.c */
struct crypt_config {
struct dm_dev *dev;
sector_t start; /* 암호화 시작 섹터 */
struct crypto_skcipher **tfms; /* 대칭키 암호 핸들 (CPU별) */
unsigned int tfms_count; /* CPU 수만큼 할당 */
char *cipher_string; /* "aes-xts-plain64" 등 */
/* IV 생성 */
const struct crypt_iv_operations *iv_gen_ops;
char *iv_mode; /* "plain64", "essiv" 등 */
unsigned int iv_size;
/* 키 관리 */
u8 *key;
unsigned int key_size;
unsigned int key_parts;
struct key *key_desc; /* keyring 키 */
/* 워크큐 */
struct workqueue_struct *io_queue;
struct workqueue_struct *crypt_queue;
mempool_t page_pool;
unsigned int num_write_bios;
};
# LUKS2 볼륨 생성 (AES-256-XTS)
$ cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha256 \
--pbkdf argon2id \
/dev/sdb1
# LUKS 볼륨 열기
$ cryptsetup open /dev/sdb1 encrypted_disk
# plain dm-crypt (LUKS 없이)
$ echo "0 $(blockdev --getsz /dev/sdb1) crypt aes-xts-plain64 \
$(xxd -l 64 -p /dev/urandom | tr -d '\n') \
0 /dev/sdb1 0" | dmsetup create plain_crypt
암호화 알고리즘
dm-crypt는 커널 Crypto API를 통해 다양한 암호화 알고리즘을 지원합니다.
알고리즘 문자열은 cipher-chainmode-ivmode 형식으로 지정합니다.
| 알고리즘 | 형식 | 키 크기 | 특징 | 권장 용도 |
|---|---|---|---|---|
| AES-XTS | aes-xts-plain64 | 256/512비트 | 표준, 하드웨어 가속 | 일반 디스크 암호화 |
| AES-CBC-ESSIV | aes-cbc-essiv:sha256 | 128/256비트 | 레거시, Watermark 방어 | 호환성 필요 시 |
| Adiantum | xchacha12,aes-adiantum-plain64 | 256비트 | AES-NI 없는 환경 최적 | 저사양 ARM 디바이스 |
| AES-GCM | aes-gcm-random | 256비트 | AEAD (인증 암호화) | dm-integrity 결합 |
| Twofish-XTS | twofish-xts-plain64 | 256/512비트 | AES 대안 | AES 회피 정책 |
| Serpent-XTS | serpent-xts-plain64 | 256/512비트 | 보안 마진 최대 | 최고 보안 요구 |
/* drivers/md/dm-crypt.c - IV 생성 운영 구조체 */
static const struct crypt_iv_operations crypt_iv_plain64_ops = {
.generator = crypt_iv_plain64_gen,
};
static int crypt_iv_plain64_gen(struct crypt_config *cc,
u8 *iv,
struct dm_crypt_request *dmreq)
{
/* IV = 섹터 번호 (64비트 리틀 엔디언) */
memset(iv, 0, cc->iv_size);
*(__le64 *)iv = cpu_to_le64(dmreq->iv_sector);
return 0;
}
/* Adiantum: ChaCha + AES로 구성된 와이드 블록 암호 */
/* AES-NI가 없는 ARMv7/저사양 환경에서 AES-XTS보다 5배 빠름 */
/* Google이 Android Go 디바이스용으로 커널 5.0에 추가 */
IV 생성 모드 상세 비교
dm-crypt의 IV(Initialization Vector) 생성 모드는 동일한 평문 블록이 동일한 암호문으로 변환되는 것을 방지합니다. 각 섹터마다 고유한 IV를 생성하여 ECB 모드의 취약점(Vulnerability)을 회피합니다. 주요 IV 생성 모드를 상세히 비교합니다.
| IV 모드 | IV 생성 방법 | 보안 수준 | 성능 | 호환성 |
|---|---|---|---|---|
plain | 섹터 번호 (32비트) | 낮음 (2TB 이상 반복) | 최고 | 레거시 |
plain64 | 섹터 번호 (64비트) | 양호 | 최고 | 권장 |
plain64be | 섹터 번호 (64비트 빅 엔디안(Endianness)) | 양호 | 최고 | 특수 HW |
essiv | E(key_hash, 섹터번호) | 우수 (Watermark 방어) | 약간 느림 | LUKS1 기본 |
benbi | 섹터 번호 빅 엔디안 (128비트) | 양호 | 최고 | 하드웨어 AES |
eboiv | 암호화된 블록의 IV | 우수 | 약간 느림 | Bitlocker 호환 |
elephant | Elephant diffuser + ESSIV | 우수 | 느림 | Bitlocker 레거시 |
random | 랜덤 IV (AEAD 전용) | 최고 | 추가 저장 필요 | AEAD 모드 |
/* ESSIV (Encrypted Salt-Sector Initialization Vector) */
/* Watermark 공격: 공격자가 섹터 번호를 알면 plain64에서
* 동일 평문의 섹터를 식별할 수 있음
* ESSIV는 섹터 번호를 AES(hash(key))로 암호화하여 방어 */
static int crypt_iv_essiv_gen(struct crypt_config *cc,
u8 *iv,
struct dm_crypt_request *dmreq)
{
/* IV = AES_ECB(SHA256(master_key), sector_number) */
memset(iv, 0, cc->iv_size);
*(__le64 *)iv = cpu_to_le64(dmreq->iv_sector);
/* ESSIV 전용 cipher로 섹터 번호 암호화 */
crypto_cipher_encrypt_one(cc->iv_gen_private.essiv.tfm,
iv, iv);
return 0;
}
/* XTS 모드에서는 ESSIV가 불필요 (내장 tweak 메커니즘)
* AES-XTS: T = AES(key2, sector_number) * alpha^j
* 각 16바이트 블록마다 고유한 tweak 값 적용 */
인라인 암호화 엔진 (ICE) 연동
최신 모바일 SoC(Qualcomm, Samsung, MediaTek)에는 Inline Crypto Engine(ICE)이
내장되어 있어, dm-crypt 없이도 하드웨어 레벨에서 블록 암호화를 수행합니다.
Linux 커널은 blk-crypto 프레임워크(block/blk-crypto.c)를 통해
ICE를 활용하며, dm-crypt와 투명하게 대체됩니다.
/* block/blk-crypto.c - 인라인 암호화 프레임워크 */
struct blk_crypto_key {
struct blk_crypto_config crypto_cfg;
unsigned int data_unit_size;
unsigned int size;
u8 raw[BLK_CRYPTO_MAX_KEY_SIZE];
};
/* bio에 암호화 컨텍스트 연결 */
void bio_crypt_set_ctx(struct bio *bio,
const struct blk_crypto_key *key,
u64 dun_lo, gfp_t gfp_mask)
{
struct bio_crypt_ctx *bc;
bc = mempool_alloc(bio_crypt_ctx_pool, gfp_mask);
bc->bc_key = key;
bc->bc_dun[0] = dun_lo;
bio->bi_crypt_context = bc;
}
/* 하드웨어가 blk-crypto를 지원하지 않으면
* blk-crypto-fallback이 소프트웨어로 처리 (= dm-crypt과 유사) */
| 방식 | 암호화 위치 | CPU 오버헤드 | 지원 플랫폼 |
|---|---|---|---|
| dm-crypt | 소프트웨어 (커널) | 높음 (AES-NI 시 낮음) | 모든 플랫폼 |
| ICE (blk-crypto) | 스토리지 컨트롤러 HW | 제로 | Qualcomm, Samsung SoC |
| SED (OPAL) | 드라이브 내부 HW | 제로 | OPAL 호환 SSD |
| blk-crypto-fallback | 소프트웨어 (대체) | dm-crypt과 유사 | ICE 미지원 시 |
dm-crypt 성능
dm-crypt의 성능은 암호화 연산의 병렬화, 하드웨어 가속, 워크큐 설정에 크게 의존합니다.
커널 5.x 이후 submit_from_crypt_cpus, no_read_workqueue,
no_write_workqueue 플래그가 추가되어 성능 최적화가 가능합니다.
# 성능 최적화된 dm-crypt 설정
$ cryptsetup open --type luks2 /dev/nvme0n1p2 encrypted \
--perf-no_read_workqueue \
--perf-no_write_workqueue \
--perf-submit_from_crypt_cpus
# 런타임에 플래그 변경
$ dmsetup table encrypted --showkeys | \
sed 's/$/ 3 no_read_workqueue no_write_workqueue submit_from_crypt_cpus/' | \
dmsetup reload encrypted
$ dmsetup resume encrypted
# fio 벤치마크: 암호화 오버헤드 측정
$ fio --name=randread --ioengine=io_uring --iodepth=64 \
--rw=randread --bs=4k --direct=1 --numjobs=4 \
--filename=/dev/mapper/encrypted --runtime=30 \
--group_reporting
| 시나리오 | NVMe 직접 | dm-crypt 기본 | dm-crypt 최적화 | 오버헤드 |
|---|---|---|---|---|
| 순차 읽기 | 3,500 MB/s | 2,800 MB/s | 3,200 MB/s | ~8% |
| 순차 쓰기 | 3,000 MB/s | 2,200 MB/s | 2,700 MB/s | ~10% |
| 랜덤 4K 읽기 | 800K IOPS | 600K IOPS | 720K IOPS | ~10% |
| 랜덤 4K 쓰기 | 500K IOPS | 350K IOPS | 430K IOPS | ~14% |
cryptsetup benchmark로 시스템의 암호화 처리량을 확인할 수 있습니다.
/* drivers/md/dm-crypt.c - crypt_map() 핵심 */
static int crypt_map(struct dm_target *ti, struct bio *bio)
{
struct crypt_config *cc = ti->private;
struct dm_crypt_io *io;
/* FLUSH/PREFLUSH 바이패스 */
if (unlikely(bio->bi_opf & REQ_PREFLUSH)) {
bio_set_dev(bio, cc->dev->bdev);
return DM_MAPIO_REMAPPED;
}
io = dm_per_bio_data(bio, cc->per_bio_data_size);
crypt_io_init(io, cc, bio, bio->bi_iter.bi_sector);
if (bio_data_dir(bio) == READ) {
if (cc->on_disk_tag_size) {
/* AEAD 모드: 태그도 함께 읽기 */
io->ctx.bio_in = bio;
io->ctx.iter_in = bio->bi_iter;
}
crypt_read(io);
} else {
crypt_write(io);
}
return DM_MAPIO_SUBMITTED;
}
/* 암호화 변환 - CPU별 tfm 사용으로 락 경합 방지 */
static int crypt_convert(struct crypt_config *cc,
struct convert_context *ctx,
bool aead_recheck,
bool reset_pending)
{
unsigned int tag_offset = 0;
sector_t sector = ctx->cc_sector;
int r;
/* CPU별 암호 핸들 선택 (lock-free) */
struct crypto_skcipher *tfm =
cc->tfms[smp_processor_id() % cc->tfms_count];
while (ctx->iter_in.bi_size) {
/* IV 생성 */
cc->iv_gen_ops->generator(cc, org_iv, &dmreq->iv_sector);
/* skcipher 비동기 요청 */
skcipher_request_set_crypt(req, &sg_in, &sg_out,
cc->sector_size, org_iv);
if (bio_data_dir(ctx->bio_in) == WRITE)
r = crypto_skcipher_encrypt(req);
else
r = crypto_skcipher_decrypt(req);
sector++;
}
return 0;
}
num_online_cpus()만큼의 crypto_skcipher 핸들을 할당합니다.
각 CPU가 자신의 핸들을 사용하므로 락 경합(Contention) 없이 병렬 암호화가 가능합니다.
이 설계는 멀티코어 시스템에서 선형적인 성능 확장을 제공합니다.
예를 들어 8코어 시스템에서 AES-XTS의 총 처리량은 약 20-30GB/s에 달합니다.
dm-crypt I/O 경로 상세
dm-crypt의 쓰기 경로(Write Path)는 사용자 공간에서 발생한 bio가
crypt_map()을 거쳐 crypt_convert()에서 암호화된 후
하위 디바이스로 전달되는 복잡한 과정을 거칩니다.
이 경로를 상세히 이해하면 성능 병목 지점을 정확히 파악하고 최적화할 수 있습니다.
쓰기 경로의 핵심 단계를 코드 수준에서 분석합니다. crypt_write()는
원본 bio의 데이터를 bounce 페이지에 복사한 후, crypt_convert()로
섹터 단위 암호화를 수행합니다. 암호화가 완료되면 암호문이 담긴 새 bio를
하위 디바이스로 전달합니다.
/* drivers/md/dm-crypt.c - 쓰기 경로 상세 분석 */
/* 1단계: crypt_write() - 쓰기 I/O 시작점 */
static void crypt_write(struct dm_crypt_io *io)
{
struct crypt_config *cc = io->cc;
/* no_write_workqueue가 설정되면 현재 컨텍스트에서 직접 암호화 */
if (test_bit(DM_CRYPT_NO_WRITE_WORKQUEUE, &cc->flags)) {
crypt_convert_write(io);
return;
}
/* 기본 경로: kcryptd 워크큐에 큐잉 */
INIT_WORK(&io->work, kcryptd_crypt_write_io_submit);
queue_work(cc->crypt_queue, &io->work);
}
/* 2단계: bounce 페이지 할당 및 암호화 */
static void crypt_convert_write(struct dm_crypt_io *io)
{
struct crypt_config *cc = io->cc;
struct bio *clone;
int r;
/* clone bio 할당 (bounce 페이지 사용) */
clone = crypt_alloc_buffer(io, io->base_bio->bi_iter.bi_size);
if (unlikely(!clone)) {
io->error = BLK_STS_RESOURCE;
crypt_dec_pending(io);
return;
}
io->ctx.bio_out = clone;
io->ctx.iter_out = clone->bi_iter;
/* 섹터 단위 암호화 수행 */
r = crypt_convert(cc, &io->ctx, false, true);
if (r == -EINPROGRESS) {
/* 비동기 암호화 진행 중 (HW 가속 시) */
return;
}
if (r == -EAGAIN) {
/* 메모리 부족 → 워크큐에서 재시도 */
queue_work(cc->crypt_queue, &io->work);
return;
}
/* 암호화 완료 → 하위 디바이스로 전달 */
crypt_write_done(io);
}
crypt_convert() 함수 상세 분석
crypt_convert()는 dm-crypt의 핵심 암호화 루프입니다.
입력 bio의 각 섹터에 대해 IV를 생성하고, 커널 Crypto API의
skcipher 인터페이스를 통해 암호화/복호화를 수행합니다.
CPU별로 독립적인 crypto_skcipher 핸들을 사용하여 락 경합 없이 병렬 처리합니다.
/* drivers/md/dm-crypt.c - crypt_convert() 전체 분석 */
static int crypt_convert(struct crypt_config *cc,
struct convert_context *ctx,
bool aead_recheck,
bool reset_pending)
{
struct crypt_config *cc = io->cc;
unsigned int tag_offset = 0;
unsigned int sector_step = cc->sector_size >> SECTOR_SHIFT;
int r;
/* CPU별 암호 핸들 선택 — 락 프리 병렬 처리 */
struct skcipher_request *req;
unsigned int key_index = cyclic_inc(&cc->tfms_idx) % cc->tfms_count;
if (reset_pending)
atomic_set(&ctx->cc_pending, 1);
while (ctx->iter_in.bi_size && ctx->iter_out.bi_size) {
/* 암호화 요청(Request) 할당 (mempool에서) */
req = crypt_alloc_req(cc, key_index);
if (IS_ERR(req)) {
r = PTR_ERR(req);
break;
}
/* IV 생성: plain64, essiv, random 등 */
cc->iv_gen_ops->generator(cc, org_iv,
&dmreq->iv_sector);
/* scatterlist 설정: 입력/출력 버퍼 */
sg_init_table(&sg_in, 1);
sg_set_page(&sg_in, bvec_page(&bv_in),
cc->sector_size, bv_in.bv_offset);
sg_init_table(&sg_out, 1);
sg_set_page(&sg_out, bvec_page(&bv_out),
cc->sector_size, bv_out.bv_offset);
/* skcipher 요청 설정 */
skcipher_request_set_crypt(req, &sg_in, &sg_out,
cc->sector_size, org_iv);
/* 비동기 완료 콜백 설정 */
skcipher_request_set_callback(req,
CRYPTO_TFM_REQ_MAY_BACKLOG,
kcryptd_async_done, &dmreq->ctx);
/* 암호화 또는 복호화 실행 */
if (bio_data_dir(ctx->bio_in) == WRITE)
r = crypto_skcipher_encrypt(req);
else
r = crypto_skcipher_decrypt(req);
switch (r) {
case 0:
/* 동기 완료 */
atomic_dec(&ctx->cc_pending);
ctx->cc_sector += sector_step;
continue;
case -EINPROGRESS:
case -EBUSY:
/* 비동기 진행 중 (HW 가속 등)
* kcryptd_async_done()에서 완료 처리 */
ctx->cc_sector += sector_step;
continue;
default:
/* 오류 발생 */
return r;
}
}
return 0;
}
crypt_alloc_req() / crypt_free_req() 메모리 관리
dm-crypt는 암호화 요청 객체를 효율적으로 관리하기 위해 mempool을 사용합니다.
crypt_alloc_req()는 미리 할당된 풀에서 skcipher_request를
가져오고, crypt_free_req()는 풀에 반환합니다.
이 패턴은 I/O 경로에서 메모리 할당 실패를 방지하는 핵심 전략입니다.
/* drivers/md/dm-crypt.c - 암호화 요청 메모리 관리 */
/* dm_crypt_request: 각 섹터 암호화 요청의 메타데이터 */
struct dm_crypt_request {
struct convert_context *ctx;
struct scatterlist sg_in[1];
struct scatterlist sg_out[1];
sector_t iv_sector;
unsigned int tag_offset;
u8 iv[]; /* IV 버퍼 (가변 크기) */
};
/* 요청 할당: mempool에서 가져옴 */
static struct skcipher_request *crypt_alloc_req(
struct crypt_config *cc,
unsigned int key_index)
{
struct skcipher_request *req;
struct dm_crypt_request *dmreq;
/* mempool에서 할당 — GFP_NOIO 컨텍스트에서도 안전
* mempool은 최소 보장 개수를 사전 할당하므로
* 메모리 부족 시에도 deadlock 없이 할당 가능 */
req = mempool_alloc(&cc->req_pool, GFP_NOIO);
/* skcipher_request에 적절한 tfm 핸들 설정 */
skcipher_request_set_tfm(req, cc->tfms[key_index]);
/* dm_crypt_request는 skcipher_request 뒤에 위치
* (메모리 레이아웃: [skcipher_request][dm_crypt_request][iv]) */
dmreq = (struct dm_crypt_request *)
skcipher_request_ctx(req);
return req;
}
/* 요청 해제: mempool에 반환 */
static void crypt_free_req(struct crypt_config *cc,
struct skcipher_request *req)
{
mempool_free(req, &cc->req_pool);
}
/* mempool 초기화 (crypt_ctr에서 호출) */
/* 최소 MIN_IOS(64)개의 요청 객체를 사전 할당 */
r = mempool_init_kmalloc_pool(&cc->req_pool,
MIN_IOS, cc->dmreq_start +
sizeof(struct dm_crypt_request) +
cc->iv_size);
GFP_NOIO로만 메모리를 할당해야 합니다.
일반 kmalloc(GFP_KERNEL)을 사용하면 I/O 재진입(Reentrancy)으로 인한
데드락(Deadlock)이 발생할 수 있기 때문입니다.
mempool은 최소 개수를 보장하므로 메모리 압박 상황에서도 안전합니다.
bio 분할 및 병합 로직
dm-crypt는 큰 bio를 sector_size 단위로 처리하지만,
bounce 페이지 풀의 크기 제한으로 인해 bio를 분할해야 할 경우가 있습니다.
또한 하위 디바이스의 max_sectors 제한도 bio 분할의 원인이 됩니다.
/* drivers/md/dm-crypt.c - bio 분할/병합 관련 */
/* bounce 버퍼 할당 - 큰 bio는 분할 처리 */
static struct bio *crypt_alloc_buffer(
struct dm_crypt_io *io,
unsigned int size)
{
struct crypt_config *cc = io->cc;
struct bio *clone;
unsigned int nr_iovecs = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;
gfp_t gfp_mask = GFP_NOIO | __GFP_HIGHMEM;
unsigned int remaining = size;
struct page *page;
clone = bio_alloc_bioset(cc->dev->bdev, nr_iovecs,
io->base_bio->bi_opf,
GFP_NOIO, &cc->bs);
while (remaining) {
/* page_pool에서 bounce 페이지 할당
* mempool이므로 메모리 부족 시에도 보장 */
page = mempool_alloc(&cc->page_pool, gfp_mask);
unsigned int len = min_t(unsigned int,
remaining, PAGE_SIZE);
__bio_add_page(clone, page, len, 0);
remaining -= len;
}
return clone;
}
/* dm-crypt의 max_io_len 설정 (crypt_ctr에서) */
/* 하위 디바이스의 제한을 존중하면서 최대 I/O 크기 결정 */
ti->max_io_len = min_t(sector_t,
ti->len,
BIO_MAX_VECS * (PAGE_SIZE >> SECTOR_SHIFT));
/* bio 완료 후 bounce 페이지 해제 */
static void crypt_free_buffer_pages(
struct crypt_config *cc,
struct bio *clone)
{
struct bio_vec *bv;
struct bvec_iter_all iter_all;
bio_for_each_segment_all(bv, clone, iter_all) {
/* 페이지를 mempool에 반환 */
mempool_free(bv->bv_page, &cc->page_pool);
}
}
인라인 암호화 (blk-crypto) 연동 코드
커널 5.9 이후 dm-crypt는 blk-crypto 프레임워크와 연동하여
인라인 암호화 엔진(ICE)을 활용할 수 있습니다.
crypt_select_inline()이 하위 디바이스의 하드웨어 지원 여부를 확인하고,
지원되면 소프트웨어 암호화를 건너뛰고 bio에 crypto context를 설정합니다.
/* drivers/md/dm-crypt.c - blk-crypto 인라인 암호화 연동 */
/* DM 테이블 생성 시 인라인 암호화 가능 여부 판단 */
static bool crypt_is_hw_inline_supported(
struct crypt_config *cc)
{
struct request_queue *q = bdev_get_queue(cc->dev->bdev);
/* 하위 디바이스의 blk_crypto_profile 확인 */
if (!q->crypto_profile)
return false;
/* 요청한 알고리즘/키 크기를 HW가 지원하는지 검증 */
return blk_crypto_config_supported_natively(
q, &cc->crypto_cfg);
}
/* 인라인 모드에서의 crypt_map: 소프트웨어 암호화 건너뛰기 */
static int crypt_map_inline(struct dm_target *ti,
struct bio *bio)
{
struct crypt_config *cc = ti->private;
/* bio에 crypto context 설정
* 하드웨어가 실제 암호화를 수행 */
bio_crypt_set_ctx(bio,
&cc->blk_key,
(u64)(bio->bi_iter.bi_sector - cc->start),
GFP_NOIO);
/* 섹터 재매핑만 수행 (암호화는 HW가 처리) */
bio_set_dev(bio, cc->dev->bdev);
bio->bi_iter.bi_sector = cc->start +
dm_target_offset(ti, bio->bi_iter.bi_sector);
return DM_MAPIO_REMAPPED;
}
/* blk_crypto_key 초기화 (crypt_ctr에서) */
struct blk_crypto_config crypto_cfg = {
.crypto_mode = BLK_ENCRYPTION_MODE_AES_256_XTS,
.data_unit_size = cc->sector_size,
.dun_bytes = sizeof(u64),
};
r = blk_crypto_init_key(&cc->blk_key,
cc->key, cc->key_size,
&crypto_cfg);
crypt_convert(),
워크큐 컨텍스트 스위칭을 모두 건너뜁니다. bio에 bio_crypt_ctx만 설정하면
스토리지 컨트롤러가 데이터를 읽고 쓸 때 자동으로 암/복호화를 수행합니다.
CPU 오버헤드가 제로이므로 NVMe SSD에서 라인 속도(Line Rate) 암호화가 가능합니다.
XTS 모드 블록 처리
XTS(XEX-based Tweaked-codebook mode with ciphertext Stealing)는 디스크 암호화의 표준 모드입니다. IEEE P1619 표준으로 정의되어 있으며, 각 데이터 유닛(Data Unit, 일반적으로 512바이트 또는 4096바이트 섹터)을 독립적으로 암호화합니다. XTS는 두 개의 AES 키를 사용하여 tweak 값을 생성하고, 이 tweak로 각 16바이트 블록에 고유한 변환을 적용합니다.
| XTS 특성 | 설명 |
|---|---|
| 키 구조 | 512비트 = Key1(256비트, 데이터 암호화) + Key2(256비트, Tweak 생성) |
| Tweak 생성 | T = AES_Key2(sector_number), 섹터별 고유값 |
| 블록별 변환 | T * a^j (GF(2^128) 갈루아 필드(Galois Field) 곱셈으로 블록 인덱스 반영) |
| 병렬화 | 블록 간 의존성 없음 — 각 블록을 독립적으로 병렬 암/복호화 가능 |
| ciphertext stealing | 마지막 불완전 블록 처리 (16바이트 미만 잔여분) |
| 보안 수준 | 128비트 키 = 2^64 블록까지 안전, 256비트 키 = 2^128 블록까지 안전 |
LUKS2 헤더 구조 상세
LUKS2 헤더는 바이너리 헤더(Binary Header), JSON 메타데이터 영역, 키슬롯 바이너리 영역으로
구성됩니다. JSON 메타데이터에는 keyslots, segments,
digests, tokens, config 객체가 포함되며,
이들 간의 관계를 통해 유연한 키 관리와 데이터 매핑이 가능합니다.
/* LUKS2 JSON 메타데이터 전체 구조 예시 */
/* cryptsetup luksDump --dump-json-metadata /dev/sdb1 */
{
"config": {
"json_size": "12288",
"keyslots_size": "16744448"
},
"keyslots": {
"0": {
"type": "luks2",
"key_size": 64,
"af": {
"type": "luks1",
"stripes": 4000,
"hash": "sha256"
},
"kdf": {
"type": "argon2id",
"time": 4,
"memory": 1048576,
"cpus": 4,
"salt": "base64-encoded-salt..."
},
"area": {
"type": "raw",
"offset": "32768",
"size": "258048",
"encryption": "aes-xts-plain64",
"key_size": 64
}
},
"1": {
"type": "luks2",
"key_size": 64,
"kdf": { "type": "argon2id", /* ... */ },
"area": {
"offset": "290816",
"size": "258048",
/* ... */
}
}
},
"tokens": {
"0": {
"type": "systemd-tpm2",
"keyslots": ["1"],
"tpm2-pcrs": [4, 7],
"tpm2-bank": "sha256",
"tpm2-blob": "base64-sealed-key..."
},
"1": {
"type": "systemd-fido2",
"keyslots": ["0"],
"fido2-credential": "base64-credential-id...",
"fido2-salt": "base64-salt...",
"fido2-rp": "io.systemd.cryptsetup"
}
},
"digests": {
"0": {
"type": "pbkdf2",
"keyslots": ["0", "1"],
"segments": ["0"],
"hash": "sha256",
"iterations": 117867,
"salt": "base64-digest-salt...",
"digest": "base64-digest-value..."
}
},
"segments": {
"0": {
"type": "crypt",
"offset": "16777216",
"size": "dynamic",
"iv_tweak": "0",
"encryption": "aes-xts-plain64",
"sector_size": 4096
}
}
}
tokens은 keyslots를 참조하여 어떤 키 슬롯을 잠금 해제할지 지정합니다.
digests는 keyslots와 segments 모두를 참조하여
특정 키 슬롯에서 복원된 Volume Master Key가 특정 데이터 세그먼트에 유효한지 검증합니다.
이 3자 관계 덕분에 다중 키, 다중 세그먼트, 다중 인증 방식을 유연하게 조합할 수 있습니다.
dm-crypt 워크큐 아키텍처
dm-crypt는 암호화/복호화 작업을 처리하기 위해 두 개의 전용 워크큐(Workqueue)를 사용합니다:
kcryptd_io(I/O 제출용)와 kcryptd(암호화/복호화 연산용).
이 분리 설계는 암호화 연산의 블로킹이 I/O 제출 경로를 차단하지 않도록 보장합니다.
/* drivers/md/dm-crypt.c - 워크큐 초기화 */
static int crypt_ctr(struct dm_target *ti,
unsigned int argc, char **argv)
{
/* ... 파싱 생략 ... */
/* kcryptd: 암호화/복호화 연산 전용 워크큐
* WQ_HIGHPRI: 높은 우선순위 (I/O 지연 최소화)
* WQ_CPU_INTENSIVE: CPU 집약적 작업 표시
* WQ_MEM_RECLAIM: 메모리 회수 경로에서도 진행 보장 */
cc->crypt_queue = alloc_workqueue("kcryptd",
WQ_HIGHPRI | WQ_CPU_INTENSIVE | WQ_MEM_RECLAIM,
num_online_cpus());
/* kcryptd_io: 암호문 bio 제출 전용 워크큐
* 암호화 완료 후 하위 디바이스에 bio를 제출
* 암호화 워커와 분리하여 I/O 파이프라인 유지 */
cc->io_queue = alloc_workqueue("kcryptd_io",
WQ_HIGHPRI | WQ_MEM_RECLAIM,
1); /* 단일 워커로 I/O 순서 보존 */
/* 성능 플래그 파싱 */
if (test_bit(DM_CRYPT_NO_READ_WORKQUEUE, &cc->flags))
ti->per_io_data_size += sizeof(struct dm_crypt_io);
if (test_bit(DM_CRYPT_NO_WRITE_WORKQUEUE, &cc->flags))
DMINFO("no_write_workqueue enabled: "
"write encryption in submit context");
/* submit_from_crypt_cpus: 암호화 완료 CPU에서 직접 bio 제출
* kcryptd_io 워크큐 바이패스로 NUMA 지역성 유지 */
if (test_bit(DM_CRYPT_WRITE_INLINE, &cc->flags))
DMINFO("submit_from_crypt_cpus enabled");
return 0;
}
/* 읽기 완료 콜백 - 워크큐 vs 직접 복호화 */
static void crypt_endio(struct bio *clone)
{
struct dm_crypt_io *io = clone->bi_private;
struct crypt_config *cc = io->cc;
if (bio_data_dir(clone) == READ) {
if (test_bit(DM_CRYPT_NO_READ_WORKQUEUE, &cc->flags)) {
/* endio 컨텍스트에서 직접 복호화
* softirq/workqueue에서 호출될 수 있으므로
* CPU 집약 작업이 허용되는 환경인지 확인 */
if (in_hardirq()) {
/* hardirq에서는 워크큐 사용 */
kcryptd_queue_crypt(io);
} else {
crypt_dec_pending(io);
}
} else {
/* 기본: kcryptd 워크큐에서 복호화 */
kcryptd_queue_crypt(io);
}
}
}
| 워크큐 | 커널 스레드명 | 역할 | 플래그 |
|---|---|---|---|
crypt_queue | kcryptd | AES-XTS 암호화/복호화 연산 수행 | WQ_HIGHPRI | WQ_CPU_INTENSIVE |
io_queue | kcryptd_io | 암호문 bio를 하위 디바이스에 제출 | WQ_HIGHPRI | WQ_MEM_RECLAIM |
| (바이패스) | (submit 스레드) | no_write_workqueue 시 직접 처리 | - |
| (바이패스) | (endio 콜백) | no_read_workqueue 시 직접 복호화 | - |
no_read_workqueue는
softirq 컨텍스트에서 암호화를 수행할 수 있으므로, 다른 softirq 처리에 영향을 줄 수 있습니다.
실제 워크로드에서 벤치마크 후 적용하세요.
dm-crypt + dm-integrity 결합
dm-crypt와 dm-integrity를 결합하면 인증 암호화(AEAD)를 구현할 수 있습니다. AES-GCM 또는 ChaCha20-Poly1305 같은 AEAD 알고리즘을 사용하여 암호화와 무결성 검증을 단일 연산으로 수행합니다. 이 결합 모드에서 dm-crypt는 암호화 담당, dm-integrity는 인증 태그 저장 담당입니다.
# AEAD 모드 LUKS2 볼륨 생성 (AES-GCM)
$ cryptsetup luksFormat --type luks2 \
--cipher aes-gcm-random \
--integrity aead \
--sector-size 4096 \
--key-size 256 \
/dev/sdb1
# HMAC 무결성 (비 AEAD, 별도 해시)
$ cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--integrity hmac-sha256 \
--sector-size 4096 \
--key-size 512 \
/dev/sdb1
# dm-integrity 디바이스가 자동 생성됨
$ dmsetup ls
encrypted (254:1)
encrypted_dif (254:0) # dm-integrity 레이어
# 상태 확인
$ cryptsetup status encrypted
type: LUKS2
cipher: aes-gcm-random
keysize: 256 bits
integrity: aead
integrity keysize: 0 bits # AEAD는 별도 키 불필요
| 모드 | 암호화 | 무결성 | 태그 크기 | 성능 오버헤드 |
|---|---|---|---|---|
| AES-GCM (AEAD) | AES-GCM | GCM 내장 | 16B | ~15% |
| AES-XTS + HMAC | AES-XTS | HMAC-SHA256 | 32B | ~25% |
| ChaCha20-Poly1305 | ChaCha20 | Poly1305 | 16B | ~12% |
| AES-XTS 단독 | AES-XTS | 없음 | 0B | ~8% |
no-journal 모드로 동작합니다.
정전 시 데이터+태그 불일치로 해당 섹터가 읽기 불가능해질 수 있습니다.
UPS 또는 배터리 백업이 없는 환경에서는 저널 모드가 권장됩니다.
Device Mapper 타겟 개발
커스텀 Device Mapper target을 개발하려면 struct target_type을 구현하고
dm_register_target()으로 등록합니다. 최소한 ctr(생성자),
dtr(소멸자), map(I/O 매핑) 콜백이 필요합니다.
/* 예제: 최소 dm target 구현 */
#include <linux/device-mapper.h>
struct my_target_data {
struct dm_dev *dev;
sector_t start;
atomic64_t io_count;
};
/* 생성자: dmsetup create 시 호출 */
static int my_ctr(struct dm_target *ti,
unsigned int argc, char **argv)
{
struct my_target_data *mtd;
int r;
if (argc != 2) {
ti->error = "requires 2 arguments: dev_path offset";
return -EINVAL;
}
mtd = kzalloc(sizeof(*mtd), GFP_KERNEL);
if (!mtd)
return -ENOMEM;
r = dm_get_device(ti, argv[0], dm_table_get_mode(ti->table),
&mtd->dev);
if (r) {
kfree(mtd);
return r;
}
if (kstrtoull(argv[1], 10, &mtd->start)) {
dm_put_device(ti, mtd->dev);
kfree(mtd);
return -EINVAL;
}
atomic64_set(&mtd->io_count, 0);
ti->private = mtd;
return 0;
}
/* 소멸자 */
static void my_dtr(struct dm_target *ti)
{
struct my_target_data *mtd = ti->private;
dm_put_device(ti, mtd->dev);
kfree(mtd);
}
/* I/O 매핑: 모든 bio에 대해 호출 */
static int my_map(struct dm_target *ti, struct bio *bio)
{
struct my_target_data *mtd = ti->private;
atomic64_inc(&mtd->io_count);
bio_set_dev(bio, mtd->dev->bdev);
bio->bi_iter.bi_sector = mtd->start +
dm_target_offset(ti, bio->bi_iter.bi_sector);
return DM_MAPIO_REMAPPED;
}
/* 상태 보고 */
static void my_status(struct dm_target *ti,
status_type_t type,
unsigned int status_flags,
char *result, unsigned int maxlen)
{
struct my_target_data *mtd = ti->private;
switch (type) {
case STATUSTYPE_INFO:
snprintf(result, maxlen, "io_count=%lld",
atomic64_read(&mtd->io_count));
break;
case STATUSTYPE_TABLE:
snprintf(result, maxlen, "%s %llu",
mtd->dev->name, mtd->start);
break;
}
}
static struct target_type my_target = {
.name = "my_target",
.version = {1, 0, 0},
.module = THIS_MODULE,
.ctr = my_ctr,
.dtr = my_dtr,
.map = my_map,
.status = my_status,
};
static int __init my_init(void)
{
return dm_register_target(&my_target);
}
static void __exit my_exit(void)
{
dm_unregister_target(&my_target);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
| 콜백 | 호출 시점 | 반환값 | 필수 여부 |
|---|---|---|---|
ctr | 테이블 로드 시 | 0 성공, 음수 오류 | 필수 |
dtr | 디바이스 제거 시 | 없음 | 필수 |
map | 매 bio마다 | DM_MAPIO_* | 필수 |
end_io | bio 완료 시 | DM_ENDIO_* | 선택 |
status | dmsetup status/table | 없음 | 선택 |
message | dmsetup message | 0 또는 오류 | 선택 |
iterate_devices | 하위 디바이스 탐색 | 0 또는 오류 | 선택 |
io_hints | I/O 힌트 설정 | 없음 | 선택 |
DM_MAPIO_SUBMITTED: target이 bio를 완전히 처리 (비동기 완료 예정),
DM_MAPIO_REMAPPED: bio가 하위 디바이스로 redirect됨 (DM 코어가 submit),
DM_MAPIO_REQUEUE: bio를 재큐 (리소스 부족 시),
DM_MAPIO_KILL: bio를 에러로 완료
/* Makefile for out-of-tree DM target module */
obj-m := dm-mytarget.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
# 빌드 및 로드
$ make
$ sudo insmod dm-mytarget.ko
$ sudo dmsetup create mydev --table "0 2097152 my_target /dev/sdb1 0"
# 상태 확인
$ sudo dmsetup status mydev
0 2097152 my_target io_count=42
# 제거
$ sudo dmsetup remove mydev
$ sudo rmmod dm-mytarget
고급 target 개발 시 고려해야 할 사항들입니다:
| 고려사항 | 설명 | 관련 콜백/플래그 |
|---|---|---|
| bio 분할 | target 경계에서 bio가 분할될 수 있음 | max_io_len() |
| flush 지원 | REQ_PREFLUSH/REQ_FUA 처리 | num_flush_bios |
| discard 지원 | TRIM/discard 명령 전달 | num_discard_bios |
| suspend/resume | 테이블 교체 시 I/O 일시 중지 | presuspend(), resume() |
| 에러 처리 | 하위 디바이스 에러 전파 | end_io() |
| 메모리 할당 | map() 컨텍스트에서 GFP_NOIO 사용 | mempool 사전 할당 |
| 통계/모니터링 | I/O 카운터, 지연 시간 측정 | status(), message() |
성능 최적화
Device Mapper target들의 성능을 최적화하려면 I/O 스케줄링, prefetch, 병렬 처리, 메모리 할당 전략을 종합적으로 고려해야 합니다. 특히 dm-crypt는 암호화 연산이 병목(Bottleneck)이 되므로, 하드웨어 가속과 병렬화가 핵심입니다.
# dm-crypt I/O 스케줄러 최적화
$ echo none > /sys/block/dm-0/queue/scheduler
$ echo 256 > /sys/block/dm-0/queue/read_ahead_kb
# 암호화 성능 벤치마크
$ cryptsetup benchmark
# Tests are approximate using memory only (no storage IO).
# Algorithm | Key | Encryption | Decryption
aes-xts 256b 4512.5 MiB/s 4487.3 MiB/s
aes-xts 512b 3891.2 MiB/s 3867.8 MiB/s
serpent-xts 256b 618.3 MiB/s 612.7 MiB/s
twofish-xts 256b 504.1 MiB/s 510.3 MiB/s
aes-cbc 256b 1356.8 MiB/s 4231.4 MiB/s
no_read_workqueue + no_write_workqueue + submit_from_crypt_cpus를
모두 활성화하면 NVMe SSD에서 암호화 오버헤드를 10% 미만으로 줄일 수 있습니다.
same_cpu_crypt는 NUMA 시스템에서 캐시 지역성을 개선합니다.
보안 고려사항
dm-crypt의 보안은 키 관리, 알고리즘 선택, 부팅 체인 무결성에 의존합니다. 커널의 keyring 서브시스템, TPM, FIPS 모드를 활용하여 보안을 강화할 수 있습니다.
# TPM 2.0 Sealed Key로 LUKS 잠금 해제
$ systemd-cryptenroll --tpm2-device=auto /dev/sdb1
$ cryptsetup open --token-only /dev/sdb1 encrypted
# FIDO2 하드웨어 토큰 등록
$ systemd-cryptenroll --fido2-device=auto /dev/sdb1
# 커널 keyring 연동
$ keyctl add logon dmcrypt:my_key 32 @s < /path/to/key
$ echo "0 $(blockdev --getsz /dev/sdb1) crypt aes-xts-plain64 \
:64:logon:dmcrypt:my_key 0 /dev/sdb1 0" | dmsetup create enc
# FIPS 모드 확인
$ cat /proc/sys/crypto/fips_enabled
1
# FIPS 모드에서는 승인된 알고리즘만 사용 가능
# AES-XTS, AES-CBC, SHA-256, HMAC-SHA256 등
| 보안 기능 | 설명 | 설정 방법 |
|---|---|---|
| TPM Sealed Key | PCR 값에 바인딩된 암호키 | systemd-cryptenroll --tpm2-device |
| FIDO2 Token | 하드웨어 인증 토큰 | systemd-cryptenroll --fido2-device |
| Argon2id KDF | 메모리-하드 키 유도 | LUKS2 기본, --pbkdf argon2id |
| Key Slot 격리(Isolation) | 다중 인증 방식 지원 | LUKS2 최대 32개 슬롯 |
| Suspend 보호 | RAM에서 키 삭제 | cryptsetup luksSuspend |
| FIPS 모드 | 승인 알고리즘만 허용 | fips=1 부팅 파라미터 |
cryptsetup luksSuspend로 키를 메모리에서 삭제하거나,
CONFIG_INIT_ON_FREE_DEFAULT_ON으로 해제 메모리를 0으로 초기화하세요.
# TPM2 PCR 정책으로 LUKS 키 봉인
# PCR7 = Secure Boot 상태, PCR4 = 부트 관리자
$ systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=4+7 /dev/sdb1
# TPM 봉인 키 상태 확인
$ systemd-cryptenroll /dev/sdb1
SLOT TYPE
0 password
1 tpm2 (pcrs: 4+7, device: /dev/tpmrm0)
# FIDO2 YubiKey로 잠금 해제
$ systemd-cryptenroll --fido2-device=auto --fido2-with-client-pin=yes --fido2-with-user-presence=yes /dev/sdb1
# 복구 키 생성 (비상 잠금 해제용)
$ systemd-cryptenroll --recovery-key /dev/sdb1
# 출력: mmmm-mmmm-mmmm-mmmm-mmmm-mmmm-mmmm-mmmm (base58 복구 키)
luksSuspend와 luksResume을 활용한 키 보호 전략도 중요합니다.
시스템이 절전 모드(suspend to RAM)에 진입할 때 luksSuspend로
dm-crypt의 마스터 키를 커널 메모리에서 삭제하면, Cold Boot 공격으로부터
키를 보호할 수 있습니다. 복귀 시 luksResume으로 비밀번호를
재입력받아 키를 복원합니다.
# 절전 전 키 삭제
$ cryptsetup luksSuspend encrypted
# 복귀 후 키 복원
$ cryptsetup luksResume encrypted
# systemd suspend hook 설정
# /etc/systemd/system/cryptsetup-suspend@.service
[Unit]
Description=Suspend dm-crypt device
Before=sleep.target
[Service]
Type=oneshot
ExecStart=/usr/bin/cryptsetup luksSuspend %i
ExecStartPost=/usr/bin/systemctl suspend
[Install]
WantedBy=sleep.target
CONFIG_INIT_ON_FREE_DEFAULT_ON=y로 해제 메모리를 자동 초기화하고,
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y로 할당 메모리도 초기화하면
메모리 잔류 데이터를 통한 키 유출 위험을 줄일 수 있습니다.
| 공격 벡터 | 위협 | 대응 방안 |
|---|---|---|
| Cold Boot | RAM에서 키 추출 | luksSuspend, INIT_ON_FREE |
| Evil Maid | 부트로더/initramfs 변조 | Secure Boot + TPM PCR |
| DMA 공격 | Thunderbolt/PCIe DMA로 메모리 접근 | IOMMU, Thunderbolt 보안 레벨 |
| 브루트포스 | 비밀번호 반복 시도 | Argon2id (메모리-하드 KDF) |
| 키 로깅 | 비밀번호 입력 가로채기 | FIDO2/TPM (비밀번호 불필요) |
| Wear Leveling | SSD에서 삭제된 키 복구 | SED (Self-Encrypting Drive) |
| Side Channel | 타이밍/전력 분석으로 키 추론 | constant-time crypto, AES-NI |
실전 활용
dm-crypt는 서버 디스크 암호화, 임베디드 기기 데이터 보호, 컨테이너 빌드 아티팩트 보호, CI/CD 파이프라인(Pipeline) 등 다양한 실전 시나리오에서 활용됩니다.
## 시나리오 1: 서버 전체 디스크 암호화 (Clevis + Tang)
# 1. LUKS2 포맷
$ cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 --key-size 512 \
--pbkdf argon2id /dev/nvme0n1p3
# 2. Tang 서버 바인딩 (네트워크 기반 자동 잠금 해제)
$ clevis luks bind -d /dev/nvme0n1p3 tang \
'{"url":"http://tang.internal:7500"}'
# 3. 부팅 시 자동 잠금 해제 (initramfs)
$ dracut -f --regenerate-all # clevis dracut 모듈 포함
## 시나리오 2: 데이터베이스 서버 무결성 보호
# HMAC-SHA256 무결성 + AES-XTS 암호화
$ cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--integrity hmac-sha256 \
--key-size 512 \
--sector-size 4096 \
/dev/nvme0n1p3
# 성능 최적화 옵션으로 열기
$ cryptsetup open /dev/nvme0n1p3 db_encrypted \
--perf-no_read_workqueue \
--perf-no_write_workqueue \
--perf-submit_from_crypt_cpus
# XFS 파일시스템으로 데이터베이스 볼륨 구성
$ mkfs.xfs -f -s size=4096 /dev/mapper/db_encrypted
$ mount -o noatime,nodiratime /dev/mapper/db_encrypted /var/lib/postgresql
| 시나리오 | dm target | 키 관리 | 부팅 자동화 |
|---|---|---|---|
| 서버 FDE | dm-crypt (LUKS2) | TPM / Tang / Passphrase | Clevis + dracut |
| Android 기기 | dm-crypt / ICE | eFuse + KEK | init |
| IoT 디바이스 | dm-crypt (Adiantum) | eFuse / 키 서버 | initramfs |
| K8s PVC | dm-crypt (CSI 드라이버) | 시크릿 / KMS | CSI NodeStageVolume |
systemd-cryptenroll을 통해
TPM2, FIDO2, PKCS#11 토큰, 복구 키를 LUKS2 키 슬롯에 간편하게 등록할 수 있습니다.
기존의 crypttab + keyfile 방식보다 보안과 편의성 모두 향상됩니다.
LUKS2 키 관리 운영
# LUKS2 헤더 정보 확인
$ cryptsetup luksDump /dev/sdb1
LUKS header information
Version: 2
Epoch: 5
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Label: encrypted-data
Subsystem: (no subsystem)
Flags: (no flags)
Keyslots:
0: luks2
Key: 512 bits
Priority: normal
Cipher: aes-xts-plain64
Cipher key: 512 bits
PBKDF: argon2id
Time cost: 4
Memory: 1048576
Threads: 4
1: luks2 (tpm2)
Key: 512 bits
Priority: prefer
Cipher: aes-xts-plain64
Tokens:
0: systemd-tpm2
tpm2-pcrs: 4+7
tpm2-bank: sha256
Data segments:
0: crypt
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-xts-plain64
sector: 4096 [bytes]
# 키 슬롯 추가 (새 비밀번호)
$ cryptsetup luksAddKey /dev/sdb1
# 키 슬롯 제거
$ cryptsetup luksKillSlot /dev/sdb1 2
# 온라인 마스터 키 변경 (LUKS2 전용)
$ cryptsetup reencrypt --init-only /dev/sdb1
$ cryptsetup reencrypt --resume-only /dev/sdb1
# 알고리즘 변경 (재암호화)
$ cryptsetup reencrypt /dev/sdb1 --cipher aes-xts-plain64 --key-size 512
# LUKS 볼륨 크기 확장
$ cryptsetup resize encrypted
$ resize2fs /dev/mapper/encrypted
cryptsetup luksHeaderBackup으로 헤더를 별도 저장소에 백업하세요.
LUKS2 헤더는 이중화(primary + secondary)되어 있어 단일 손상에는
자동 복구되지만, 물리적 손상이나 실수로 인한 덮어쓰기에는 무력합니다.
## 시나리오 3: Kubernetes 시크릿 볼륨 암호화
# CSI 드라이버 + dm-crypt으로 PVC 암호화
# 1. StorageClass에 암호화 파라미터 정의
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: encrypted-ssd
provisioner: csi.example.com
parameters:
encryption: "true"
cipher: "aes-xts-plain64"
keySize: "512"
pbkdf: "argon2id"
# 2. 노드에서 PVC가 마운트될 때 자동 LUKS 포맷/열기
# CSI NodeStageVolume에서:
# - cryptsetup luksFormat /dev/xvdf
# - cryptsetup open /dev/xvdf pvc-xxx
# - mkfs.ext4 /dev/mapper/pvc-xxx
# - mount /dev/mapper/pvc-xxx /var/lib/kubelet/pods/...
dm-crypt 디버깅(Debugging) 및 모니터링
# dm-crypt I/O 통계 확인
$ dmsetup status encrypted
0 2097152 crypt aes-xts-plain64 in_flight:12 ...
# 블록 레이어 I/O 통계
$ cat /sys/block/dm-0/stat
28547 0 456752 1230 15823 0 253168 890 0 1420 2120
# dm-crypt 워크큐 모니터링
$ cat /proc/sys/kernel/threads-max
$ ps aux | grep kcryptd
root 123 0.5 0.0 [kcryptd]
root 124 0.5 0.0 [kcryptd_io]
# crypto API 알고리즘 확인
$ cat /proc/crypto | grep -A5 "name.*xts(aes)"
name : xts(aes)
driver : xts-aes-aesni
module : aesni_intel
priority : 401
refcnt : 3
type : skcipher
# AES-NI 활성화 확인
$ grep aes /proc/cpuinfo | head -1
flags : ... aes ...
# perf를 이용한 암호화 성능 프로파일링
$ perf top -p $(pgrep kcryptd) -g
# aesni_xts_encrypt/decrypt 함수가 상위에 표시되면 정상
# blktrace로 dm-crypt I/O 지연 분석
$ blktrace -d /dev/dm-0 -o trace &
$ fio --name=test --filename=/dev/mapper/encrypted --rw=randread --bs=4k --iodepth=32 --numjobs=1 --runtime=10
$ kill %1
$ blkparse -i trace -o analysis.txt
$ btt -i trace.blktrace.0 -o btt_result
| 문제 상황 | 증상 | 해결 방법 |
|---|---|---|
| AES-NI 미활성 | CPU 사용률 높음, 처리량 낮음 | modprobe aesni_intel, BIOS 확인 |
| 워크큐 병목 | kcryptd CPU 100% | no_read/write_workqueue 활성화 |
| NUMA 비효율 | 원격 메모리 접근 증가 | submit_from_crypt_cpus 활성화 |
| AEAD 저널 병목 | 쓰기 대기 시간(Latency) 증가 | bitmap 모드 전환, 저널 크기 증가 |
| 키 분실 | 볼륨 접근 불가 | 복구 키, LUKS 헤더 백업 |
# LUKS 헤더 백업 (필수!)
$ cryptsetup luksHeaderBackup /dev/sdb1 --header-backup-file /backup/sdb1-luks-header.img
# LUKS 헤더 복원
$ cryptsetup luksHeaderRestore /dev/sdb1 --header-backup-file /backup/sdb1-luks-header.img
# ftrace로 dm 함수 추적
$ echo 1 > /sys/kernel/debug/tracing/events/block/block_bio_remap/enable
$ echo "dm*" > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
$ cat /sys/kernel/debug/tracing/trace_pipe | head -50
커널 설정 (Kconfig) 상세
| 설정 | 설명 | 권장값 | 의존성 |
|---|---|---|---|
CONFIG_BLK_DEV_DM | Device Mapper 코어 | y | BLK_DEV |
CONFIG_DM_CRYPT | dm-crypt 암호화 | y | BLK_DEV_DM, CRYPTO |
CONFIG_DM_INTEGRITY | dm-integrity 무결성 (AEAD 결합) | y | BLK_DEV_DM, BLK_DEV_INTEGRITY |
CONFIG_CRYPTO_AES_NI_INTEL | AES-NI 가속 | y (x86) | CRYPTO_AES, X86 |
CONFIG_CRYPTO_XTS | XTS 모드 | y | CRYPTO |
CONFIG_CRYPTO_ADIANTUM | Adiantum 알고리즘 | y (ARM) | CRYPTO |
CONFIG_CRYPTO_USER_API_AEAD | 사용자 AEAD API | y | CRYPTO_AEAD |
DM Target 개발 완전 예제
Device Mapper target을 개발하려면 struct target_type의 콜백 함수를 구현하고
커널 모듈로 등록합니다. 이 섹션에서는 dm-linear를 클론한 완전한 모듈 예제를 통해
ctr(생성자), dtr(소멸자), map(I/O 매핑), status(상태 보고), message(런타임 명령) 콜백의
실제 구현 패턴을 상세히 살펴봅니다.
| 콜백 함수 | 호출 시점 | 역할 |
|---|---|---|
ctr | dmsetup create / DM_TABLE_LOAD | 인자 파싱, 하위 디바이스 획득, private 데이터 초기화 |
dtr | 테이블 교체 / dmsetup remove | 하위 디바이스 해제, 메모리 정리 |
map | bio 도착 시 (I/O 경로) | bio를 하위 디바이스로 remapping (bi_bdev, bi_sector 재설정) |
status | dmsetup status / dmsetup table | 런타임 정보 또는 테이블 매개변수 출력 |
message | dmsetup message | 런타임 파라미터 변경 (통계 리셋 등) |
end_io | 하위 디바이스 I/O 완료 시 | 완료 후처리, 오류 핸들링, 통계 갱신 |
iterate_devices | DM 코어 내부 | 하위 디바이스 순회 (디스카드, 토폴로지(Topology) 전파) |
DM_MAPIO_REMAPPED는 DM 코어가 remapped bio를 제출하도록 지시합니다.
DM_MAPIO_SUBMITTED는 target이 직접 bio를 제출했음을 의미하고,
DM_MAPIO_KILL은 I/O 오류로 bio를 종료합니다.
DM_MAPIO_REQUEUE는 나중에 재시도하도록 bio를 큐에 되돌립니다.
아래는 dm-linear를 클론한 완전한 커널 모듈 예제입니다. I/O 카운터와 message 콜백을 추가하여 런타임 통계 리셋 기능도 포함합니다.
/* dm-mylinear.c — dm-linear 클론 + I/O 카운터 + message 콜백 */
#include <linux/module.h>
#include <linux/device-mapper.h>
#include <linux/bio.h>
#define DM_MSG_PREFIX "mylinear"
struct mylinear_c {
struct dm_dev *dev;
sector_t start;
atomic64_t read_ios;
atomic64_t write_ios;
};
/* 생성자: dmsetup create 시 호출 */
static int mylinear_ctr(struct dm_target *ti,
unsigned int argc, char **argv)
{
struct mylinear_c *lc;
unsigned long long tmp;
int r;
if (argc != 2) {
ti->error = "requires exactly 2 arguments: <dev_path> <offset>";
return -EINVAL;
}
lc = kzalloc(sizeof(*lc), GFP_KERNEL);
if (!lc) {
ti->error = "cannot allocate private data";
return -ENOMEM;
}
/* 하위 디바이스 획득 */
r = dm_get_device(ti, argv[0],
dm_table_get_mode(ti->table), &lc->dev);
if (r) {
ti->error = "device lookup failed";
kfree(lc);
return r;
}
/* 오프셋 파싱 */
if (sscanf(argv[1], "%llu", &tmp) != 1 ||
tmp != (sector_t)tmp) {
ti->error = "invalid offset";
dm_put_device(ti, lc->dev);
kfree(lc);
return -EINVAL;
}
lc->start = tmp;
atomic64_set(&lc->read_ios, 0);
atomic64_set(&lc->write_ios, 0);
ti->private = lc;
ti->num_flush_bios = 1;
ti->num_discard_bios = 1;
ti->num_secure_erase_bios = 1;
ti->num_write_zeroes_bios = 1;
return 0;
}
/* 소멸자 */
static void mylinear_dtr(struct dm_target *ti)
{
struct mylinear_c *lc = ti->private;
dm_put_device(ti, lc->dev);
kfree(lc);
}
/* I/O 매핑: 모든 bio에 대해 호출 */
static int mylinear_map(struct dm_target *ti,
struct bio *bio)
{
struct mylinear_c *lc = ti->private;
/* I/O 통계 수집 */
if (bio_data_dir(bio) == READ)
atomic64_inc(&lc->read_ios);
else
atomic64_inc(&lc->write_ios);
/* bio를 하위 디바이스로 remapping */
bio_set_dev(bio, lc->dev->bdev);
if (bio_sectors(bio) || op_is_zone_mgmt(bio->bi_opf))
bio->bi_iter.bi_sector = lc->start +
dm_target_offset(ti, bio->bi_iter.bi_sector);
return DM_MAPIO_REMAPPED;
}
/* 상태 보고 */
static void mylinear_status(struct dm_target *ti,
status_type_t type, unsigned int status_flags,
char *result, unsigned int maxlen)
{
struct mylinear_c *lc = ti->private;
switch (type) {
case STATUSTYPE_INFO:
snprintf(result, maxlen,
"reads=%lld writes=%lld",
atomic64_read(&lc->read_ios),
atomic64_read(&lc->write_ios));
break;
case STATUSTYPE_TABLE:
snprintf(result, maxlen, "%s %llu",
lc->dev->name, (unsigned long long)lc->start);
break;
case STATUSTYPE_IMA:
*result = '\0';
break;
}
}
/* message 콜백: 런타임 명령 */
static int mylinear_message(struct dm_target *ti,
unsigned int argc, char **argv,
char *result, unsigned int maxlen)
{
struct mylinear_c *lc = ti->private;
if (argc == 1 && !strcasecmp(argv[0], "reset_stats")) {
atomic64_set(&lc->read_ios, 0);
atomic64_set(&lc->write_ios, 0);
DMINFO("statistics reset");
return 0;
}
DMERR("unrecognised message: %s", argv[0]);
return -EINVAL;
}
/* 하위 디바이스 순회 */
static int mylinear_iterate_devices(struct dm_target *ti,
iterate_devices_callout_fn fn, void *data)
{
struct mylinear_c *lc = ti->private;
return fn(ti, lc->dev, lc->start, ti->len, data);
}
static struct target_type mylinear_target = {
.name = "mylinear",
.version = {1, 0, 0},
.features = DM_TARGET_PASSES_INTEGRITY,
.module = THIS_MODULE,
.ctr = mylinear_ctr,
.dtr = mylinear_dtr,
.map = mylinear_map,
.status = mylinear_status,
.message = mylinear_message,
.iterate_devices = mylinear_iterate_devices,
};
static int __init dm_mylinear_init(void)
{
return dm_register_target(&mylinear_target);
}
static void __exit dm_mylinear_exit(void)
{
dm_unregister_target(&mylinear_target);
}
module_init(dm_mylinear_init);
module_exit(dm_mylinear_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("DM mylinear — linear clone with I/O stats");
# Makefile — out-of-tree DM target 빌드
obj-m += dm-mylinear.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
linux-headers-$(uname -r))가 설치되어 있어야 합니다.
make로 빌드한 뒤 insmod dm-mylinear.ko로 로드하면
dmsetup targets에 mylinear가 나타납니다.
# 모듈 로드 및 DM 디바이스 생성
sudo insmod dm-mylinear.ko
dmsetup targets | grep mylinear
# mylinear v1.0.0
# 루프 디바이스 생성 (테스트용 1GB)
dd if=/dev/zero of=/tmp/test.img bs=1M count=1024
sudo losetup /dev/loop0 /tmp/test.img
# DM 디바이스 생성: 0 <sectors> mylinear <dev> <offset>
# 1GB = 2097152 sectors (512B 단위)
echo "0 2097152 mylinear /dev/loop0 0" | sudo dmsetup create mytest
# 동작 확인
ls -la /dev/mapper/mytest
sudo mkfs.ext4 /dev/mapper/mytest
sudo mount /dev/mapper/mytest /mnt/test
echo "hello dm" | sudo tee /mnt/test/hello.txt
# I/O 통계 확인
sudo dmsetup status mytest
# 0 2097152 mylinear reads=42 writes=15
# 통계 리셋
sudo dmsetup message mytest 0 reset_stats
sudo dmsetup status mytest
# 0 2097152 mylinear reads=0 writes=0
# 정리
sudo umount /mnt/test
sudo dmsetup remove mytest
sudo losetup -d /dev/loop0
sudo rmmod dm-mylinear
# 검증: dmesg에서 모듈 로그 확인
dmesg | grep mylinear
# [ 123.456] device-mapper: mylinear: statistics reset
# /proc 확인
cat /proc/devices | grep device-mapper
# 253 device-mapper
# sysfs에서 DM 디바이스 확인
ls /sys/block/dm-0/dm/
# name suspended uuid
cat /sys/block/dm-0/dm/name
# mytest
drivers/md/Kconfig와 Makefile에 항목을 추가하여
커널 빌드 시스템(Build System)에 포함시키는 것이 바람직합니다.
blktrace/btrace 분석
blktrace는 블록 레이어의 I/O 이벤트를 실시간으로 추적하는 도구로,
DM 디바이스의 I/O 패턴을 분석하는 데 필수적입니다. dm-crypt 환경에서는
암호화/복호화에 의한 I/O 지연과 워크큐 컨텍스트 스위칭 오버헤드를 정량적으로 측정할 수 있습니다.
| Action 코드 | 의미 | 설명 |
|---|---|---|
Q | Queued | bio가 request queue에 진입 |
G | Get request | request 구조체(Struct) 할당 |
I | Inserted | I/O 스케줄러에 삽입 |
D | Issued | 드라이버에 디스패치(Dispatch) |
C | Completed | I/O 완료 |
M | Merged | 기존 request에 병합 |
A | Remap | DM/MD에 의한 remap (DM 분석의 핵심) |
X | Split | bio가 분할됨 |
# blktrace 기본 사용법: dm-crypt 디바이스 추적
# -d: 대상 디바이스, -o -: stdout 출력
sudo blktrace -d /dev/dm-0 -o - | blkparse -i - -o trace.out
# btrace: blktrace + blkparse 간편 래퍼
sudo btrace /dev/dm-0
# 특정 시간(10초) 동안 추적
sudo blktrace -d /dev/dm-0 -w 10 -o crypt_trace
blkparse -i crypt_trace -d crypt_trace.bin
# DM 하위 디바이스도 동시 추적 (remap 관계 확인)
sudo blktrace -d /dev/dm-0 -d /dev/sda -w 5 -o multi_trace
blkparse -i multi_trace -M -o multi_out.txt
# dm-crypt I/O 패턴 분석 예제
# A(remap) 이벤트만 필터링: DM 리매핑 추적
blkparse -i crypt_trace -a remap -o remap_only.txt
# 출력 예시 해석:
# 253,0 0 1 0.000000000 1234 A R 2048 + 8 <- (8,0) 4096
# │ │ │ │ │ │ │ │ │ │ └ 하위 디바이스 섹터
# │ │ │ │ │ │ │ │ │ └ 하위 디바이스 (major,minor)
# │ │ │ │ │ │ │ │ └ 길이 (섹터)
# │ │ │ │ │ │ │ └ DM 디바이스 섹터
# │ │ │ │ │ │ └ R=Read, W=Write
# │ │ │ │ │ └ A=remap
# │ │ │ │ └ PID
# │ │ │ └ 타임스탬프
# └────┘──┘ 디바이스 (major,minor) + CPU
# btt: blktrace timing 통계 분석
btt -i crypt_trace.bin -l crypt_latency.dat
# Q2C: 전체 레이턴시 (요청~완료)
# D2C: 디바이스 레이턴시 (디스패치~완료)
# Q2G: 큐잉 레이턴시
# iowatcher: blktrace 결과를 시각화 SVG로 변환
iowatcher -t crypt_trace -o crypt_io.svg
A(remap) 이벤트를 분석하면
암호화/복호화에 의한 I/O 지연을 파악할 수 있습니다. btt의 Q2C 통계로
dm-crypt 오버헤드를 정량적으로 측정하세요. no_read_workqueue 활성화 전후의
Q2C 차이를 비교하면 워크큐 바이패스의 효과를 확인할 수 있습니다.
FIPS 140-2/3 고려사항
FIPS(Federal Information Processing Standards) 140-2/3은 미국 정부가 요구하는 암호화 모듈 인증 표준입니다. 리눅스 커널이 FIPS 모드로 동작할 때 dm-crypt가 받는 제약과 설정 방법을 정리합니다.
| 구분 | FIPS 승인 알고리즘 | FIPS 비승인 (사용 불가) |
|---|---|---|
| 블록 암호 | AES-128/192/256 (CBC, XTS, GCM, CCM) | Blowfish, Twofish, Serpent, Camellia |
| 해시 | SHA-1, SHA-224/256/384/512, SHA-3 | MD5, RIPEMD, Whirlpool |
| MAC | HMAC-SHA-256, HMAC-SHA-512, CMAC-AES | HMAC-MD5 |
| KDF | PBKDF2-SHA-256, Argon2id (FIPS 140-3 SP 800-132) | scrypt (일부 모듈에서 비승인) |
| 난수 | DRBG (SP 800-90A: CTR_DRBG, HMAC_DRBG) | 커널 기본 /dev/urandom (비인증) |
# 커널 FIPS 모드 확인
cat /proc/sys/crypto/fips_enabled
# 1 = FIPS 모드 활성화
# FIPS 모드 부팅 활성화 (GRUB)
# /etc/default/grub에 추가:
# GRUB_CMDLINE_LINUX="fips=1"
sudo update-grub && sudo reboot
# FIPS self-test 결과 확인
dmesg | grep -i fips
# [ 0.123] fips: FIPS 140-3 self-tests passed
# [ 0.456] alg: self-tests for aes passed
# [ 0.789] alg: self-tests for sha256 passed
# FIPS 모드에서 dm-crypt 설정 (승인 알고리즘만 사용)
# AES-XTS-256: FIPS 승인 ✓
sudo cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 --key-size 512 \
--hash sha256 --pbkdf argon2id \
/dev/sda2
# FIPS 비승인 알고리즘 시도 → 실패
sudo cryptsetup luksFormat --cipher serpent-xts-plain64 /dev/sda2
# Error: cipher serpent-xts-plain64 not available in FIPS mode
LUKS1 vs LUKS2 비교
LUKS(Linux Unified Key Setup)는 dm-crypt 위에서 키 관리, 헤더 포맷, 다중 패스프레이즈를 표준화한 디스크 암호화 포맷입니다. LUKS2는 JSON 메타데이터, Argon2id KDF, 토큰/세그먼트 추상화, dm-integrity 연동 등 대폭 개선된 2세대 포맷입니다.
| 기능 | LUKS1 | LUKS2 |
|---|---|---|
| 헤더 포맷 | 고정 바이너리 (592 bytes phdr) | JSON 메타데이터 (이중화) |
| 최대 키 슬롯 | 8개 (고정 크기) | 사실상 무제한 (가변 크기) |
| KDF | PBKDF2 전용 | Argon2id (기본) / PBKDF2 |
| 무결성 연동 | 미지원 | dm-integrity (AEAD: AES-GCM/CHACHA20-POLY1305) |
| 온라인 리키잉 | 불가 | cryptsetup reencrypt (온라인) |
| 토큰 | 미지원 | FIDO2, TPM2, systemd-cryptenroll |
| 세그먼트 | 단일 (전체 디바이스) | 다중 세그먼트 (부분 암호화 가능) |
| 헤더 복구 | 단일 헤더 (손상 시 복구 어려움) | Primary + Secondary (자동 복구) |
| 호환성 | cryptsetup 1.0+ | cryptsetup 2.0+ |
| LUKS2 JSON 객체 | 설명 |
|---|---|
keyslots | 키 슬롯 정의 (KDF 파라미터, AF 스트라이프, 암호화된 키 위치) |
segments | 데이터 영역 매핑 (오프셋, 크기, 암호, IV 모드) |
tokens | 외부 잠금해제 메커니즘 (TPM2 sealed key, FIDO2, Clevis) |
digests | 키 검증용 다이제스트 (keyslot → volume key 매핑) |
config | JSON 영역 크기, 키슬롯 영역 크기 설정 |
# LUKS1 → LUKS2 마이그레이션
# 주의: 반드시 백업 후 진행!
sudo cryptsetup luksDump /dev/sda2 | head -5
# LUKS header information
# Version: 1
# LUKS1 → LUKS2 변환
sudo cryptsetup convert --type luks2 /dev/sda2
sudo cryptsetup luksDump /dev/sda2 | head -5
# LUKS header information
# Version: 2
# LUKS2에서 Argon2id로 KDF 업그레이드
sudo cryptsetup luksConvertKey --pbkdf argon2id \
--pbkdf-memory 1048576 --pbkdf-parallel 4 /dev/sda2
# LUKS2 JSON 메타데이터 확인
sudo cryptsetup luksDump --dump-json-metadata /dev/sda2 | python3 -m json.tool
# LUKS2 + dm-integrity: 인증 암호화 (AEAD)
# AES-256-GCM: 암호화 + 무결성 동시 보장
sudo cryptsetup luksFormat --type luks2 \
--cipher aes-gcm-random --key-size 256 \
--integrity aead \
--sector-size 4096 \
/dev/sda2
# 내부적으로 dm-crypt + dm-integrity 스택 생성
sudo cryptsetup open /dev/sda2 secure_vol
dmsetup table secure_vol
# 0 <size> crypt aes-gcm-random ... integrity:28:aead
# 온라인 리키잉 (LUKS2 전용)
# 사용 중인 볼륨의 마스터 키를 교체
sudo cryptsetup reencrypt --cipher aes-xts-plain64 \
--key-size 512 /dev/sda2
cryptsetup luksHeaderBackup으로
헤더를 백업하세요. GRUB2의 LUKS2 지원은 제한적이므로 (Argon2id 미지원 등)
부트 파티션 암호화에는 LUKS1 또는 PBKDF2 기반 LUKS2를 사용해야 할 수 있습니다.
하드웨어 가속
최신 스토리지 컨트롤러와 프로세서는 인라인 암호화 엔진을 내장하여
dm-crypt의 소프트웨어 암호화 오버헤드를 제거합니다. 커널 5.9+의
blk-crypto 프레임워크는 하드웨어/소프트웨어 폴백을 투명하게 통합합니다.
| 하드웨어 가속 | 벤더 | 방식 | 성능 특성 |
|---|---|---|---|
| Inline Crypto Engine (ICE) | Qualcomm | UFS/eMMC 컨트롤러 내장 AES 엔진 | CPU 사용률 0%, 라인 속도 암호화 |
| FBE HW | Samsung | eMMC/UFS 인라인 암호화 | CPU 사용률 0%, Knox 보안 연동 |
| AES-NI | Intel/AMD | CPU 명령어 셋 (소프트웨어 가속) | CPU 사용, 3-5 GB/s (코어당) |
| ARMv8 CE | ARM | Crypto Extensions 명령어 | CPU 사용, 1-3 GB/s (코어당) |
| CCP | AMD | Cryptographic Co-Processor | 오프로드, DMA 기반 |
| QAT | Intel | QuickAssist Technology | 가속 카드, 대량 암호화/압축 |
# blk-crypto 하드웨어 지원 확인
# UFS 컨트롤러의 crypto capabilities
cat /sys/block/sda/queue/crypto/modes/AES-256-XTS
# 512 (지원하는 데이터 유닛 크기)
# blk-crypto 프로파일 확인
ls /sys/block/sda/queue/crypto/
# max_dun_bits modes/ num_keyslots
cat /sys/block/sda/queue/crypto/num_keyslots
# 32 (하드웨어 키 슬롯 수)
# Intel AES-NI 지원 확인
grep -o aes /proc/cpuinfo | head -1
# aes
lsmod | grep aesni
# aesni_intel ...
/* blk-crypto 키 설정 예시 (커널 내부) */
struct blk_crypto_key key;
/* 키 초기화 */
blk_crypto_init_key(&key, raw_key, 64,
BLK_ENCRYPTION_MODE_AES_256_XTS, 0,
512); /* data_unit_size */
/* bio에 crypto context 설정 */
bio_crypt_set_ctx(bio, &key, dun, GFP_NOIO);
/* blk-crypto가 자동으로 판단:
* - HW 지원 → 인라인 엔진에 키 프로그래밍
* - HW 미지원 → blk-crypto-fallback (SW 암호화) */
blk-crypto는 파일시스템(fscrypt)과 블록 디바이스 사이에서
투명하게 동작합니다. 하드웨어 인라인 엔진이 있으면 bio의 crypto context를 하드웨어에 전달하고,
없으면 자동으로 소프트웨어 폴백(blk-crypto-fallback.c)으로 전환합니다.
이 덕분에 fscrypt 코드는 하드웨어 존재 여부를 알 필요가 없습니다.
LUKS2 토큰 시스템
LUKS2의 토큰(Token) 시스템은 키 슬롯 잠금 해제 방식을 추상화합니다.
systemd-cryptenroll을 통해 TPM2, FIDO2, PKCS#11, 복구 키 등
다양한 인증 메커니즘을 LUKS2 헤더에 등록할 수 있습니다.
토큰은 JSON 메타데이터에 저장되며, 각 토큰은 하나 이상의 키 슬롯과 연결됩니다.
| 토큰 유형 | systemd-cryptenroll 옵션 | 인증 방식 | 보안 수준 |
|---|---|---|---|
| 비밀번호 | --password | 사용자 입력 + Argon2id KDF | 비밀번호 복잡도에 의존 |
| TPM2 | --tpm2-device=auto | PCR 봉인 키 (Sealed Key) | 하드웨어 기반, Secure Boot 연동 |
| FIDO2 | --fido2-device=auto | HMAC-Secret Extension | 물리 토큰, 피싱 방어 |
| PKCS#11 | --pkcs11-token-uri=... | 스마트카드 / HSM | 엔터프라이즈 PKI |
| 복구 키 | --recovery-key | 256비트 랜덤 키 (base58) | 비상 복구용 |
# TPM2 토큰 등록 (Secure Boot PCR 바인딩)
# PCR 7: Secure Boot 정책, PCR 4: 부트 관리자
$ systemd-cryptenroll \
--tpm2-device=auto \
--tpm2-pcrs=4+7 \
--tpm2-with-pin=yes \
/dev/nvme0n1p3
# FIDO2 토큰 등록 (YubiKey 5)
$ systemd-cryptenroll \
--fido2-device=auto \
--fido2-with-client-pin=yes \
--fido2-with-user-presence=yes \
--fido2-with-user-verification=no \
/dev/nvme0n1p3
# 복구 키 생성 (안전한 장소에 보관)
$ systemd-cryptenroll --recovery-key /dev/nvme0n1p3
# Recovery key: pmmm-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx
# 등록된 토큰 목록 확인
$ systemd-cryptenroll /dev/nvme0n1p3
SLOT TYPE
0 password
1 tpm2
2 fido2
3 recovery
# 특정 키 슬롯 제거
$ systemd-cryptenroll --wipe-slot=2 /dev/nvme0n1p3
# 토큰 기반 자동 잠금 해제
$ cryptsetup open --token-only /dev/nvme0n1p3 encrypted
# TPM2가 자동으로 키를 제공 (PCR 일치 시)
/* LUKS2 토큰 JSON 구조 상세 */
/* TPM2 토큰 */
{
"type": "systemd-tpm2",
"keyslots": ["1"],
"tpm2-pcrs": [4, 7],
"tpm2-bank": "sha256",
"tpm2-primary-alg": "ecc",
"tpm2-blob": "base64-encoded-sealed-key...",
"tpm2-policy-hash": "hex-encoded-policy...",
"tpm2-pin": true
}
/* FIDO2 토큰 */
{
"type": "systemd-fido2",
"keyslots": ["2"],
"fido2-credential": "base64-credential-id...",
"fido2-salt": "base64-hmac-salt...",
"fido2-rp": "io.systemd.cryptsetup",
"fido2-clientPin-required": true,
"fido2-up-required": true,
"fido2-uv-required": false
}
/* Clevis (Tang 서버) 토큰 — 네트워크 기반 */
{
"type": "clevis",
"keyslots": ["3"],
"clevis": {
"pin": "tang",
"tang": {
"url": "http://tang.internal:7500",
"adv": { /* Tang 서버 공개키 */ }
}
}
}
--tpm2-with-pin=yes를 사용하세요.
이 경우 TPM2 봉인 해제에 PIN 입력이 추가로 필요합니다.
luksSuspend / luksResume 운영
luksSuspend는 dm-crypt 디바이스를 일시 중지하고
커널 메모리에서 마스터 키를 삭제합니다. 이 기능은 노트북 절전(Suspend to RAM) 시
Cold Boot 공격으로부터 암호키를 보호하는 핵심 방어 수단입니다.
luksResume으로 비밀번호를 재입력하면 키가 복원되고 I/O가 재개됩니다.
# luksSuspend: I/O 중지 + 커널에서 키 삭제
# 주의: 실행 즉시 모든 I/O가 블로킹됨 (파일시스템 접근 불가)
$ cryptsetup luksSuspend encrypted
# 상태 확인: suspended 상태
$ dmsetup info encrypted
Name: encrypted
State: SUSPENDED # ← I/O 중지됨
Tables present: LIVE
# luksResume: 비밀번호 재입력 → 키 복원 → I/O 재개
$ cryptsetup luksResume encrypted
Enter passphrase for /dev/sdb1:
# 상태 확인: active 상태 복원
$ dmsetup info encrypted
Name: encrypted
State: ACTIVE # ← I/O 재개됨
systemd 기반 시스템에서 절전 전후 자동으로 luksSuspend/luksResume을
실행하는 서비스 유닛(Unit)을 구성할 수 있습니다.
# /etc/systemd/system/crypt-suspend@.service
[Unit]
Description=Suspend dm-crypt device %i before sleep
Before=sleep.target
StopWhenUnneeded=yes
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/cryptsetup luksSuspend %i
ExecStop=/bin/sh -c '/usr/bin/systemd-ask-password \
--id=luks-resume "Enter passphrase for %i:" | \
/usr/bin/cryptsetup luksResume %i'
[Install]
WantedBy=sleep.target
# 서비스 활성화
$ systemctl enable crypt-suspend@encrypted.service
# 동작 순서:
# 1. 절전 진입 → luksSuspend encrypted (키 삭제)
# 2. RAM에서 키 부재 (Cold Boot 공격 방어)
# 3. 절전 복귀 → systemd-ask-password 호출
# 4. 사용자 비밀번호 입력 → luksResume (키 복원)
luksSuspend를 실행하면
시스템 전체가 멈춥니다 (셸 포함). 따라서 luksResume을 실행할
별도의 수단(initramfs 기반 잠금 해제 화면, 또는 위 systemd 서비스)이 반드시 필요합니다.
cryptsetup-suspend 패키지(Arch Linux 등)가 이 문제를 해결합니다.
원격 잠금 해제
헤드리스(Headless) 서버나 원격 데이터센터에서는 부팅 시 LUKS 비밀번호를 물리적으로 입력할 수 없습니다. initramfs에 SSH 서버(Dropbear)를 내장하거나, Clevis/Tang을 사용한 네트워크 기반 자동 잠금 해제로 이 문제를 해결합니다.
| 방식 | 설명 | 보안 특성 | 적용 환경 |
|---|---|---|---|
| Dropbear + initramfs | 부팅 시 SSH로 원격 비밀번호 입력 | SSH 키 인증 | 소규모 서버 |
| Clevis + Tang | 네트워크 존재 시 자동 잠금 해제 | Shamir Secret Sharing + ECMR | 데이터센터 |
| Clevis + TPM2 | TPM + 네트워크 이중 조건 | 하드웨어 + 네트워크 | 고보안 환경 |
| Clevis SSS | t-of-n 임계값 정책 | 일부 조건만 충족해도 해제 | 혼합 정책 |
## 방법 1: Dropbear SSH를 initramfs에 내장
# Debian/Ubuntu 설치
$ apt install dropbear-initramfs
# SSH 공개 키 등록
$ cp ~/.ssh/id_ed25519.pub /etc/dropbear/initramfs/authorized_keys
# /etc/dropbear/initramfs/dropbear.conf 설정
DROPBEAR_OPTIONS="-p 2222 -s -j -k"
# -p 2222: 포트 지정
# -s: 비밀번호 인증 비활성화 (키만 허용)
# -j -k: 포트 포워딩 비활성화
# initramfs 재생성
$ update-initramfs -u
# 원격에서 부팅 시 잠금 해제
$ ssh -p 2222 root@server "cryptroot-unlock"
Enter passphrase for /dev/sda2:
## 방법 2: Clevis + Tang (네트워크 기반 자동 해제)
# Tang 서버 설치 (전용 키 서버)
$ dnf install tang
$ systemctl enable --now tangd.socket
# 클라이언트: LUKS에 Tang 바인딩
$ clevis luks bind -d /dev/nvme0n1p3 tang \
'{"url":"http://tang.internal:7500"}'
# initramfs에 Clevis 포함
$ dracut -f --regenerate-all # dracut 기반
# 또는
$ apt install clevis-initramfs # Debian 기반
$ update-initramfs -u
# 부팅 시 Tang 서버와 통신하여 자동 잠금 해제
# Tang 서버가 접근 불가하면 비밀번호 입력 대기
## 방법 3: Clevis SSS (Shamir Secret Sharing)
# 2-of-3 정책: TPM + Tang + Password 중 2개 충족 시 해제
$ clevis luks bind -d /dev/nvme0n1p3 sss \
'{"t":2,"pins":{
"tpm2":{"pcr_ids":"4+7"},
"tang":{"url":"http://tang.internal:7500"},
"tang":{"url":"http://tang2.internal:7500"}
}}'
전체 디스크 암호화 성능 벤치마크
dm-crypt 성능을 정확히 측정하려면 알고리즘 성능(CPU 한계), 디스크 I/O 성능,
그리고 결합 성능을 각각 분리하여 벤치마크해야 합니다.
cryptsetup benchmark는 CPU 암호화 처리량을, fio는
실제 디스크 I/O 성능을 측정합니다.
## 1단계: 알고리즘 벤치마크 (CPU 처리량만 측정)
$ cryptsetup benchmark
# 메모리에서만 테스트 — 디스크 I/O 없음
# Tests are approximate using memory only (no storage IO).
# Algorithm | Key | Encryption | Decryption
# aes-cbc 256b 1421.3 MiB/s 4512.8 MiB/s
# aes-xts 256b 4487.3 MiB/s 4432.1 MiB/s
# aes-xts 512b 3891.2 MiB/s 3867.8 MiB/s
# serpent-xts 256b 618.3 MiB/s 612.7 MiB/s
# adiantum 256b 1823.5 MiB/s 1801.2 MiB/s
# 해석:
# - AES-XTS-256: AES-NI 덕분에 ~4.5 GB/s (단일 코어)
# - AES-XTS-512: 256비트 키보다 ~10% 느림 (두 AES 키 처리)
# - AES-CBC 복호화 >> 암호화: CBC 복호화는 병렬화 가능
# - Serpent: AES의 1/7 속도 (소프트웨어만 사용)
# - Adiantum: ChaCha12 기반이라 AES-NI 없어도 빠름
## 2단계: 디스크 기준선(Baseline) 측정 (암호화 없이)
$ fio --name=baseline --ioengine=io_uring --iodepth=64 \
--rw=randread --bs=4k --direct=1 --numjobs=4 \
--filename=/dev/nvme0n1p3 --runtime=30 \
--group_reporting --output-format=json+
## 3단계: dm-crypt 기본 설정 측정
$ cryptsetup open /dev/nvme0n1p3 bench_crypt
$ fio --name=crypt_default --ioengine=io_uring --iodepth=64 \
--rw=randread --bs=4k --direct=1 --numjobs=4 \
--filename=/dev/mapper/bench_crypt --runtime=30 \
--group_reporting --output-format=json+
## 4단계: dm-crypt 최적화 설정 측정
$ cryptsetup close bench_crypt
$ cryptsetup open /dev/nvme0n1p3 bench_crypt \
--perf-no_read_workqueue \
--perf-no_write_workqueue \
--perf-submit_from_crypt_cpus
$ fio --name=crypt_optimized --ioengine=io_uring --iodepth=64 \
--rw=randread --bs=4k --direct=1 --numjobs=4 \
--filename=/dev/mapper/bench_crypt --runtime=30 \
--group_reporting --output-format=json+
## 5단계: 혼합 워크로드(Mixed Workload) 측정
$ fio --name=mixed --ioengine=io_uring --iodepth=32 \
--rw=randrw --rwmixread=70 --bs=4k --direct=1 \
--numjobs=4 --filename=/dev/mapper/bench_crypt \
--runtime=60 --group_reporting
## 6단계: 순차 대역폭 측정
$ fio --name=seq_read --ioengine=io_uring --iodepth=4 \
--rw=read --bs=1M --direct=1 --numjobs=1 \
--filename=/dev/mapper/bench_crypt \
--runtime=30 --group_reporting
| 벤치마크 시나리오 | NVMe 직접 | dm-crypt 기본 | dm-crypt 최적화 | 오버헤드 |
|---|---|---|---|---|
| 순차 읽기 (1M bs) | 3,500 MB/s | 2,800 MB/s | 3,200 MB/s | ~8% |
| 순차 쓰기 (1M bs) | 3,000 MB/s | 2,200 MB/s | 2,700 MB/s | ~10% |
| 랜덤 4K 읽기 (QD64) | 800K IOPS | 600K IOPS | 720K IOPS | ~10% |
| 랜덤 4K 쓰기 (QD64) | 500K IOPS | 350K IOPS | 430K IOPS | ~14% |
| 혼합 70/30 (QD32) | 450K IOPS | 300K IOPS | 380K IOPS | ~15% |
cryptsetup benchmark에서 AES-XTS-256이 4.5GB/s이고 디스크 순차 읽기가
3.5GB/s라면, 암호화는 디스크 속도의 병목이 아닙니다(CPU > Disk).
랜덤 4K IOPS에서 오버헤드가 더 큰 이유는 각 I/O마다 IV 생성 + 컨텍스트 스위칭 비용이
고정적으로 발생하기 때문입니다. no_read/write_workqueue는 이 고정 비용을 줄여줍니다.
# perf를 이용한 dm-crypt 상세 프로파일링
$ perf record -g -p $(pgrep kcryptd) -- sleep 10
$ perf report --sort=dso,symbol
# 예상 상위 함수:
# 45% aesni_xts_encrypt ← AES-NI 암호화 (정상)
# 15% memcpy ← bounce 페이지 복사
# 8% __alloc_pages ← 페이지 할당
# 5% crypt_convert ← dm-crypt 루프 오버헤드
# CPU 사용률 실시간 모니터링
$ perf top -p $(pgrep -d, kcryptd) -g
# aesni_xts_encrypt가 최상위면 정상 동작
# __alloc_pages가 높으면 메모리 압박 (page_pool 증가 고려)
# NUMA 효과 확인
$ numastat -p $(pgrep kcryptd)
# other_node 비율이 높으면 submit_from_crypt_cpus 활성화
Integrity + Encryption (AEAD) 모드 상세
AEAD(Authenticated Encryption with Associated Data) 모드는 암호화와 무결성 검증을 단일 암호 연산으로 수행합니다. dm-crypt의 AEAD 모드에서는 각 섹터마다 16바이트 인증 태그(Authentication Tag)를 생성하여 dm-integrity 레이어가 이 태그를 별도 저장합니다. 읽기 시 태그 검증에 실패하면 I/O 에러를 반환하여 변조된 데이터가 사용자에게 전달되지 않습니다.
/* drivers/md/dm-crypt.c - AEAD 모드 암호화 */
/* AEAD 요청 구조: 인증 태그를 포함한 확장 요청 */
static int crypt_convert_aead(struct crypt_config *cc,
struct convert_context *ctx)
{
struct aead_request *req;
u8 *iv, *tag;
/* AEAD 핸들 (AES-GCM, ChaCha20-Poly1305 등) */
req = aead_request_alloc(cc->tfms_aead[key_idx],
GFP_NOIO);
/* IV 생성: random 모드
* AEAD에서는 IV 재사용이 치명적 → 랜덤 IV 필수
* random IV는 dm-integrity 태그 영역에 함께 저장 */
get_random_bytes(iv, cc->iv_size);
/* scatterlist: [IV][평문데이터] → [IV][암호문][태그] */
sg_init_table(sg_in, 3);
sg_set_buf(&sg_in[0], iv, cc->iv_size); /* AAD */
sg_set_page(&sg_in[1], page, cc->sector_size, offset);
sg_set_buf(&sg_in[2], tag, cc->on_disk_tag_size);
aead_request_set_crypt(req, sg_in, sg_out,
cc->sector_size, iv);
aead_request_set_ad(req, cc->iv_size);
if (bio_data_dir(ctx->bio_in) == WRITE) {
r = crypto_aead_encrypt(req);
/* 암호화 + 인증 태그 생성 (16바이트 GCM tag) */
} else {
r = crypto_aead_decrypt(req);
/* 복호화 + 인증 태그 검증
* 실패 시 -EBADMSG 반환 → I/O 에러 */
}
return r;
}
/* AEAD 읽기 검증 실패 시 */
if (r == -EBADMSG) {
DMERR("INTEGRITY FAILURE sector %llu: "
"authentication tag mismatch",
(unsigned long long)sector);
/* 보안 정책에 따라:
* 1. I/O 에러 반환 (기본)
* 2. 시스템 로그에 기록
* 3. IMA 이벤트 생성 */
}
| AEAD 알고리즘 | 암호화 | 인증 | 태그 크기 | IV 크기 | 성능 |
|---|---|---|---|---|---|
| aes-gcm-random | AES-CTR | GHASH | 16B | 12B (랜덤) | AES-NI로 가속 |
| chacha20-poly1305 | ChaCha20 | Poly1305 | 16B | 12B (랜덤) | AES-NI 없는 환경에 적합 |
| aes-ccm-random | AES-CTR | CBC-MAC | 16B | 11B | GCM보다 약간 느림 |
| aegis128-random | AES 기반 | 내장 | 16B | 16B | AES-NI 전용, 매우 빠름 |
# AEAD 모드 생성 (AES-GCM)
$ cryptsetup luksFormat --type luks2 \
--cipher aes-gcm-random \
--key-size 256 \
--integrity aead \
--sector-size 4096 \
/dev/sdb1
# AEAD 모드 상태 확인
$ cryptsetup open /dev/sdb1 secure
$ cryptsetup status secure
type: LUKS2
cipher: aes-gcm-random
keysize: 256 bits
integrity: aead
integrity keysize: 0 bits
# 내부 스택 확인: dm-crypt + dm-integrity 2개 디바이스
$ dmsetup ls --tree
secure (254:1)
└─secure_dif (254:0) # dm-integrity 레이어
└─ (8:1) # 물리 디바이스
# 무결성 검증 실패 시뮬레이션 (테스트 목적)
$ dd if=/dev/urandom of=/dev/dm-0 bs=512 count=1 seek=100 oflag=direct
# → 해당 섹터 읽기 시 I/O 에러 발생
$ dd if=/dev/mapper/secure of=/dev/null bs=512 count=1 skip=100 iflag=direct
# dd: error reading '/dev/mapper/secure': Input/output error
$ dmesg | tail -1
# device-mapper: crypt: INTEGRITY AEAD ERROR, sector 100
dm-crypt와 io_uring 연동
io_uring은 Linux 5.1에서 도입된 비동기 I/O 인터페이스로,
시스템 콜 오버헤드를 최소화합니다. dm-crypt 위에서 io_uring을 사용하면
높은 I/O 큐 깊이에서 기존 libaio보다 더 나은 성능을 달성할 수 있습니다.
dm-crypt의 no_read/write_workqueue 최적화와 io_uring의
커널 폴링(Polling) 모드가 결합되면 상당한 시너지 효과를 얻습니다.
# io_uring + dm-crypt 벤치마크 비교
# libaio 기반 (기존)
$ fio --name=libaio_crypt \
--ioengine=libaio --iodepth=128 \
--rw=randread --bs=4k --direct=1 \
--numjobs=4 \
--filename=/dev/mapper/encrypted \
--runtime=30 --group_reporting
# 결과 예시: 650K IOPS, avg lat: 780us
# io_uring 기반 (최적화)
$ fio --name=io_uring_crypt \
--ioengine=io_uring --iodepth=128 \
--rw=randread --bs=4k --direct=1 \
--numjobs=4 \
--filename=/dev/mapper/encrypted \
--runtime=30 --group_reporting
# 결과 예시: 720K IOPS, avg lat: 700us
# io_uring 커널 폴링 모드 (IORING_SETUP_IOPOLL)
# NVMe의 폴링 큐를 직접 사용 — 인터럽트 오버헤드 제거
$ fio --name=io_uring_poll \
--ioengine=io_uring --iodepth=128 \
--hipri=1 \
--rw=randread --bs=4k --direct=1 \
--numjobs=4 \
--filename=/dev/mapper/encrypted \
--runtime=30 --group_reporting
# 결과 예시: 760K IOPS, avg lat: 660us
| I/O 엔진 | dm-crypt 기본 | dm-crypt 최적화 | 특징 |
|---|---|---|---|
| sync (read/write) | 50K IOPS | 55K IOPS | 단일 스레드, 동기 |
| libaio | 600K IOPS | 680K IOPS | 비동기, 시스템 콜 기반 |
| io_uring | 650K IOPS | 720K IOPS | 비동기, 링 버퍼 기반 |
| io_uring + IOPOLL | 680K IOPS | 760K IOPS | 커널 폴링, 최저 레이턴시 |
no_read_workqueue + no_write_workqueue + io_uring(IOPOLL)을
결합하면 dm-crypt 오버헤드를 최소화할 수 있습니다.
io_uring의 IORING_SETUP_SQPOLL(커널 제출 폴링)은 dm-crypt 환경에서
시스템 콜 빈도를 줄여 CPU 효율을 더욱 높입니다.
다만 IOPOLL은 O_DIRECT I/O에서만 동작합니다.
ZRAM + dm-crypt 조합
ZRAM(Compressed RAM Block Device)은 메모리에 압축된 블록 디바이스를 생성합니다. ZRAM 위에 dm-crypt를 쌓으면 암호화된 압축 스왑(Swap) 또는 암호화된 임시 스토리지를 구현할 수 있습니다. 이 조합은 메모리가 제한된 임베디드 환경에서 보안과 메모리 효율을 동시에 제공합니다.
# ZRAM + dm-crypt 암호화 스왑 구성
# 1. ZRAM 디바이스 생성 (4GB 압축 블록)
$ modprobe zram num_devices=1
$ echo lz4 > /sys/block/zram0/comp_algorithm
$ echo 4G > /sys/block/zram0/disksize
# 2. ZRAM 위에 dm-crypt 구성 (일회용 랜덤 키)
$ cryptsetup open --type plain \
--cipher aes-xts-plain64 \
--key-size 256 \
--key-file /dev/urandom \
/dev/zram0 zram_crypt
# 3. 암호화된 ZRAM을 스왑으로 설정
$ mkswap /dev/mapper/zram_crypt
$ swapon -p 100 /dev/mapper/zram_crypt
# 스왑 상태 확인
$ swapon --show
NAME TYPE SIZE USED PRIO
/dev/mapper/zram_crypt partition 4G 0B 100
# 참고: systemd-zram-generator가 자동화 가능
# /etc/systemd/zram-generator.conf:
# [zram0]
# zram-size = ram / 2
# compression-algorithm = lz4
| 스왑 유형 | 속도 | 보안 | 메모리 효율 | 사용 사례 |
|---|---|---|---|---|
| 디스크 스왑 (plain) | 디스크 속도 | 취약 (평문) | 디스크 공간 사용 | 비보안 환경 |
| 디스크 + dm-crypt 스왑 | 디스크 속도 - 암호화 | 강함 | 디스크 공간 사용 | 보안 서버 |
| ZRAM 스왑 | 메모리 속도 | 취약 (RAM 상) | 2-3x 압축 | 임베디드/저메모리 |
| ZRAM + dm-crypt 스왑 | 메모리 속도 - 암호화 | 강함 | 2-3x 압축 | 보안 + 메모리 효율 |
# ZRAM + dm-crypt 임시 파일시스템 (tmpfs 대안)
# 2GB 암호화 압축 블록 디바이스 생성
$ echo 2G > /sys/block/zram0/disksize
$ cryptsetup open --type plain \
--cipher aes-xts-plain64 --key-size 256 \
--key-file /dev/urandom \
/dev/zram0 secure_tmp
# ext4 파일시스템 구성
$ mkfs.ext4 /dev/mapper/secure_tmp
$ mount -o nosuid,nodev /dev/mapper/secure_tmp /tmp/secure
# 사용: 민감한 임시 파일 저장
# 시스템 종료 시 메모리와 함께 자동 소멸
# (일회용 랜덤 키이므로 재부팅 후 복구 불가)
# 정리
$ umount /tmp/secure
$ cryptsetup close secure_tmp
$ echo 1 > /sys/block/zram0/reset
관련 문서
- dm-verity — Merkle Tree 기반 읽기 전용 블록 무결성 검증
- dm-integrity — 읽기/쓰기 무결성 보호, 저널링, AEAD 결합
- Device Mapper / LVM — DM 프레임워크 기본, dm-linear, dm-thin, LVM2
- 블록 I/O — 블록 레이어 아키텍처, bio 구조, I/O 경로
- 파일시스템 개요 — VFS, 마운트(Mount), superblock
- I/O 스케줄러 — mq-deadline, BFQ, kyber
- NVMe — NVMe 프로토콜, SQ/CQ, 멀티큐
- Linux Crypto Framework — 커널 암호 API, skcipher, AEAD, ahash
- Secure Boot — UEFI Secure Boot, MOK, 커널 서명
- io_uring — 비동기 I/O 인터페이스
- 커널 문서:
Documentation/admin-guide/device-mapper/dm-crypt.html - cryptsetup/LUKS2: gitlab.com/cryptsetup
- dm-crypt 커널 소스:
drivers/md/dm-crypt.c - 커널 문서 — dm-crypt: kernel.org dm-crypt 문서
- dm-crypt 커널 소스 (Bootlin): dm-crypt.c
- LUKS2 온디스크 포맷 명세: LUKS2 On-Disk Format Specification
- cryptsetup 매뉴얼 페이지: man cryptsetup(8)