dm-verity / dm-integrity / dm-crypt — Device Mapper 데이터 검증, 무결성(Integrity), 암호화(Encryption)
Linux Device Mapper는 블록 디바이스 위에 가상 레이어를 쌓아 데이터 검증(dm-verity), 무결성 보호(dm-integrity),
암호화(dm-crypt)를 투명하게 제공합니다. Android Verified Boot의 핵심인 dm-verity Merkle Tree,
저널링(Journaling) 기반 dm-integrity, LUKS/dm-crypt 블록 암호화, 그리고 이들의 결합(AEAD)까지
커널 내부 구현(drivers/md/)을 심층 분석합니다.
핵심 요약
- Device Mapper — 블록 디바이스 위에 가상 매핑(Mapping) 레이어를 제공하는 커널 프레임워크
- dm-verity — Merkle Tree 기반 읽기 전용(Read-Only) 블록 무결성 검증 (Android Verified Boot 핵심)
- dm-integrity — 읽기/쓰기 모두 지원하는 무결성 태그 + 저널링
- dm-crypt — 블록 레벨 투명 암호화 (LUKS, AES-XTS, Adiantum)
- AEAD 결합 — dm-crypt + dm-integrity를 결합한 인증 암호화 (AES-GCM)
단계별 이해
- Device Mapper 레이어 이해
커널의 블록 I/O 스택에서 DM은 bio를 가로채 target 드라이버에 전달합니다. - dm-verity로 읽기 검증
빌드 시 Merkle Tree를 생성하고, 런타임에 블록 단위로 해시를 검증합니다. - dm-integrity로 실시간 무결성
매 쓰기마다 무결성 태그를 계산하고 저널에 기록하여 crash-safety를 보장합니다. - dm-crypt로 암호화
섹터 단위로 AES-XTS 암호화를 적용하여 디스크 데이터를 보호합니다. - 결합 활용
dm-crypt + dm-integrity AEAD 모드로 암호화와 무결성을 동시에 달성합니다.
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 verity 1 /dev/sda1 /dev/sda2 4096 4096 262144 1 sha256 root_hash salt
/* 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-verity 개요
dm-verity는 읽기 전용 블록 디바이스의 무결성을 검증하는 Device Mapper target입니다.
ChromeOS에서 최초 도입되어 Android Verified Boot(AVB)의 핵심 메커니즘으로 자리잡았습니다.
커널 소스 drivers/md/dm-verity-target.c에 구현되어 있으며,
빌드 시점에 생성된 Merkle Tree의 루트 해시를 런타임에 블록 단위로 검증합니다.
dm-verity의 핵심 원리는 간단합니다: 디스크의 모든 4KB 블록에 대해 SHA-256(또는 다른 해시) 값을 계산하고, 이 해시들을 다시 해싱하여 트리 구조를 만듭니다. 최종 루트 해시 하나만 신뢰할 수 있으면 전체 디스크의 무결성을 검증할 수 있습니다. 읽기 요청이 들어올 때마다 해당 블록의 해시를 Merkle Tree에서 확인하여 변조를 탐지합니다.
/* drivers/md/dm-verity.h */
struct dm_verity {
struct dm_dev *data_dev; /* 데이터 디바이스 */
struct dm_dev *hash_dev; /* 해시 트리 디바이스 */
struct dm_target *ti;
struct dm_verity_fec *fec; /* FEC 옵션 */
unsigned int data_dev_block_bits; /* 데이터 블록 크기 비트 */
unsigned int hash_dev_block_bits; /* 해시 블록 크기 비트 */
unsigned int hash_per_block_bits; /* 블록당 해시 수 비트 */
unsigned int levels; /* 트리 레벨 수 */
unsigned int digest_size; /* 해시 출력 크기 */
unsigned int salt_size;
const char *alg_name; /* "sha256" 등 */
u8 *root_digest; /* 루트 해시 */
u8 *salt; /* salt 값 */
sector_t data_blocks; /* 총 데이터 블록 수 */
sector_t hash_start; /* 해시 영역 시작 */
sector_t hash_blocks; /* 해시 블록 수 */
enum verity_mode mode; /* EIO, logging, restart, panic */
unsigned int validated_blocks;
struct workqueue_struct *verify_wq;
};
DM_VERITY_MODE_EIO(기본): 검증 실패 시 -EIO 반환,
DM_VERITY_MODE_LOGGING: 로그만 기록하고 I/O 허용,
DM_VERITY_MODE_RESTART: 시스템 재부팅,
DM_VERITY_MODE_PANIC: 커널 패닉(Kernel Panic) 유발
| 속성 | dm-verity | 비교 대상 |
|---|---|---|
| 방향 | 읽기 전용 | dm-integrity: 읽기/쓰기 |
| 검증 단위 | 블록 (4KB 기본) | 파일 단위: fs-verity |
| 해시 저장 | 별도 파티션/영역 | dm-integrity: 인라인 태그 |
| 주요 용도 | Verified Boot, 컨테이너(Container) 이미지 | 서버 디스크 무결성 |
| 오버헤드(Overhead) | 읽기 시 해시 계산 + 트리 탐색 | 읽기/쓰기 모두 오버헤드 |
| 커널 소스 | dm-verity-target.c | dm-integrity.c |
Merkle Tree 구조
dm-verity의 Merkle Tree는 데이터 블록들의 해시를 계층적으로 구성합니다. 리프(leaf) 레벨에서 각 데이터 블록(4KB)의 해시(SHA-256 기준 32바이트)를 계산하고, 이 해시들을 하나의 해시 블록(역시 4KB)에 모아 다시 해싱합니다. 4KB 블록에 32바이트 해시가 128개 들어가므로, 각 레벨에서 팬아웃(fan-out)은 128입니다. 1GB 데이터의 경우 SHA-256 기준으로 약 8MB + 64KB + 512B + 32B = 약 8.06MB의 해시 공간이 필요하며, 이는 전체 데이터의 약 0.8%에 불과합니다.
/* Merkle Tree 레벨별 블록 수 계산 (커널 내부) */
/* drivers/md/dm-verity-target.c: verity_ctr() */
static int verity_hash_levels(struct dm_verity *v)
{
sector_t hash_position;
int levels = 0;
sector_t data_blocks = v->data_blocks;
/* hash_per_block = 블록_크기 / digest_size
* SHA-256, 4KB 블록: 4096/32 = 128 */
while (data_blocks > 1) {
data_blocks = DIV_ROUND_UP(data_blocks,
1 << v->hash_per_block_bits);
levels++;
}
v->levels = levels;
/* 각 레벨의 해시 블록 시작 위치 계산 */
hash_position = 0;
for (int i = levels - 1; i >= 0; i--) {
v->hash_level_block[i] = hash_position;
data_blocks = DIV_ROUND_UP(v->data_blocks,
(sector_t)1 << (v->hash_per_block_bits * (i + 1)));
hash_position += data_blocks;
}
return 0;
}
veritysetup format의 --hash 옵션으로 지정합니다.
dm-verity 설정
dm-verity를 설정하려면 먼저 veritysetup 도구로 Merkle Tree를 생성하고,
이를 DM 테이블에 로드합니다. veritysetup은 cryptsetup 패키지에 포함되어 있습니다.
설정 과정은 오프라인 해시 생성과 온라인 검증 활성화 2단계로 나뉩니다.
# 1. 데이터 디바이스에 대한 해시 트리 생성
$ veritysetup format /dev/sda1 /dev/sda2
VERITY header information for /dev/sda2
UUID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Hash type: 1
Data blocks: 262144
Data block size: 4096
Hash block size: 4096
Hash algorithm: sha256
Salt: abcdef0123456789abcdef0123456789...
Root hash: 4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d...
# 2. dm-verity 디바이스 활성화
$ veritysetup open /dev/sda1 verified_root /dev/sda2 \
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d...
# 3. 읽기 전용으로 마운트
$ mount -o ro /dev/mapper/verified_root /mnt/verified
# 4. dmsetup으로 직접 설정하는 경우
$ echo "0 2097152 verity 1 /dev/sda1 /dev/sda2 4096 4096 \
262144 1 sha256 \
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d... \
abcdef0123456789abcdef0123456789..." | dmsetup create verified_root
| 필드 | 설명 | 예시 값 |
|---|---|---|
| version | verity 포맷 버전 (현재 1) | 1 |
| data_dev | 데이터 블록 디바이스 | /dev/sda1 |
| hash_dev | 해시 트리 디바이스 | /dev/sda2 |
| data_block_size | 데이터 블록 크기 | 4096 |
| hash_block_size | 해시 블록 크기 | 4096 |
| num_data_blocks | 데이터 블록 수 | 262144 |
| hash_start_block | 해시 데이터 시작 블록 | 1 |
| algorithm | 해시 알고리즘 | sha256 |
| root_hash | Merkle Tree 루트 해시 | 64자 hex |
| salt | 해시 솔트 (선택) | hex 또는 - |
커널 내부 구현
dm-verity의 커널 구현은 drivers/md/dm-verity-target.c에 있습니다.
핵심 함수는 verity_map()과 verity_end_io()이며,
워크큐 기반으로 해시 검증을 수행합니다. 읽기 bio가 도착하면 verity_map()이
bi_end_io를 교체하고 데이터 디바이스로 remap합니다. 데이터 읽기가 완료되면
verity_end_io()가 워크큐에서 해시 검증을 스케줄합니다.
/* drivers/md/dm-verity-target.c */
static int verity_map(struct dm_target *ti, struct bio *bio)
{
struct dm_verity *v = ti->private;
struct dm_verity_io *io;
/* 쓰기 요청은 거부 (읽기 전용) */
if (bio_data_dir(bio) == WRITE) {
bio->bi_status = BLK_STS_IOERR;
bio_endio(bio);
return DM_MAPIO_SUBMITTED;
}
io = dm_per_bio_data(bio, ti->per_io_data_size);
io->v = v;
io->orig_bi_end_io = bio->bi_end_io;
io->block = bio->bi_iter.bi_sector >> (v->data_dev_block_bits - SECTOR_SHIFT);
io->n_blocks = bio->bi_iter.bi_size >> v->data_dev_block_bits;
bio->bi_end_io = verity_end_io;
bio_set_dev(bio, v->data_dev->bdev);
verity_prefetch_io(v, io);
return DM_MAPIO_REMAPPED;
}
static void verity_end_io(struct bio *bio)
{
struct dm_verity_io *io = dm_per_bio_data(bio, ...);
if (bio->bi_status) {
verity_finish_io(io, bio->bi_status);
return;
}
INIT_WORK(&io->work, verity_work);
queue_work(io->v->verify_wq, &io->work);
}
verity_verify_io()는 각 데이터 블록에 대해 bottom-up으로 Merkle Tree를 순회합니다:
데이터 블록의 해시를 계산하고, 이를 Level 0 해시와 비교한 뒤,
Level 0 해시 블록의 해시를 Level 1과 비교하는 방식으로 루트까지 올라갑니다.
한 번 검증된 해시 블록은 dm_bufio 캐시에 보관되어 재검증을 피합니다.
/* drivers/md/dm-verity-target.c - verity_verify_io() 핵심 */
static int verity_verify_io(struct dm_verity_io *io)
{
struct dm_verity *v = io->v;
struct bvec_iter start;
unsigned int b;
for (b = 0; b < io->n_blocks; b++) {
int r;
sector_t cur_block = io->block + b;
struct ahash_request *req = verity_io_hash_req(v, io);
/* 1단계: 데이터 블록 해시 계산 */
r = verity_hash_for_block(v, io, cur_block,
verity_io_want_digest(v, io),
&is_zero);
if (unlikely(r < 0))
return r;
/* 2단계: Merkle Tree 검증 (bottom-up) */
r = verity_verify_level(v, io, cur_block, 0,
verity_io_want_digest(v, io));
if (r)
return r;
}
return 0;
}
/* 해시 블록 prefetch - 성능 최적화 핵심 */
static void verity_prefetch_io(struct dm_verity *v,
struct dm_verity_io *io)
{
int i;
for (i = v->levels - 1; i >= 0; i--) {
sector_t hash_block_start, hash_block_end;
/* 이 bio가 접근할 해시 블록 범위 계산 */
hash_block_start = io->block >> (v->hash_per_block_bits * (i + 1));
hash_block_end = (io->block + io->n_blocks - 1) >>
(v->hash_per_block_bits * (i + 1));
hash_block_start += v->hash_level_block[i];
hash_block_end += v->hash_level_block[i];
/* dm_bufio를 통해 비동기 prefetch */
dm_bufio_prefetch(v->bufio, hash_block_start,
hash_block_end - hash_block_start + 1);
}
}
check_at_most_once 옵션을 활성화하면 각 블록이 최대 1회만 검증됩니다.
검증된 블록은 비트맵(Bitmap)으로 추적되어 재검증을 건너뜁니다.
이 옵션은 신뢰할 수 있는 스토리지(예: 읽기 전용 미디어)에서
성능을 크게 개선하지만, 메모리를 추가로 소비합니다.
1TB 디바이스 기준 약 32MB의 비트맵 메모리가 필요합니다.
/* dm-verity target 등록 - 모듈 초기화 */
static struct target_type verity_target = {
.name = "verity",
.version = {1, 9, 0},
.module = THIS_MODULE,
.ctr = verity_ctr,
.dtr = verity_dtr,
.map = verity_map,
.status = verity_status,
.prepare_ioctl = verity_prepare_ioctl,
.iterate_devices = verity_iterate_devices,
.io_hints = verity_io_hints,
};
/* verity_ctr() 주요 파라미터 파싱 */
static int verity_ctr(struct dm_target *ti,
unsigned int argc, char **argv)
{
struct dm_verity *v;
int r, i;
unsigned int num;
v = kzalloc(sizeof(*v), GFP_KERNEL);
v->ti = ti;
/* argv[0]: version
* argv[1]: data_dev
* argv[2]: hash_dev
* argv[3]: data_block_size
* argv[4]: hash_block_size
* argv[5]: num_data_blocks
* argv[6]: hash_start_block
* argv[7]: algorithm
* argv[8]: root_hash
* argv[9]: salt (또는 "-") */
r = dm_get_device(ti, argv[1], FMODE_READ, &v->data_dev);
r = dm_get_device(ti, argv[2], FMODE_READ, &v->hash_dev);
/* Crypto API로 해시 알고리즘 할당 */
v->tfm = crypto_alloc_ahash(argv[7], 0, 0);
v->digest_size = crypto_ahash_digestsize(v->tfm);
/* Merkle Tree 레벨 계산 */
verity_hash_levels(v);
/* dm_bufio 클라이언트 생성 (해시 블록 캐시) */
v->bufio = dm_bufio_client_create(v->hash_dev->bdev,
1 << v->hash_dev_block_bits, 1, 0,
NULL, NULL, 0);
/* 검증 워크큐 생성 */
v->verify_wq = alloc_workqueue("kverityd",
WQ_CPU_INTENSIVE | WQ_MEM_RECLAIM, num_online_cpus());
/* 선택적 파라미터: FEC, check_at_most_once 등 */
r = verity_parse_opt_args(ti, v, argc - 10, argv + 10);
ti->private = v;
return 0;
}
| verity_ctr 선택 파라미터 | 설명 | 커널 버전 |
|---|---|---|
check_at_most_once | 블록당 최대 1회 검증 | 4.17+ |
ignore_corruption | 검증 실패 시 I/O 허용 (로깅만) | 4.4+ |
restart_on_corruption | 검증 실패 시 시스템 재부팅 | 4.4+ |
panic_on_corruption | 검증 실패 시 커널 패닉 | 4.17+ |
use_fec_from_device | FEC 디바이스 지정 | 4.4+ |
fec_roots | RS 패리티 바이트 수 | 4.4+ |
fec_blocks | FEC 보호 블록 수 | 4.4+ |
fec_start | FEC 데이터 시작 블록 | 4.4+ |
root_hash_sig_key_desc | 루트 해시 서명 검증(Signature Verification) keyring 키 | 5.4+ |
dm-verity의 성능 특성을 이해하는 것이 중요합니다. 각 4KB 데이터 블록을 읽을 때마다
SHA-256 해시 1회 + Merkle Tree 레벨 수만큼의 추가 해시 검증이 필요합니다.
단, dm_bufio 캐시 덕분에 상위 레벨 해시 블록은 대부분 캐시 히트되며,
실질적인 오버헤드는 리프 레벨 해시 1회 + 해시 블록 읽기 1-2회 정도입니다.
SHA-256의 소프트웨어 처리량(Throughput)은 약 500MB/s이며, SHA-NI 하드웨어 가속 시 2-3GB/s까지 향상됩니다.
Android Verified Boot
Android Verified Boot(AVB)는 dm-verity를 기반으로 시스템 파티션의 무결성을 보장합니다. AVB 2.0(libavb)은 부트로더에서 vbmeta 파티션을 검증하고, vbmeta에 포함된 dm-verity 루트 해시를 커널의 dm-verity target에 전달합니다. 이 체인을 통해 부트로더 → 커널 → 시스템 파티션까지 전체 부팅 경로의 무결성이 보장됩니다.
# Android 디바이스에서 dm-verity 상태 확인
$ adb shell dmctl list devices
system-verity : active
vendor-verity : active
# vbmeta 구조 확인
$ avbtool info_image --image vbmeta.img
Header Block: 256 bytes
Authentication Block: 576 bytes
Algorithm: SHA256_RSA4096
Rollback Index: 1
Descriptors:
Hashtree descriptor:
dm-verity Version: 1
Image Size: 3221225472 bytes
Tree Offset: 3221225472
Tree Size: 25427968
Data Block Size: 4096
Hash Algorithm: sha256
FEC num roots: 2
Root Digest: abc123...
| AVB 부팅 상태 | 설명 | 사용자 표시 |
|---|---|---|
| GREEN | 모든 검증 통과, 공식 키 | 정상 부팅 |
| YELLOW | 검증 통과, 사용자 커스텀 키 | 경고 표시 |
| ORANGE | 부트로더 언락, 검증 비활성 | 경고 + 5초 대기 |
| RED | 검증 실패 | 부팅 거부 또는 경고 |
# Android dm-verity 커맨드라인 (init에서 파싱)
# 부트로더가 커널 cmdline에 삽입하는 dm-verity 파라미터
androidboot.veritymode=enforcing
dm="1 vroot none ro 1,
0 6291456 verity 1
PARTUUID=a1b2c3d4 PARTUUID=e5f6g7h8
4096 4096 786432 786433 sha256
4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
10 restart_on_corruption ignore_zero_blocks use_fec_from_device
PARTUUID=i9j0k1l2 fec_roots 2 fec_blocks 786432 fec_start 786432"
# Android init에서 dm-verity 설정 과정
# 1. first_stage_init: 부트 이미지에서 fstab 로드
# 2. fs_mgr: fstab의 verify 옵션 파싱
# 3. fs_mgr_setup_verity(): dm-verity 테이블 생성
# 4. dm_ioctl(DM_TABLE_LOAD): 커널에 테이블 로드
# 5. dm_ioctl(DM_DEV_SUSPEND): 디바이스 활성화
# 6. mount(): 검증된 디바이스 마운트
# Android fstab.device 예시
# system /system ext4 ro,barrier=1 wait,verify,first_stage_mount
# vendor /vendor ext4 ro,barrier=1 wait,verify,first_stage_mount
# vbmeta에 rollback protection 인덱스 설정
$ avbtool make_vbmeta_image --output vbmeta.img --key /path/to/key.pem --algorithm SHA256_RSA4096 --rollback_index 3 --chain_partition system:1:/path/to/system_key.avbpubkey --chain_partition vendor:2:/path/to/vendor_key.avbpubkey
--rollback_index 0을 사용하세요.
| Android 파티션 | 보호 방식 | 검증 타이밍 |
|---|---|---|
| boot | AVB 서명 검증 (전체 이미지) | 부트로더 |
| init_boot | AVB 서명 검증 | 부트로더 |
| system | dm-verity (Merkle Tree + FEC) | first_stage_init |
| vendor | dm-verity | first_stage_init |
| product | dm-verity | first_stage_init |
| system_ext | dm-verity | first_stage_init |
| vbmeta | RSA-4096 서명 | 부트로더 |
| userdata | dm-crypt (FBE) 또는 ICE | vold |
| metadata | dm-default-key (메타데이터 암호화) | init |
Android 12+에서는 File-Based Encryption(FBE)이 기본이며,
dm-default-key target으로 메타데이터 암호화를 추가합니다.
기존의 Full-Disk Encryption(FDE, dm-crypt 단일)은 deprecated되었습니다.
FBE는 각 사용자의 CE(Credential Encrypted) 키와 DE(Device Encrypted) 키를
분리하여, 잠금 화면에서도 알람이나 전화 수신이 가능합니다.
Forward Error Correction (FEC)
dm-verity FEC는 Reed-Solomon 부호를 사용하여 손상된 블록을 자동 복구합니다.
drivers/md/dm-verity-fec.c에 구현되어 있으며,
Android에서는 시스템 파티션에 약 0.8%의 FEC 데이터를 추가하여
비트 에러로 인한 검증 실패 시 자동 복구를 시도합니다.
FEC는 dm-verity의 선택 기능으로, veritysetup format에
--fec-device와 --fec-roots 옵션을 지정하여 활성화합니다.
/* drivers/md/dm-verity-fec.h */
struct dm_verity_fec {
struct dm_dev *dev; /* FEC 데이터 디바이스 */
sector_t start; /* FEC 데이터 시작 섹터 */
sector_t blocks; /* FEC 블록 수 */
sector_t rounds; /* RS 인코딩 라운드 수 */
unsigned int rsn; /* RS 블록 크기 (255 기본) */
unsigned int roots; /* RS 패리티 바이트 수 (2 기본) */
struct dm_bufio_client *bufio;
mempool_t rs_pool; /* RS 컨텍스트 풀 */
};
# FEC가 포함된 dm-verity 설정
$ veritysetup format --fec-device=/dev/sda3 --fec-roots=2 \
/dev/sda1 /dev/sda2
$ veritysetup open --fec-device=/dev/sda3 --fec-roots=2 \
/dev/sda1 verified_root /dev/sda2 ROOT_HASH
--fec-roots=2로 설정하면 RS(255,253)이 적용되어
전체 데이터의 약 0.8%만 추가 공간이 필요합니다. roots 값을 높이면 복구 능력이 증가하지만
공간 오버헤드도 비례하여 증가합니다.
FEC 디코딩 과정을 상세히 살펴보면, dm-verity가 검증 실패를 감지하면
verity_fec_decode()를 호출합니다. 이 함수는 손상된 블록이 포함된
RS 블록을 찾아 decode_rs8()으로 디코딩을 시도합니다.
RS(255,253)에서는 최대 1바이트의 에러를 정정할 수 있으며,
erasure 위치를 알면 최대 2바이트까지 복구 가능합니다.
FEC 복구 성공 시 정정된 데이터가 반환되고, 실패 시 원래의 검증 실패 오류가 전달됩니다.
| FEC 파라미터 | RS 코드 | 패리티 | 공간 오버헤드 | 복구 능력 |
|---|---|---|---|---|
| roots=2 | RS(255,253) | 2바이트/블록 | ~0.8% | 1 에러/블록 |
| roots=4 | RS(255,251) | 4바이트/블록 | ~1.6% | 2 에러/블록 |
| roots=8 | RS(255,247) | 8바이트/블록 | ~3.1% | 4 에러/블록 |
| roots=24 | RS(255,231) | 24바이트/블록 | ~9.4% | 12 에러/블록 |
dm-integrity 개요
dm-integrity는 읽기/쓰기 모두 지원하는 블록 레벨 무결성 보호를 제공합니다.
dm-verity와 달리 쓰기 시에도 무결성 태그를 자동으로 계산하여 저장하며,
저널링을 통해 crash-safety를 보장합니다. 커널 소스 drivers/md/dm-integrity.c에 구현되어 있으며,
Journal, Bitmap, Direct(no-journal) 세 가지 모드를 제공합니다.
/* drivers/md/dm-integrity.c */
struct dm_integrity_c {
struct dm_dev *dev;
struct dm_dev *meta_dev; /* 별도 메타 디바이스 (선택) */
unsigned int tag_size; /* 태그 크기 (바이트) */
unsigned int sectors_per_block; /* 블록당 섹터 수 */
__u64 provided_data_sectors; /* 사용 가능 데이터 섹터 */
unsigned int journal_entries;
unsigned int journal_sections;
struct journal_entry *journal;
struct crypto_shash *internal_hash;
struct crypto_shash *journal_mac;
enum integrity_mode mode; /* J, B, D (journal/bitmap/direct) */
bool recalculate;
};
| 모드 | 저널 | Crash Safety | 성능 | 주요 용도 |
|---|---|---|---|---|
| Journal (J) | 전체 저널 | 완전 보장 | 쓰기 2x 오버헤드 | 단독 사용 |
| Bitmap (B) | 비트맵만 | dirty 블록 재검증 | 저널보다 빠름 | 대용량 디스크 |
| Direct (D) | 없음 | 비보장 | 최소 오버헤드 | AEAD 모드 전용 |
저널링 메커니즘
dm-integrity의 저널은 데이터와 무결성 태그를 원자적(Atomic)으로 기록하여 crash-safety를 보장합니다. 저널은 순환 버퍼(circular buffer)로 구현되며, commit 단위로 데이터+태그 쌍을 기록합니다. Bitmap 모드에서는 저널 대신 비트맵으로 dirty 블록을 추적하며, crash 후 복구 시 dirty 비트가 설정된 블록만 재검증합니다.
/* drivers/md/dm-integrity.c - 저널 엔트리 구조 */
struct journal_entry {
union {
struct {
__le64 sector; /* 대상 섹터 번호 */
__le32 data_csum; /* 데이터 체크섬 */
__le32 tag_csum; /* 태그 체크섬 */
} s;
__le64 commit_id; /* 커밋 ID */
} u;
};
--journal-watermark 옵션으로 저널 flush 임계값을 설정할 수 있습니다.
저널이 가득 차면 쓰기가 차단되므로, 워크로드에 맞는 적절한 크기 설정이 중요합니다.
dm-integrity 설정
dm-integrity는 integritysetup 도구로 설정합니다.
cryptsetup 패키지에 포함되어 있으며, 포맷 후 활성화하는 2단계로 진행합니다.
# 1. dm-integrity 포맷 (SHA-256 해시, 저널 모드)
$ integritysetup format /dev/sdb1 \
--integrity sha256 \
--tag-size 32 \
--sector-size 4096 \
--journal-size 64M
# 2. dm-integrity 디바이스 활성화
$ integritysetup open /dev/sdb1 integrity_disk \
--integrity sha256
# 3. 파일시스템 생성 및 마운트
$ mkfs.ext4 /dev/mapper/integrity_disk
$ mount /dev/mapper/integrity_disk /mnt/secure
# 4. Bitmap 모드로 설정 (성능 우선)
$ integritysetup format /dev/sdb1 \
--integrity sha256 \
--integrity-bitmap-mode \
--bitmap-sectors-per-bit 65536
$ integritysetup open /dev/sdb1 integrity_bitmap \
--integrity sha256 \
--integrity-bitmap-mode
| 옵션 | 설명 | 기본값 |
|---|---|---|
--integrity | 해시 알고리즘 | sha256 |
--tag-size | 무결성 태그 크기 (바이트) | 알고리즘 출력 크기 |
--sector-size | 섹터 크기 | 512 |
--journal-size | 저널 크기 | 자동 계산 |
--journal-watermark | 저널 flush 임계값 (%) | 50% |
--journal-commit-time | 저널 commit 주기 (ms) | 10000 |
--integrity-bitmap-mode | 비트맵 모드 사용 | 비활성 |
dm-integrity 커널 내부 구현
dm-integrity의 커널 구현은 drivers/md/dm-integrity.c에 있으며,
약 4,500줄에 달하는 복잡한 코드입니다. 핵심 동작은 bio를 분할하여
데이터와 무결성 태그를 동시에 처리하는 것입니다.
/* drivers/md/dm-integrity.c - map 함수 */
static int integrity_map(struct dm_target *ti, struct bio *bio)
{
struct dm_integrity_c *ic = ti->private;
struct dm_integrity_io *dio;
dio = dm_per_bio_data(bio, ti->per_io_data_size);
dio->ic = ic;
dio->bi_sector = bio->bi_iter.bi_sector;
dio->bi_integrity = bio_integrity(bio);
if (bio_data_dir(bio) == WRITE) {
if (ic->mode == 'J') {
/* Journal 모드: 저널에 먼저 기록 */
integrity_journal_write(ic, dio);
} else if (ic->mode == 'B') {
/* Bitmap 모드: dirty 비트 설정 */
integrity_bitmap_write(ic, dio);
}
/* 무결성 태그 계산 및 저장 */
integrity_metadata_write(ic, dio);
} else {
/* READ: 데이터 읽기 후 태그 검증 */
integrity_metadata_read(ic, dio);
}
/* bio를 데이터 디바이스로 remap */
bio_set_dev(bio, ic->dev->bdev);
bio->bi_iter.bi_sector = integrity_data_sector(ic, dio->bi_sector);
return DM_MAPIO_REMAPPED;
}
/* 무결성 태그 검증 (읽기 시) */
static int integrity_check_tag(struct dm_integrity_c *ic,
u8 *data, u8 *stored_tag,
sector_t sector)
{
u8 computed_tag[MAX_TAG_SIZE];
int r;
/* 해시 계산 */
r = crypto_shash_digest(ic->internal_hash_desc,
data, ic->sectors_per_block << SECTOR_SHIFT,
computed_tag);
if (r)
return r;
/* 저장된 태그와 비교 */
if (memcmp(computed_tag, stored_tag, ic->tag_size)) {
DMERR("integrity checksum failed at sector %llu",
(unsigned long long)sector);
return -EILSEQ;
}
return 0;
}
dm-integrity의 I/O 경로에서 주의할 점은 메타데이터 I/O입니다.
데이터와 태그가 디스크 상에서 인터리빙되어 있기 때문에,
연속적인 데이터 I/O가 비연속적인 디스크 I/O로 변환될 수 있습니다.
이로 인해 HDD에서는 성능 저하가 심각할 수 있으며, SSD에서는 상대적으로 영향이 적습니다.
별도의 메타 디바이스(--data-device)를 사용하면 이 문제를 완화할 수 있습니다.
dm-integrity의 내부 동작을 더 깊이 살펴보면, 데이터 섹터와 태그 영역의 매핑이 핵심입니다.
논리 섹터 번호를 물리 섹터 번호로 변환할 때, 태그 영역의 크기를 고려하여 오프셋(Offset)을 계산합니다.
이 변환은 get_data_sector() 함수에서 수행됩니다.
/* drivers/md/dm-integrity.c - 섹터 변환 */
static sector_t get_data_sector(struct dm_integrity_c *ic,
area_t area, sector_t offset)
{
sector_t result;
/* 데이터 영역과 태그 영역이 인터리빙됨
* area = 논리 섹터 / interleave_sectors
* offset = 논리 섹터 % interleave_sectors */
result = area * (ic->interleave_sectors + ic->tag_area_sectors);
result += ic->initial_sectors; /* 슈퍼블록 + 저널 공간 */
result += offset;
return result;
}
/* 태그 읽기/쓰기 - 메타데이터 I/O */
static void rw_tag(struct dm_integrity_c *ic,
unsigned int tag_offset,
u8 *tag, unsigned int tag_size,
int op)
{
sector_t tag_sector;
unsigned int tag_sector_offset;
/* 태그의 디스크 위치 계산 */
tag_sector = tag_offset / (ic->tag_size * ic->journal_section_entries);
tag_sector_offset = tag_offset % (ic->tag_size * ic->journal_section_entries);
if (op == TAG_READ)
dm_bufio_read(ic->tag_bufio, tag_sector, ...);
else
dm_bufio_write_dirty(ic->tag_bufio, tag_sector, ...);
}
--data-device)를 사용하면
데이터와 태그를 물리적으로 분리하여 이 문제를 완화할 수 있습니다.
# 별도 메타 디바이스로 dm-integrity 설정 (성능 최적화)
$ integritysetup format --data-device /dev/nvme0n1p1 /dev/nvme1n1p1 --integrity sha256 --sector-size 4096
# 결과: 데이터는 nvme0n1p1, 태그는 nvme1n1p1에 저장
# 두 NVMe를 병렬로 사용하여 성능 극대화
| dm-integrity 커널 함수 | 역할 | 호출 시점 |
|---|---|---|
integrity_ctr() | target 생성, 슈퍼블록(Superblock) 로드 | 테이블 로드 |
integrity_dtr() | target 해제, 저널 flush | 디바이스 제거 |
integrity_map() | bio remap + 태그 처리 | 매 I/O |
integrity_end_io() | 읽기 태그 검증 | 읽기 완료 |
do_journal_write() | 저널 -> 실데이터 플러시(Flush) | 저널 watermark |
integrity_recalc() | 기존 데이터 태그 재계산 | recalculate 모드 |
integrity_bitmap_flush() | 비트맵 dirty 플러시 | bitmap 모드 |
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 + 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 또는 배터리 백업이 없는 환경에서는 저널 모드가 권장됩니다.
dm-flakey / dm-dust
dm-flakey와 dm-dust는 테스트 및 결함 주입(fault injection)용 Device Mapper target입니다. 파일시스템이나 스토리지 스택의 에러 처리 경로를 테스트하는 데 사용됩니다.
/* dm-flakey: 주기적으로 I/O 에러를 발생시킴 */
# 10초 정상 동작 후 5초간 모든 I/O에 에러 반환
$ echo "0 $(blockdev --getsz /dev/sdb1) flakey /dev/sdb1 0 10 5" | \
dmsetup create flakey_disk
# 옵션: 쓰기만 실패, 읽기는 정상
$ echo "0 $(blockdev --getsz /dev/sdb1) flakey /dev/sdb1 0 10 5 \
1 drop_writes" | dmsetup create flakey_write
# dm-flakey 커널 구현 (간략화)
static int flakey_map(struct dm_target *ti, struct bio *bio)
{
struct flakey_c *fc = ti->private;
unsigned elapsed = jiffies - fc->start_time;
if (elapsed % (fc->up_interval + fc->down_interval)
>= fc->up_interval) {
/* down 구간: 에러 반환 또는 데이터 변조 */
if (fc->drop_writes && bio_data_dir(bio) == WRITE) {
bio_endio(bio); /* 쓰기 무시 */
return DM_MAPIO_SUBMITTED;
}
bio->bi_status = BLK_STS_IOERR;
bio_endio(bio);
return DM_MAPIO_SUBMITTED;
}
/* up 구간: 정상 통과 */
flakey_map_bio(ti, bio);
return DM_MAPIO_REMAPPED;
}
/* dm-dust: 특정 블록에 읽기 에러를 주입 */
# dm-dust 디바이스 생성
$ echo "0 $(blockdev --getsz /dev/sdb1) dust /dev/sdb1 0 4096" | \
dmsetup create dusty_disk
# 특정 블록에 bad block 마킹
$ dmsetup message dusty_disk 0 addbadblock 100
$ dmsetup message dusty_disk 0 addbadblock 200
# bad block 활성화
$ dmsetup message dusty_disk 0 enable
# 블록 100, 200 읽기 시 -EIO 반환
# 파일시스템의 에러 핸들링 로직 테스트에 활용
# bad block 제거
$ dmsetup message dusty_disk 0 removebadblock 100
dm-user / dm-verity-loadpin
dm-user는 사용자 공간에서 Device Mapper target을 구현할 수 있게 하는 메커니즘입니다. ChromeOS와 Android에서 보안 부팅 체인을 강화하는 데 사용됩니다. dm-verity-loadpin은 커널 모듈(Kernel Module)과 펌웨어(Firmware) 로딩을 dm-verity로 검증된 파티션으로 제한합니다.
/* security/loadpin/loadpin.c */
/* LoadPin: 모든 커널 모듈/펌웨어가
* 동일한 검증된 파일시스템에서만 로드되도록 강제 */
static int loadpin_read_file(struct file *file,
enum kernel_read_file_id id,
bool contents)
{
struct super_block *load_root = READ_ONCE(pinned_root);
if (!load_root) {
/* 첫 번째 로드: 이 파일시스템을 "pin" */
pinned_root = file->f_path.mnt->mnt_sb;
return 0;
}
/* 이후 모든 로드는 pinned root와 동일한 fs에서만 허용 */
if (file->f_path.mnt->mnt_sb != load_root) {
pr_info("LoadPin: denied loading from different fs\n");
return -EPERM;
}
return 0;
}
/* dm-verity + LoadPin = 검증된 파티션에서만 커널 모듈 로드 */
# ChromeOS dm-verity + LoadPin 부팅 체인
# 1. 부트로더가 커널 서명 검증
# 2. 커널이 rootfs에 dm-verity 활성화
# 3. LoadPin이 첫 번째 모듈 로드 경로를 기록
# 4. 이후 모든 모듈/펌웨어는 동일 경로에서만 로드 허용
# Android: dm-verity + dm-user (snapuserd)
# snapuserd는 Virtual A/B OTA 업데이트에서
# copy-on-write 스냅샷을 사용자 공간에서 처리
$ ps aux | grep snapuserd
root 1234 0.5 0.2 snapuserd /dev/dm-user/system_cow
dm-user와 snapuserd를
사용하여 OTA 업데이트 중 copy-on-write 스냅샷을 관리합니다.
dm-verity 위에 dm-snapshot(또는 dm-user 기반 COW)을 쌓아
업데이트 중에도 시스템 무결성을 유지합니다.
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-verity와 dm-crypt는 각각 해시 계산과 암호화 연산이 병목(Bottleneck)이 되므로, 하드웨어 가속과 병렬화가 핵심입니다.
# dm-verity prefetch 및 캐시 최적화
# check_at_most_once: 각 블록을 최대 1회만 검증 (메모리 사용 증가)
$ echo "0 2097152 verity 1 /dev/sda1 /dev/sda2 4096 4096 \
262144 1 sha256 ROOT_HASH SALT \
2 check_at_most_once use_fec_from_device /dev/sda3" | \
dmsetup create verified_opt
# dm-crypt I/O 스케줄러 최적화
$ echo none > /sys/block/dm-0/queue/scheduler
$ echo 256 > /sys/block/dm-0/queue/read_ahead_kb
# dm-integrity bitmap 모드 (성능 우선)
$ integritysetup format /dev/sdb1 \
--integrity sha256 \
--integrity-bitmap-mode \
--bitmap-sectors-per-bit 65536 \
--bitmap-flush-time 5000
# 암호화 성능 벤치마크
$ 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/dm-verity/dm-integrity의 보안은 키 관리, 알고리즘 선택, 부팅 체인 무결성에 의존합니다. 커널의 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로 할당 메모리도 초기화하면
메모리 잔류 데이터를 통한 키 유출 위험을 줄일 수 있습니다.
또한 CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y로
루트 해시의 커널 키링(Keyring) 서명 검증을 활성화할 수 있습니다.
| 공격 벡터 | 위협 | 대응 방안 |
|---|---|---|
| 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-verity 루트 해시 서명 검증 */
/* CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y 필요 */
/* 서명 검증 흐름:
* 1. verity_ctr()에서 root_hash_sig_key_desc 옵션 파싱
* 2. 커널 keyring에서 서명 키 조회 (.builtin_trusted_keys)
* 3. PKCS#7 서명 검증 (verify_pkcs7_signature())
* 4. 서명이 유효하면 dm-verity 활성화 허용 */
static int verity_verify_root_hash(struct dm_verity *v,
const void *root_hash,
size_t root_hash_len,
const void *sig,
size_t sig_len)
{
struct key *key;
int r;
/* .dm_verity_keys 또는 .builtin_trusted_keys 키링 검색 */
key = keyring_search(
make_key_ref(VERIFY_USE_SECONDARY_KEYRING, 1),
&key_type_asymmetric, v->root_hash_sig_key_desc);
r = verify_pkcs7_signature(root_hash, root_hash_len,
sig, sig_len,
VERIFY_USE_SECONDARY_KEYRING,
VERIFYING_MODULE_SIGNATURE,
NULL, NULL);
return r;
}
실전 활용
dm-verity, dm-integrity, 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: 임베디드 검증 부팅 이미지 빌드
# 1. 루트 파일시스템 이미지 생성
$ mksquashfs rootfs/ rootfs.img -comp zstd
# 2. dm-verity 해시 트리 생성
$ veritysetup format rootfs.img hash.img > verity_info.txt
$ ROOT_HASH=$(grep "Root hash" verity_info.txt | awk '{print $3}')
# 3. FEC 데이터 생성
$ veritysetup format --fec-device=fec.img --fec-roots=2 \
rootfs.img hash.img
# 4. 커널 cmdline에 dm-verity 파라미터 추가
$ echo "dm=\"1 vroot none ro 1,0 $(stat -c%s rootfs.img | \
awk '{print $1/512}') verity 1 PARTUUID=xxx PARTUUID=yyy \
4096 4096 $(stat -c%s rootfs.img | awk '{print $1/4096}') \
1 sha256 $ROOT_HASH -\"" >> cmdline.txt
## 시나리오 3: 컨테이너 이미지 + dm-verity
# composefs / ostree + dm-verity
$ mkcomposefs --digest-store=objects/ rootfs/ image.cfs
# composefs는 erofs + dm-verity를 사용하여
# 컨테이너 이미지의 무결성을 검증
| 시나리오 | dm target | 키 관리 | 부팅 자동화 |
|---|---|---|---|
| 서버 FDE | dm-crypt (LUKS2) | TPM / Tang / Passphrase | Clevis + dracut |
| Android 기기 | dm-verity + dm-crypt | eFuse + KEK | AVB + init |
| ChromeOS | dm-verity + LoadPin | TPM + 서명 키 | cros_init |
| IoT 디바이스 | dm-verity | 공개키 서명 | U-Boot + FIT |
| 컨테이너 | dm-verity (composefs) | Sigstore / Cosign | containerd |
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)되어 있어 단일 손상에는
자동 복구되지만, 물리적 손상이나 실수로 인한 덮어쓰기에는 무력합니다.
## 시나리오 4: 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/...
## 시나리오 5: dm-verity + SquashFS 컨테이너 이미지
# 1. 컨테이너 rootfs를 SquashFS 이미지로 압축
$ mksquashfs container_rootfs/ image.squashfs -comp zstd -Xcompression-level 19
# 2. dm-verity 해시 트리 생성
$ veritysetup format image.squashfs hash.img --hash sha256 --data-block-size 4096
Root hash: abcdef...
# 3. 이미지 서명 (cosign 또는 GPG)
$ cosign sign --key cosign.key docker.io/myapp:latest
# 4. 런타임에 검증된 마운트
$ losetup /dev/loop0 image.squashfs
$ losetup /dev/loop1 hash.img
$ veritysetup open /dev/loop0 verified_container /dev/loop1 abcdef...
$ mount -t squashfs /dev/mapper/verified_container /mnt/container
## 시나리오 6: dm-integrity로 서버 데이터 무결성 보호
# 데이터베이스 서버: 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-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 활성화 |
| 저널 병목 (integrity) | 쓰기 대기 시간(Latency) 증가 | bitmap 모드 전환, 저널 크기 증가 |
| 해시 캐시 미스 (verity) | 해시 블록 읽기 I/O 급증 | check_at_most_once, readahead 증가 |
| 키 분실 | 볼륨 접근 불가 | 복구 키, 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
# dm-verity 에러 로그 확인
$ dmesg | grep -i verity
[ 12.345678] device-mapper: verity: sha256 using implementation "sha256-avx2"
[ 12.345679] device-mapper: verity: 262144 data blocks verified
# dm-integrity 에러 로그
$ dmesg | grep -i integrity
[ 15.678901] device-mapper: integrity: initialized, tag size 32
[ 15.678902] device-mapper: integrity: journal sectors 131072
# 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_VERITY | dm-verity 검증 | y | BLK_DEV_DM |
CONFIG_DM_VERITY_FEC | FEC 지원 | y | DM_VERITY |
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG | 루트 해시 서명 검증 | y (보안) | DM_VERITY, SYSTEM_DATA_VERIFICATION |
CONFIG_DM_INTEGRITY | dm-integrity 무결성 | y | BLK_DEV_DM, BLK_DEV_INTEGRITY |
CONFIG_DM_FLAKEY | dm-flakey (테스트) | m | BLK_DEV_DM |
CONFIG_DM_DUST | dm-dust (테스트) | m | BLK_DEV_DM |
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-verity vs fs-verity 비교
커널은 dm-verity 외에도 fs-verity(fs/verity/)를 제공합니다.
두 가지 모두 Merkle Tree 기반 무결성 검증이지만, 동작 레벨이 다릅니다.
| 속성 | dm-verity | fs-verity |
|---|---|---|
| 동작 레벨 | 블록 디바이스 | 파일시스템 (per-file) |
| 검증 범위 | 전체 파티션 | 개별 파일 |
| 설정 단위 | 파티션 | 파일 (inode) |
| 쓰기 지원 | 읽기 전용 | 읽기 전용 (immutable) |
| 커널 소스 | drivers/md/ | fs/verity/ |
| 사용 도구 | veritysetup | fsverity (util-linux) |
| 지원 FS | 무관 (블록 레벨) | ext4, f2fs, btrfs |
| 주요 용도 | Verified Boot, OS 이미지 | APK, 커널 모듈 개별 검증 |
| 해시 저장 | 별도 파티션 | 파일 메타데이터 (xattr) |
# fs-verity로 개별 파일 검증 설정
$ fsverity enable /mnt/ext4/important_binary
$ fsverity measure /mnt/ext4/important_binary
sha256:abcdef1234567890... /mnt/ext4/important_binary
# Android에서 APK 검증에 fs-verity 사용 (Android 11+)
# dm-verity는 system 파티션 전체 검증
# fs-verity는 개별 APK 파일 검증
dm-bufio 캐시 아키텍처
dm-verity와 dm-integrity 모두 해시/태그 블록 읽기에 dm_bufio 캐시를 사용합니다.
drivers/md/dm-bufio.c에 구현된 dm-bufio는 블록 단위 캐시로,
LRU 정책으로 관리되며, 메모리 압박 시 shrinker를 통해 회수됩니다.
dm-verity 성능은 해시 블록의 캐시 적중률에 크게 의존합니다.
/* drivers/md/dm-bufio.c - 캐시 클라이언트 생성 */
struct dm_bufio_client *dm_bufio_client_create(
struct block_device *bdev,
unsigned int block_size,
unsigned int reserved_buffers,
unsigned int aux_size,
void (*alloc_callback)(struct dm_buffer *),
void (*write_callback)(struct dm_buffer *),
unsigned int flags);
/* 캐시 조회/읽기 */
struct dm_buffer *dm_bufio_read(
struct dm_bufio_client *c,
sector_t block,
void **data);
/* 캐시 히트 시 즉시 반환, 미스 시 디스크에서 읽기 */
/* 비동기 prefetch (dm-verity에서 사용) */
void dm_bufio_prefetch(
struct dm_bufio_client *c,
sector_t block,
unsigned int n_blocks);
/* 메모리 압박 시 캐시 축소 (shrinker) */
static unsigned long dm_bufio_shrink_scan(
struct shrinker *shrink,
struct shrink_control *sc)
{
/* LRU 리스트에서 가장 오래된 버퍼부터 해제 */
return __scan(c, sc->nr_to_scan, sc->gfp_mask);
}
/sys/module/dm_bufio/parameters/에서 캐시 관련 파라미터를 확인하고 조정할 수 있습니다.
max_cache_size_bytes로 최대 캐시 크기를 설정하고,
current_allocated_bytes로 현재 사용량을 모니터링합니다.
dm-verity 워크로드에서 캐시 크기를 충분히 확보하면 해시 블록의 디스크 I/O를
대폭 줄일 수 있습니다.
| dm-bufio 파라미터 | 설명 | 기본값 |
|---|---|---|
max_cache_size_bytes | 최대 캐시 크기 | 가용 메모리의 25% |
max_age_seconds | LRU 최대 보유 시간 | 300초 |
retain_bytes | shrinker가 유지할 최소 크기 | 262144 (256KB) |
peak_allocated_bytes | 최대 할당 기록 (읽기 전용) | - |
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-verity 환경에서는
해시 트리 읽기와 데이터 블록 읽기가 인터리빙되는 패턴을 관찰할 수 있습니다.
| 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-verity 디바이스 추적
# -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 verity_trace
blkparse -i verity_trace -d verity_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-verity I/O 패턴 분석 예제
# A(remap) 이벤트만 필터링: DM 리매핑 추적
blkparse -i verity_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
# dm-verity 패턴: 해시 블록 읽기 + 데이터 블록 읽기 반복
# hash_dev 읽기 (상위 Merkle 노드) → data_dev 읽기 (실제 데이터)
# btt: blktrace timing 통계 분석
btt -i verity_trace.bin -l verity_latency.dat
# Q2C: 전체 레이턴시 (요청~완료)
# D2C: 디바이스 레이턴시 (디스패치~완료)
# Q2G: 큐잉 레이턴시
# iowatcher: blktrace 결과를 시각화 SVG로 변환
iowatcher -t verity_trace -o verity_io.svg
A(remap) 이벤트를 분석하면
데이터 블록 하나를 읽을 때 발생하는 해시 블록 I/O 횟수를 파악할 수 있습니다.
Merkle Tree의 깊이가 d이면, 캐시 미스 시 최대 d개의 해시 블록 읽기가 선행됩니다.
btt의 Q2C 통계로 dm-verity 오버헤드를 정량적으로 측정하세요.
FIPS 140-2/3 고려사항
FIPS(Federal Information Processing Standards) 140-2/3은 미국 정부가 요구하는 암호화 모듈 인증 표준입니다. 리눅스 커널이 FIPS 모드로 동작할 때 dm-crypt, dm-integrity, dm-verity가 받는 제약과 설정 방법을 정리합니다.
| 구분 | 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
# dm-verity: SHA-256 사용 (FIPS 승인)
sudo veritysetup format --hash sha256 /dev/sda1 /dev/sda2
# ✓ FIPS 모드에서 정상 동작
# dm-integrity + HMAC: FIPS 승인 해시 필수
sudo integritysetup format --integrity hmac-sha256 \
--integrity-key-size 32 --integrity-key-file /path/to/key \
/dev/sda3
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 코드는 하드웨어 존재 여부를 알 필요가 없습니다.
배치 검증 최적화
dm-verity의 읽기 성능은 Merkle Tree 해시 블록의 I/O 오버헤드에 크게 좌우됩니다. 커널은 해시 블록 프리페치(prefetch), dm-bufio 캐싱, readahead 조합을 통해 대용량 순차 읽기 시나리오에서 검증 오버헤드를 최소화합니다.
| 최적화 기법 | 구현 위치 | 효과 |
|---|---|---|
| 해시 블록 프리페치 | verity_prefetch_io() | 데이터 읽기 전 해시 블록을 미리 로드 |
| dm-bufio 페이지 캐시(Page Cache) | dm_bufio_read() | 해시 블록을 메모리에 캐싱하여 디스크 I/O 절약 |
| 프리페치 클러스터 | DM_VERITY_DEFAULT_PREFETCH_SIZE | 한 번에 여러 해시 블록을 묶어 읽기 |
| readahead 연동 | 블록 레이어 readahead | 순차 읽기 시 데이터+해시 블록 사전 로드 |
| FEC 인터리빙 최적화 | verity_fec_decode() | FEC 블록도 클러스터 프리페치 |
/* dm-verity-target.c: 프리페치 워커 */
static void verity_prefetch_io(struct work_struct *work)
{
struct dm_verity_prefetch_work *pw =
container_of(work, struct dm_verity_prefetch_work, work);
struct dm_verity *v = pw->v;
int i;
/* Merkle Tree 각 레벨의 해시 블록을 프리페치 */
for (i = v->levels - 1; i >= 0; i--) {
sector_t hash_block_start, hash_block_end;
/* 데이터 블록 범위 → 해시 블록 범위 변환 */
verity_hash_at_level(v, pw->block, i,
&hash_block_start, NULL);
verity_hash_at_level(v, pw->block + pw->n_blocks - 1,
i, &hash_block_end, NULL);
/* dm-bufio prefetch: 비동기 읽기 요청 */
dm_bufio_prefetch(v->bufio, hash_block_start,
hash_block_end - hash_block_start + 1);
}
kfree(pw);
}
/* dm-verity map 경로에서 프리페치 스케줄링 */
static void verity_submit_prefetch(struct dm_verity *v,
struct dm_verity_io *io)
{
struct dm_verity_prefetch_work *pw;
pw = kmalloc(sizeof(*pw), GFP_NOIO);
if (!pw)
return;
INIT_WORK(&pw->work, verity_prefetch_io);
pw->v = v;
pw->block = io->block;
pw->n_blocks = io->n_blocks;
queue_work(v->verify_wq, &pw->work);
}
# dm-bufio 캐시 상태 확인
cat /sys/module/dm_bufio/parameters/current_allocated_bytes
# 67108864 (64MB)
# 캐시 크기 조정 (해시 트리 전체를 캐싱할 만큼 확보)
# 1GB 디바이스 + SHA-256 + 4KB 블록 = 약 8MB 해시 트리
echo 134217728 | sudo tee /sys/module/dm_bufio/parameters/max_cache_size_bytes
# 128MB로 확대
# readahead 크기 조정 (DM 디바이스)
blockdev --getra /dev/dm-0
# 256 (128KB, 기본값)
sudo blockdev --setra 2048 /dev/dm-0
# 1MB로 확대 → 순차 읽기 성능 향상
# 프리페치 효과 측정: fio 순차 읽기
fio --name=seqread --filename=/dev/dm-0 --rw=read \
--bs=128k --numjobs=4 --iodepth=32 --size=1G \
--direct=1 --group_reporting
# dm-bufio 캐시 히트율 확인
cat /sys/module/dm_bufio/parameters/current_allocated_bytes
# peak vs current로 캐시 워킹셋 파악
cat /sys/module/dm_bufio/parameters/peak_allocated_bytes
max_cache_size_bytes를 해시 트리 전체 크기 이상으로
설정하면 steady-state에서 해시 블록의 디스크 I/O가 0에 수렴합니다.
Android에서는 부팅 초기에 전체 해시 트리를 프리로드하는 최적화를 적용하여
verity_prefetch_io 워커가 부팅 직후 해시 트리를 워밍업합니다.
관련 문서
- 블록 I/O — 블록 레이어 아키텍처, bio 구조, I/O 경로
- Device Mapper / LVM — DM 프레임워크 기본, dm-linear, dm-thin, LVM2
- 파일시스템 개요 — 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/(verity, crypt, integrity) - cryptsetup/LUKS2: gitlab.com/cryptsetup
- Android Verified Boot: AVB 소스
- dm-verity 커널 소스:
drivers/md/dm-verity-target.c,dm-verity-fec.c - dm-integrity 커널 소스:
drivers/md/dm-integrity.c - dm-crypt 커널 소스:
drivers/md/dm-crypt.c - 커널 문서 — dm-verity: kernel.org dm-verity 문서
- 커널 문서 — dm-crypt: kernel.org dm-crypt 문서
- 커널 문서 — dm-integrity: kernel.org dm-integrity 문서
- dm-verity 커널 소스 (Bootlin): dm-verity-target.c
- dm-integrity 커널 소스 (Bootlin): dm-integrity.c
- dm-crypt 커널 소스 (Bootlin): dm-crypt.c
- LWN.net — dm-verity 소개: Verified boot with dm-verity
- LWN.net — dm-integrity 소개: dm-integrity: a new device-mapper target
- LUKS2 온디스크 포맷 명세: LUKS2 On-Disk Format Specification
- cryptsetup 매뉴얼 페이지: man cryptsetup(8)