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)을 심층 분석합니다.

관련 Device Mapper 문서:
전제 조건: 블록 I/O 문서와 Device Mapper / LVM 문서를 먼저 읽으세요. 블록 디바이스의 기본 I/O 경로와 Device Mapper의 target 개념을 이해해야 합니다.
일상 비유: dm-crypt는 금고 자체의 잠금(Lock) 장치와 비슷합니다. 디스크에 기록되는 모든 데이터를 투명하게 암호화하여, 물리적으로 디스크를 탈취하더라도 암호키 없이는 내용물을 읽을 수 없도록 보호합니다.

핵심 요약

  • 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)

단계별 이해

  1. Device Mapper 레이어 이해
    커널의 블록 I/O 스택에서 DM은 bio를 가로채 target 드라이버에 전달합니다.
  2. dm-crypt로 암호화
    섹터 단위로 AES-XTS 암호화를 적용하여 디스크 데이터를 보호합니다.
  3. LUKS 키 관리
    LUKS2는 Argon2id KDF, 다중 키 슬롯, TPM/FIDO2 토큰을 지원합니다.
  4. 성능 최적화
    워크큐 바이패스, 하드웨어 가속, 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 디바이스를 관리합니다.

사용자 공간: dmsetup / cryptsetup / veritysetup / integritysetup ioctl(DM_TABLE_LOAD) DM Core (dm.c) mapped_device / dm_table / dm_target dm-verity Merkle Tree 검증 dm-integrity 저널링 무결성 dm-crypt 블록 암호화 블록 레이어 (bio / request) 물리 블록 디바이스 (HDD / SSD / NVMe)

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_tabletarget 매핑 테이블drivers/md/dm-table.c
dm_target개별 매핑 세그먼트drivers/md/dm.h
dm_ioI/O 요청 추적drivers/md/dm.c
dm_ioctl사용자 공간 인터페이스drivers/md/dm-ioctl.c
dm_bufio해시/메타데이터 블록 캐시(Cache)drivers/md/dm-bufio.c
DM 테이블 형식: dmsetup으로 전달하는 테이블은 시작섹터 길이 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 스택 쌓기: DM 디바이스 위에 다른 DM 디바이스를 쌓을 수 있습니다. 예: 물리 디바이스 -> 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) 지원 등이 추가되었습니다.

속성LUKS1LUKS2
메타데이터 형식바이너리 헤더JSON + 바이너리
키 슬롯최대 8개최대 32개
PBKDFPBKDF2Argon2id (기본), 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
    }
  }
}
*/
Argon2id vs PBKDF2: Argon2id는 메모리-하드 함수로, GPU/ASIC 기반 브루트포스 공격에 강합니다. LUKS2 기본 설정은 Argon2id (time=4, memory=1GB, cpus=4)이며, 약 2초의 키 유도 시간이 소요됩니다. 저사양 기기에서는 --pbkdf-memory 옵션으로 메모리를 줄일 수 있습니다.
WRITE 경로 평문 bio crypt_convert() AES-XTS 암호화 암호문 디스크 READ 경로 암호문 디스크 AES-XTS 복호화 crypt_convert() 평문 bio IV (Initialization Vector) 생성 plain64: 섹터 번호를 IV로 사용 ESSIV: 암호화된 섹터 번호 (Watermark 공격 방어)
/* 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-XTSaes-xts-plain64256/512비트표준, 하드웨어 가속일반 디스크 암호화
AES-CBC-ESSIVaes-cbc-essiv:sha256128/256비트레거시, Watermark 방어호환성 필요 시
Adiantumxchacha12,aes-adiantum-plain64256비트AES-NI 없는 환경 최적저사양 ARM 디바이스
AES-GCMaes-gcm-random256비트AEAD (인증 암호화)dm-integrity 결합
Twofish-XTStwofish-xts-plain64256/512비트AES 대안AES 회피 정책
Serpent-XTSserpent-xts-plain64256/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에 추가 */
Adiantum 선택 기준: AES 하드웨어 가속(AES-NI, ARMv8 CE)이 있으면 AES-XTS가 최선입니다. 가속이 없는 저사양 ARM 디바이스(ARMv7 등)에서는 Adiantum이 소프트웨어 AES-XTS보다 약 5배 빠르며, 보안 수준은 동등합니다.

IV 생성 모드 상세 비교

dm-crypt의 IV(Initialization Vector) 생성 모드는 동일한 평문 블록이 동일한 암호문으로 변환되는 것을 방지합니다. 각 섹터마다 고유한 IV를 생성하여 ECB 모드의 취약점(Vulnerability)을 회피합니다. 주요 IV 생성 모드를 상세히 비교합니다.

IV 모드IV 생성 방법보안 수준성능호환성
plain섹터 번호 (32비트)낮음 (2TB 이상 반복)최고레거시
plain64섹터 번호 (64비트)양호최고권장
plain64be섹터 번호 (64비트 빅 엔디안(Endianness))양호최고특수 HW
essivE(key_hash, 섹터번호)우수 (Watermark 방어)약간 느림LUKS1 기본
benbi섹터 번호 빅 엔디안 (128비트)양호최고하드웨어 AES
eboiv암호화된 블록의 IV우수약간 느림Bitlocker 호환
elephantElephant 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 값 적용 */
XTS vs CBC+ESSIV: XTS 모드는 자체적으로 섹터별 tweak를 생성하므로 별도 IV 모드가 필요 없습니다. CBC+ESSIV는 레거시 호환용이며, 새 설정에서는 항상 AES-XTS를 사용하세요. XTS는 narrow-block tweakable cipher로, 16바이트 블록 단위의 독립적 암호화를 보장합니다.

인라인 암호화 엔진 (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 플래그가 추가되어 성능 최적화가 가능합니다.

WRITE bio crypt_map() dmcrypt_write io_queue (workqueue) crypt_queue (encrypt) no_write_workqueue: 직접 암호화 (컨텍스트 스위칭 회피) 성능 최적화 플래그 no_read_workqueue : 읽기 복호화를 bio 완료 컨텍스트에서 직접 수행 no_write_workqueue: 쓰기 암호화를 submit 컨텍스트에서 직접 수행 submit_from_crypt_cpus: 암호화 완료 CPU에서 직접 bio submit (NUMA 친화) same_cpu_crypt : 암호화/복호화를 동일 CPU에서 수행 (캐시 최적화)
# 성능 최적화된 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/s2,800 MB/s3,200 MB/s~8%
순차 쓰기3,000 MB/s2,200 MB/s2,700 MB/s~10%
랜덤 4K 읽기800K IOPS600K IOPS720K IOPS~10%
랜덤 4K 쓰기500K IOPS350K IOPS430K IOPS~14%
AES-NI 가속 효과: AES-NI가 활성화된 x86_64 CPU에서 AES-XTS의 단일 코어 처리량은 약 4-5 GB/s입니다. ARMv8 Crypto Extension도 유사한 성능을 제공합니다. 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;
}
CPU별 tfm 할당: dm-crypt는 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()에서 암호화된 후 하위 디바이스로 전달되는 복잡한 과정을 거칩니다. 이 경로를 상세히 이해하면 성능 병목 지점을 정확히 파악하고 최적화할 수 있습니다.

dm-crypt Write Path 상세 write() 시스템 콜 VFS → 파일시스템 → submit_bio() dm_submit_bio() dm_table_find_target() → crypt target crypt_map(ti, bio) dm_crypt_io 할당 → crypt_write(io) 기본 경로 no_write_workqueue kcryptd_io 워크큐에 작업 큐잉 bounce 페이지 할당 (page_pool) crypt_convert() — 섹터별 암호화 루프 IV 생성 → skcipher_request_set_crypt() → crypto_skcipher_encrypt() submit 컨텍스트에서 직접 암호화 submit_bio_noacct() → 하위 블록 디바이스

쓰기 경로의 핵심 단계를 코드 수준에서 분석합니다. 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);
mempool 전략: dm-crypt는 I/O 경로에서 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);
인라인 vs 소프트웨어 암호화 경로: 인라인 암호화가 가능하면 dm-crypt는 bounce 페이지 할당, 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바이트 블록에 고유한 변환을 적용합니다.

AES-XTS 암호화 모드 (IEEE P1619) Key1 (AES-256 데이터 키) Key2 (AES-256 Tweak 키) T = AES_Key2(sector_number) 섹터별 기본 Tweak 값 생성 512바이트 섹터 = 32개 x 16바이트 블록 Block 0 암호화 (j=0) P[0] (16B) XOR T * a^0 AES_Key1( ) XOR T * a^0 C[0] (암호문 16B) C = (AES_K1(P XOR T)) XOR T Block 1 (j=1) P[1] XOR T*a^1 AES_Key1( ) XOR T*a^1 C[1] T*a^j: GF(2^128) 곱셈 ... Block 31 (j=31) P[31] XOR T*a^31 AES_Key1( ) XOR T*a^31 C[31] T = AES_K2(sector) , C[j] = AES_K1(P[j] XOR T*a^j) XOR T*a^j (a = GF(2^128)의 원시원소)
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 블록까지 안전
XTS vs CBC+ESSIV 보안 비교: XTS는 narrow-block tweakable cipher로, 동일 섹터 내에서도 각 16바이트 블록마다 고유한 tweak가 적용됩니다. CBC 모드에서는 한 블록의 변경이 이후 블록에 전파(Propagation)되지만, XTS에서는 각 블록이 독립적이므로 비트 플립(Bit Flip) 공격에 대한 영향이 해당 블록에 국한됩니다. 반면 CBC는 블록 간 체이닝으로 오류 전파가 발생하여 탐지에 유리한 측면도 있습니다.

LUKS2 헤더 구조 상세

LUKS2 헤더는 바이너리 헤더(Binary Header), JSON 메타데이터 영역, 키슬롯 바이너리 영역으로 구성됩니다. JSON 메타데이터에는 keyslots, segments, digests, tokens, config 객체가 포함되며, 이들 간의 관계를 통해 유연한 키 관리와 데이터 매핑이 가능합니다.

LUKS2 온디스크 레이아웃 디스크 물리 레이아웃 Primary Binary Header (4KB) Primary JSON Area keyslots, segments, digests, tokens, config Secondary Binary Header (백업) Secondary JSON Area (백업) Keyslots Binary Area AF-split된 암호화 키 데이터 암호화된 데이터 영역 segments[0].offset부터 시작 JSON 메타데이터 관계도 tokens FIDO2, TPM2, Clevis keyslots KDF: argon2id/pbkdf2 AF: luks1 (stripes: 4000) unlock digests PBKDF2 (master key 검증) verify segments type: crypt cipher, sector_size, offset key → segment config dm-crypt 활성화 (Volume Master Key) LUKS2 잠금 해제 순서 1. token으로 인증 (TPM2/FIDO2) 또는 비밀번호 입력 2. keyslot의 KDF로 키 유도 (Argon2id) 3. AF-split 역변환으로 Volume Master Key 복원 4. digest로 복원된 키 검증 (PBKDF2 해시 비교) 5. segment 정보로 dm-crypt 매핑 테이블 구성 6. Volume Master Key를 dm-crypt에 전달
/* 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
    }
  }
}
JSON 메타데이터 관계 정리: tokenskeyslots를 참조하여 어떤 키 슬롯을 잠금 해제할지 지정합니다. digestskeyslotssegments 모두를 참조하여 특정 키 슬롯에서 복원된 Volume Master Key가 특정 데이터 세그먼트에 유효한지 검증합니다. 이 3자 관계 덕분에 다중 키, 다중 세그먼트, 다중 인증 방식을 유연하게 조합할 수 있습니다.

dm-crypt 워크큐 아키텍처

dm-crypt는 암호화/복호화 작업을 처리하기 위해 두 개의 전용 워크큐(Workqueue)를 사용합니다: kcryptd_io(I/O 제출용)와 kcryptd(암호화/복호화 연산용). 이 분리 설계는 암호화 연산의 블로킹이 I/O 제출 경로를 차단하지 않도록 보장합니다.

dm-crypt 워크큐 아키텍처 WRITE 경로 submit 컨텍스트 (사용자 스레드) crypt_map() → crypt_write() 기본 no_write_wq kcryptd crypt_queue (WQ_HIGHPRI) bounce 할당 + 암호화 직접 암호화 submit 컨텍스트에서 워크큐 바이패스 kcryptd_io io_queue: 암호문 bio 제출 submit_bio_noacct() → 디스크 READ 경로 submit 컨텍스트 (사용자 스레드) crypt_map() → crypt_read() 하위 디바이스에서 암호문 읽기 bio 완료 콜백 crypt_endio() (bio 완료) 기본 no_read_wq kcryptd crypt_queue: 복호화 처리 직접 복호화 endio 컨텍스트에서 평문 bio 반환 → 사용자 워크큐 최적화 효과 기본 경로: submit → kcryptd(암호화) → kcryptd_io(제출) = 2회 컨텍스트 스위칭, 캐시 미스 발생 최적화 경로: submit 컨텍스트에서 직접 암호화+제출 = 0회 컨텍스트 스위칭, NVMe에서 15-20% 성능 향상
/* 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_queuekcryptdAES-XTS 암호화/복호화 연산 수행WQ_HIGHPRI | WQ_CPU_INTENSIVE
io_queuekcryptd_io암호문 bio를 하위 디바이스에 제출WQ_HIGHPRI | WQ_MEM_RECLAIM
(바이패스)(submit 스레드)no_write_workqueue 시 직접 처리-
(바이패스)(endio 콜백)no_read_workqueue 시 직접 복호화-
no_read/write_workqueue 주의사항: 워크큐 바이패스는 NVMe SSD처럼 디바이스 자체가 빠른 경우에 효과적입니다. 느린 HDD에서는 암호화 연산이 I/O 컨텍스트를 오래 점유하여 오히려 응답 시간이 증가할 수 있습니다. 또한 no_read_workqueue는 softirq 컨텍스트에서 암호화를 수행할 수 있으므로, 다른 softirq 처리에 영향을 줄 수 있습니다. 실제 워크로드에서 벤치마크 후 적용하세요.

dm-crypt + dm-integrity 결합

dm-crypt와 dm-integrity를 결합하면 인증 암호화(AEAD)를 구현할 수 있습니다. AES-GCM 또는 ChaCha20-Poly1305 같은 AEAD 알고리즘을 사용하여 암호화와 무결성 검증을 단일 연산으로 수행합니다. 이 결합 모드에서 dm-crypt는 암호화 담당, dm-integrity는 인증 태그 저장 담당입니다.

평문 데이터 dm-crypt (AEAD) AES-GCM: 암호화 + 인증 태그 생성 암호문 (ciphertext) 인증 태그 (16B) dm-integrity 데이터 + 태그를 디스크 기록 AEAD 모드 설정 명령어 cryptsetup luksFormat --type luks2 --cipher aes-gcm-random --integrity aead --sector-size 4096 --key-size 256 /dev/sdb 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-GCMGCM 내장16B~15%
AES-XTS + HMACAES-XTSHMAC-SHA25632B~25%
ChaCha20-Poly1305ChaCha20Poly130516B~12%
AES-XTS 단독AES-XTS없음0B~8%
AEAD 모드 주의사항: AEAD 모드에서는 dm-integrity가 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_iobio 완료 시DM_ENDIO_*선택
statusdmsetup status/table없음선택
messagedmsetup message0 또는 오류선택
iterate_devices하위 디바이스 탐색0 또는 오류선택
io_hintsI/O 힌트 설정없음선택
map() 반환값: 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 최적화 1. AES-NI / ARMv8 CE 가속 2. no_read/write_workqueue 3. submit_from_crypt_cpus 4. 대용량 bio 처리 (max_write_same) 5. NUMA 친화 메모리 할당 공통 최적화 1. I/O 스케줄러 선택 (none/mq-deadline) 2. readahead 크기 조정 3. 블록 크기 4096 (4K 정렬) 4. NVMe 멀티큐 활용 5. DM 스택 깊이 최소화 AEAD + dm-integrity 최적화 1. Bitmap 모드 (저널 대비 2-3x 빠름) 2. 별도 메타 디바이스 분리 3. journal-watermark 최적화 4. interleave-sectors 조정 5. buffer_sectors 튜닝 벤치마크 및 모니터링 1. cryptsetup benchmark (알고리즘 성능) 2. fio (I/O 성능 측정) 3. perf top (kcryptd 프로파일링) 4. blktrace (I/O 지연 분석) 5. /proc/crypto (가속 드라이버 확인)
# 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
NVMe + dm-crypt 최적 설정: 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 사용자 비밀번호 Argon2id PBKDF FIDO2 / YubiKey HMAC-Secret 커널 Keyring user/logon 키 LUKS2 Key Slots 최대 32개 키 슬롯 Volume Master Key dm-crypt (crypto_skcipher)
# 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 KeyPCR 값에 바인딩된 암호키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 부팅 파라미터
Cold Boot 공격 방어: dm-crypt 키는 커널 메모리에 평문으로 존재합니다. 물리적 접근이 가능한 공격자는 냉동 메모리 공격(Cold Boot Attack)으로 키를 추출할 수 있습니다. 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 복구 키)

luksSuspendluksResume을 활용한 키 보호 전략도 중요합니다. 시스템이 절전 모드(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 BootRAM에서 키 추출luksSuspend, INIT_ON_FREE
Evil Maid부트로더/initramfs 변조Secure Boot + TPM PCR
DMA 공격Thunderbolt/PCIe DMA로 메모리 접근IOMMU, Thunderbolt 보안 레벨
브루트포스비밀번호 반복 시도Argon2id (메모리-하드 KDF)
키 로깅비밀번호 입력 가로채기FIDO2/TPM (비밀번호 불필요)
Wear LevelingSSD에서 삭제된 키 복구SED (Self-Encrypting Drive)
Side Channel타이밍/전력 분석으로 키 추론constant-time crypto, AES-NI

실전 활용

dm-crypt는 서버 디스크 암호화, 임베디드 기기 데이터 보호, 컨테이너 빌드 아티팩트 보호, CI/CD 파이프라인(Pipeline) 등 다양한 실전 시나리오에서 활용됩니다.

서버 디스크 암호화 LUKS2 + AES-XTS-256 TPM sealed key + Clevis + AEAD (선택) 무결성 자동 잠금 해제 (Clevis/Tang) 임베디드 데이터 보호 dm-crypt (userdata) ICE 인라인 암호화 Adiantum (저사양 ARM) eFuse + KEK 키 보호 K8s / CI/CD CSI + LUKS2 PVC 암호화 dm-crypt 빌드 아티팩트 네트워크 잠금 해제 (Tang) 시크릿 볼륨 보호 Device Mapper Framework (dm.c) 블록 디바이스 (NVMe / eMMC / SATA)
## 시나리오 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키 관리부팅 자동화
서버 FDEdm-crypt (LUKS2)TPM / Tang / PassphraseClevis + dracut
Android 기기dm-crypt / ICEeFuse + KEKinit
IoT 디바이스dm-crypt (Adiantum)eFuse / 키 서버initramfs
K8s PVCdm-crypt (CSI 드라이버)시크릿 / KMSCSI NodeStageVolume
systemd-cryptenroll: 최신 systemd(v248+)는 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
LUKS 헤더 백업 필수: LUKS 헤더가 손상되면 전체 볼륨의 데이터를 잃게 됩니다. 반드시 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/...
서버 FDE 스택 ext4 / XFS dm-crypt (LUKS2) dm-integrity (AEAD) NVMe SSD Android 스택 ext4 (system) / f2fs (data) dm-crypt / fscrypt ICE (Inline Crypto) UFS / eMMC K8s PVC 스택 ext4 / XFS (Pod) dm-crypt (LUKS2) CSI Block Device 커널 설정 (Kconfig) CONFIG_BLK_DEV_DM=y # Device Mapper 코어 CONFIG_DM_CRYPT=y # dm-crypt 암호화 CONFIG_DM_INTEGRITY=y # dm-integrity (AEAD 결합) CONFIG_CRYPTO_AES_NI_INTEL=y # AES-NI 가속 (x86) CONFIG_CRYPTO_ADIANTUM=y # Adiantum (저사양 ARM)

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_DMDevice Mapper 코어yBLK_DEV
CONFIG_DM_CRYPTdm-crypt 암호화yBLK_DEV_DM, CRYPTO
CONFIG_DM_INTEGRITYdm-integrity 무결성 (AEAD 결합)yBLK_DEV_DM, BLK_DEV_INTEGRITY
CONFIG_CRYPTO_AES_NI_INTELAES-NI 가속y (x86)CRYPTO_AES, X86
CONFIG_CRYPTO_XTSXTS 모드yCRYPTO
CONFIG_CRYPTO_ADIANTUMAdiantum 알고리즘y (ARM)CRYPTO
CONFIG_CRYPTO_USER_API_AEAD사용자 AEAD APIyCRYPTO_AEAD

DM Target 개발 완전 예제

Device Mapper target을 개발하려면 struct target_type의 콜백 함수를 구현하고 커널 모듈로 등록합니다. 이 섹션에서는 dm-linear를 클론한 완전한 모듈 예제를 통해 ctr(생성자), dtr(소멸자), map(I/O 매핑), status(상태 보고), message(런타임 명령) 콜백의 실제 구현 패턴을 상세히 살펴봅니다.

콜백 함수호출 시점역할
ctrdmsetup create / DM_TABLE_LOAD인자 파싱, 하위 디바이스 획득, private 데이터 초기화
dtr테이블 교체 / dmsetup remove하위 디바이스 해제, 메모리 정리
mapbio 도착 시 (I/O 경로)bio를 하위 디바이스로 remapping (bi_bdev, bi_sector 재설정)
statusdmsetup status / dmsetup table런타임 정보 또는 테이블 매개변수 출력
messagedmsetup message런타임 파라미터 변경 (통계 리셋 등)
end_io하위 디바이스 I/O 완료 시완료 후처리, 오류 핸들링, 통계 갱신
iterate_devicesDM 코어 내부하위 디바이스 순회 (디스카드, 토폴로지(Topology) 전파)
VFS / 파일시스템 bio 생성 DM Core dm_submit_bio() target lookup target.map(ti, bio) bio_set_dev() + bi_sector 재계산 DM_MAPIO_REMAPPED submit_bio_noacct(bio) 하위 블록 디바이스 (/dev/sda, /dev/nvme0n1) DM_MAPIO_SUBMITTED (target이 직접 제출) DM_MAPIO_KILL → bio_io_error()
map() 반환값: 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 targetsmylinear가 나타납니다.
# 모듈 로드 및 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
주의: out-of-tree DM target은 커널 버전 업그레이드 시 API 변경으로 빌드가 깨질 수 있습니다. 프로덕션에서는 in-tree 통합을 권장하며, drivers/md/KconfigMakefile에 항목을 추가하여 커널 빌드 시스템(Build System)에 포함시키는 것이 바람직합니다.

blktrace/btrace 분석

blktrace는 블록 레이어의 I/O 이벤트를 실시간으로 추적하는 도구로, DM 디바이스의 I/O 패턴을 분석하는 데 필수적입니다. dm-crypt 환경에서는 암호화/복호화에 의한 I/O 지연과 워크큐 컨텍스트 스위칭 오버헤드를 정량적으로 측정할 수 있습니다.

Action 코드의미설명
QQueuedbio가 request queue에 진입
GGet requestrequest 구조체(Struct) 할당
IInsertedI/O 스케줄러에 삽입
DIssued드라이버에 디스패치(Dispatch)
CCompletedI/O 완료
MMerged기존 request에 병합
ARemapDM/MD에 의한 remap (DM 분석의 핵심)
XSplitbio가 분할됨
# 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
DM remap 추적 팁: dm-crypt 디바이스에서 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-3MD5, RIPEMD, Whirlpool
MACHMAC-SHA-256, HMAC-SHA-512, CMAC-AESHMAC-MD5
KDFPBKDF2-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
FIPS 부팅 시간 영향: FIPS 모드 활성화 시 커널은 부팅 과정(Boot Process)에서 모든 암호 알고리즘의 self-test를 수행합니다. 이로 인해 부팅 시간이 2-5초 증가할 수 있으며, 임베디드 시스템에서는 이 오버헤드가 중요할 수 있습니다. self-test 실패 시 해당 알고리즘은 사용 불가능해지며, dm-crypt 활성화가 실패합니다.

LUKS1 vs LUKS2 비교

LUKS(Linux Unified Key Setup)는 dm-crypt 위에서 키 관리, 헤더 포맷, 다중 패스프레이즈를 표준화한 디스크 암호화 포맷입니다. LUKS2는 JSON 메타데이터, Argon2id KDF, 토큰/세그먼트 추상화, dm-integrity 연동 등 대폭 개선된 2세대 포맷입니다.

LUKS1 (고정 바이너리 구조) LUKS2 (JSON 메타데이터) LUKS1 phdr (592 bytes) magic | version | cipher | hash | mk-digest | salt Key Slots (최대 8개, 고정 크기) KS0 KS1 KS2 ... KS7 Key Material Area (AF split) 암호화된 데이터 영역 payload_offset 이후 Binary Header (4KB) + Primary JSON Area Secondary JSON Area (이중화 복구) Keyslots (가변 크기, 무제한) Argon2id | PBKDF2 | 하드웨어 토큰 Segments: 데이터 영역 매핑 정의 Tokens (FIDO2/TPM2) dm-integrity 연동 암호화된 데이터 영역 Segments로 유연한 매핑 PBKDF2 전용 온라인 리키잉 불가 Argon2id 기본 (GPU 저항) 온라인 리키잉 + AEAD 지원
기능LUKS1LUKS2
헤더 포맷고정 바이너리 (592 bytes phdr)JSON 메타데이터 (이중화)
최대 키 슬롯8개 (고정 크기)사실상 무제한 (가변 크기)
KDFPBKDF2 전용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 매핑)
configJSON 영역 크기, 키슬롯 영역 크기 설정
# 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
LUKS2 권장: 신규 설치에서는 항상 LUKS2를 사용하세요. Argon2id KDF는 GPU/ASIC 브루트포스 공격에 대한 저항성이 PBKDF2보다 훨씬 우수하며, dm-integrity 연동으로 암호화된 데이터의 무결성까지 보장할 수 있습니다.
LUKS1 → LUKS2 변환 주의: 변환 전 반드시 cryptsetup luksHeaderBackup으로 헤더를 백업하세요. GRUB2의 LUKS2 지원은 제한적이므로 (Argon2id 미지원 등) 부트 파티션 암호화에는 LUKS1 또는 PBKDF2 기반 LUKS2를 사용해야 할 수 있습니다.

하드웨어 가속

최신 스토리지 컨트롤러와 프로세서는 인라인 암호화 엔진을 내장하여 dm-crypt의 소프트웨어 암호화 오버헤드를 제거합니다. 커널 5.9+의 blk-crypto 프레임워크는 하드웨어/소프트웨어 폴백을 투명하게 통합합니다.

파일시스템 (ext4/f2fs) fscrypt (파일 단위 암호화) per-file key → crypto_context bio + bio_crypt_ctx key, DUN (Data Unit Number), algorithm blk-crypto 프레임워크 blk_crypto_profile → HW 지원 여부 판단 HW 지원 HW 미지원 HW Inline Crypto Engine Qualcomm ICE / Samsung FBE / UFS SW Fallback (blk-crypto-fallback) 커널 crypto API (AES-NI 가속)
하드웨어 가속벤더방식성능 특성
Inline Crypto Engine (ICE)QualcommUFS/eMMC 컨트롤러 내장 AES 엔진CPU 사용률 0%, 라인 속도 암호화
FBE HWSamsungeMMC/UFS 인라인 암호화CPU 사용률 0%, Knox 보안 연동
AES-NIIntel/AMDCPU 명령어 셋 (소프트웨어 가속)CPU 사용, 3-5 GB/s (코어당)
ARMv8 CEARMCrypto Extensions 명령어CPU 사용, 1-3 GB/s (코어당)
CCPAMDCryptographic Co-Processor오프로드, DMA 기반
QATIntelQuickAssist 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 아키텍처: 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=autoPCR 봉인 키 (Sealed Key)하드웨어 기반, Secure Boot 연동
FIDO2--fido2-device=autoHMAC-Secret Extension물리 토큰, 피싱 방어
PKCS#11--pkcs11-token-uri=...스마트카드 / HSM엔터프라이즈 PKI
복구 키--recovery-key256비트 랜덤 키 (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 + FIDO2 다중 인증: 하나의 LUKS2 볼륨에 여러 토큰을 등록하면 OR 조건으로 잠금 해제됩니다. 즉, TPM2 또는 FIDO2 또는 비밀번호 중 하나만 성공하면 됩니다. AND 조건(TPM2 + 비밀번호 동시 필요)을 구현하려면 --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 주의사항: 루트(Root) 파일시스템이 LUKS 볼륨 위에 있을 때 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 + TPM2TPM + 네트워크 이중 조건하드웨어 + 네트워크고보안 환경
Clevis SSSt-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"}
    }}'
Clevis/Tang 보안 모델: Tang은 ECMR(Elliptic Curve Montgomery Recovery) 기반 키 교환을 사용합니다. 클라이언트는 Tang 서버의 공개키와 자신의 임시 키로 LUKS 키를 복원합니다. Tang 서버는 상태가 없으며(Stateless), 어떤 클라이언트가 접근했는지 기록하지 않습니다. 따라서 서버를 물리적으로 탈취해도 개별 클라이언트의 데이터를 복호화할 수 없습니다.

전체 디스크 암호화 성능 벤치마크

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/s2,800 MB/s3,200 MB/s~8%
순차 쓰기 (1M bs)3,000 MB/s2,200 MB/s2,700 MB/s~10%
랜덤 4K 읽기 (QD64)800K IOPS600K IOPS720K IOPS~10%
랜덤 4K 쓰기 (QD64)500K IOPS350K IOPS430K IOPS~14%
혼합 70/30 (QD32)450K IOPS300K IOPS380K 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-randomAES-CTRGHASH16B12B (랜덤)AES-NI로 가속
chacha20-poly1305ChaCha20Poly130516B12B (랜덤)AES-NI 없는 환경에 적합
aes-ccm-randomAES-CTRCBC-MAC16B11BGCM보다 약간 느림
aegis128-randomAES 기반내장16B16BAES-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
AEAD 디스크 공간 오버헤드: AEAD 모드에서 dm-integrity는 각 섹터마다 인증 태그(16바이트)를 저장합니다. 4096바이트 섹터 기준으로 약 0.4%의 추가 디스크 공간이 필요합니다. 512바이트 섹터에서는 약 3.1%로 증가합니다. 또한 dm-integrity의 저널 또는 비트맵 영역도 별도 공간을 차지합니다.

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 IOPS55K IOPS단일 스레드, 동기
libaio600K IOPS680K IOPS비동기, 시스템 콜 기반
io_uring650K IOPS720K IOPS비동기, 링 버퍼 기반
io_uring + IOPOLL680K IOPS760K IOPS커널 폴링, 최저 레이턴시
io_uring + dm-crypt 최적 조합: 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 스택 구조: 사용자 공간 → VFS → 스왑 서브시스템 → dm-crypt(암호화) → ZRAM(압축) → RAM. 쓰기 시 데이터가 먼저 AES-XTS로 암호화되고, 이후 LZ4/ZSTD로 압축됩니다. 암호화된 데이터는 랜덤 특성상 압축률이 낮아질 수 있으므로, 보안보다 메모리 효율이 중요하면 ZRAM만 단독 사용하는 것이 유리합니다. 반대 구성(ZRAM → dm-crypt: 먼저 압축 후 암호화)은 dm-crypt가 블록 디바이스 위에서만 동작하므로 일반적으로 구현이 어렵습니다.
# 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
내부 문서:
외부 참고 자료: