dm-integrity 가 결합된 dm-crypt 파티션에서 데이터 복구하기

증상

  • 블록 디바이스 /dev/nvme0n1p4 에 리눅스 dm-crypt 를 Authenticated disk encryption 를 포함하여 활성화한 후, 그 위에 XFS 파일시스템을 포맷하여 사용하였는데,
    • cryptsetup luksFormat 을 이용하여 LUKS 파티션을 만들 때 --integirty 옵션을 주면 됩니다.
    • 아울러 성능 문제로 cryptsetup luksOpen 명령어에 --integrity-no-journal 옵션을 켜 둔 상태였습니다.
  • 갑작스러운 전원 문제로 시스템이 다운된 후, 재부팅하였을 때 아래 커널 메시지를 내며 XFS 파일시스템이 마운트되지 않는 상황입니다.
[52276.211696] XFS: attr2 mount option is deprecated.
[52276.226177] XFS (dm-4): Mounting V5 filesystem in no-recovery mode. Filesystem will be inconsistent.
[52276.236100] trusted_key: device-mapper: crypt: dm-3: INTEGRITY AEAD ERROR, sector 2
[52276.243831] XFS (dm-4): metadata I/O error in "xfs_read_agi+0xa0/0x144" at daddr 0x2 len 1 error 84
[52276.252987] XFS (dm-4): xfs_imap_lookup: xfs_ialloc_read_agi() returned error -84, agno 0
[52276.261234] XFS (dm-4): Failed to read root inode 0x80, error 84

이는 dm-integrity 저널이 꺼진 상태에서 시스템이 예상치 않게 종료되어, integrity tag가 데이터와 맞지 않게 되어 생긴 문제 입니다.

이러한 상황에서 dm-integrity를 걷어내고 파일시스템을 마운트하는 방법을 보겠습니다.

Cipher와 키의 길이를 확인하기

cryptsetup luksDump /dev/mapper/nvme0n1p4 명령어을 이용하여 암호화 키의 길이를 확인합니다. 그 결과의 일부만 보면,

Keyslots:
  0: luks2
	Key:        768 bits
	Priority:   normal
	Cipher:     aes-xts-plain64
	Cipher key: 512 bits

Cipher는 aes-xts-plain64 이며 Cipher key 길이는 512비트 (64바이트) 입니다. 전체 키의 길이는 768비트 (96바이트) 인데, 이 중 앞의 64 바이트는 Cipher key이고 나머지는 dm-integrity를 통한 Authenticated Encryption with Additional Data (AEAD) 를 위한 키이며, 나중에 이를 걷어내게 될 것입니다.

💡
Cipher key 의 길이가 64바이트이므로 64바이트 수준의 암호화로 생각할 수 있지만, XTS의 특성상 키가 앞의 32바이트와 뒤의 32바이트("tweak key") 로 분할되므로, 실제로 암호화에 사용되는 키의 강도는 32바이트입니다.

cryptstup luksOpen 을 통해 키 획득하기

평상시와 같은 cryptseupt luksOpen 을 이용하여 LUKS 컨테이너를 열되, --disable-keyring 옵션을 추가합니다.

cryptsetup --disable-keyring --integrity-no-journal luksOpen /dev/nvme0n1p4 PV10SN1D

그 이후 lsblk 를 사용하면 그 상태가 아래와 같을 것입니다.

$ lsblk
NAME             MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme0n1          259:0    0 931.5G  0 disk  
└─nvme0n1p4      259:4    0 723.1G  0 part  
  └─PV10SN1D_dif 253:3    0 680.5G  0 crypt 
    └─PV10SN1D   253:4    0 680.5G  0 crypt 

그 이후 dmsetup table --showkeys를 하면 암호화 키를 획득할 수 있습니다.

⚠️
dmsetup table --showkeys 를 통해 노출된 키가 셸 히스토리, 스크린샷 등에 남지 않도록 주의하십시오.
PV10SN1D: 0 1427017544 crypt capi:authenc(hmac(sha256),xts(aes))-plain64 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy 0 253:3 0 1 integrity:32:aead
PV10SN1D_dif: 0 1427017544 integrity 259:4 32768 32 D 4 journal_sectors:131032 interleave_sectors:32768 buffer_sectors:128 fix_padding

위에서 x또는 y로 가린 부분이 실제 데이터로 채워져 있어야 합니다.

  • 만약 0으로 채워져 있다면, dmsetup table 명령어에 --showkeys 옵션을 사용하였는지 확인합니다.
  • 만약 :96:logon:cryptsetup:a895642f-432e-48be-a370-276a801c85e3-d0 와 같이 표시되어 있다면, cryptsetup luksOpen--disable-keyring 옵션을 주었는지 확인합니다.

이제 cryptsetup close 를 통해 열었던 LUKS 컨테이너를 도로 닫아줍니다.

Authenticated disk encryption 걷어내기

위에서 dmsetup table --showkeys 를 통해 획득하셨던 결과물에 아래의 수정을 가합니다.

  • dm-integirty target에 대하여 (PV10SN1D_dif)
    • 대문자 D 만 있는 부분을 R 로 고칩니다.
      • 만약 위의 cryptsetup luksOpen--integrity-no-journal 옵션을 사용하지 않았더라면, 대문자 D 가 아닌 J 였을 수도 있겠습니다. 아무튼 R 로 고칩니다.
      • 이 옵션의 의미에 대하여는 여기에서 확인할 수 있습니다. R은 dm-integrity의 복구 모드(recovery mode)를 의미합니다.
  • dm-crypt target에 대하여 (PV10SN1D)
    • capi:authenc(hmac(sha256),xts(aes))-plain64aes-xts-plain64로 고칩니다.
      • 이는 위의 cryptsetup luksDump /dev/mapper/nvme0n1p4 를 통하여 확인한 값입니다.
    • 바로 뒤에 따르는 키가 ᅟ192자인데, 16진법의 표현에서 2글자당 1바이트이므로 총 96바이트인데, 앞의 64바이트만 남기고 나머지는 지웁니다.
      • 다시 말하자면 32바이트를 지워야 하는데, 2글자당 1바이트이므로 64글자를 지웁니다.
      • 이 게시물에서 보여드리고 있는 예제에서는, x로 표시한 부분만 남기고 y로 표시한 부분은 지운다는 의미입니다.
      • 여기서 64타이트는 cryptsetup luksDump /dev/mapper/nvme0n1p4 를 통하여 확인한 cipher key 값입니다.
    • 이 문서를 참고하여 integrity:32:aead 옵션을 지우고, #opt_params 에 해당하는 숫자를 1만큼 내립니다.
      • 이 예제에서는, 가장 마지막에 있는 1 integirty:32:aead#opt_paramsopt_params인데, opt_params 에서 integrity:32:aead를 지우고 #opt_params 에 해당하는 1은 0으로 내립니다.

각 행의 맨 앞에 붙는 PV10SN1D:PV10SN1D_dif:를 지우고, 각 옵션을 integrity_targetcrypt_target 으로 저장하면 아래와 같습니다.

0 1427017544 integrity 259:4 32768 32 R 4 journal_sectors:131032 interleave_sectors:32768 buffer_sectors:128 fix_padding
0 1427017544 crypt aes-xts-plain64 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 0 253:3 0

이 데이터의 규격에 대해서 좀 더 자세히 알아보고 싶으시다면, 이 문서의 "TABLE FORMAT" 단락을 참고하시면 되겠습니다.

dmsetup 을 이용하여 dm-integrity와 dm-crypt를 설정하고, XFS 마운트하기

dmsetup create PV10SN1D_dif < ./integrity_target
dmsetup create PV10SN1D < ./crypt_target

이후 lsblk 를 이용하여 정상적으로 target이 생성된 것을 확인한 후,

mount -oro,norecovery /dev/mapper/PV10SN1D /mnt

를 이용하면 마운트가 정상적으로 이루어지고, 데이터를 확인할 수 있게 됩니다. 이 때의 커널 로그는 아래와 같습니다.

[55980.242565] XFS (dm-4): Mounting V5 filesystem in no-recovery mode. Filesystem will be inconsistent.

마운트 해제 이후 dmsetup remove PV10SN1Ddmsetup remove PV10SN1D_dif 를 하면 정리됩니다.

💡
dm-integrity 의 복구 모드(R)을 이용하였으므로, dm-integrity 의 무결성 검사가 완전히 비활성화되며 장치에 쓰기 작업이 허용되지 않습니다. 파일시스템 마운트 후 읽을 수 있는 데이터를 안전한 저장소로 복사하여야 합니다.

또한 norecovery 는 파일시스템 자체의 복구 능력을 사용할 수 없는 설정입니다. 파일시스템 수준의 불일치(inconsistency)가 보일 수 있습니다. 현 단계에서 최대한의 데이터를 안전한 곳으로 저장한 후, 아래 단락을 참고하여 추가적인 복구를 시도하면 좋습니다.

norecovery 를 사용하지 않고 파일시스템을 마운트하고 싶다면

파일시스템 (이 경우는 XFS) 가 가진 자체적인 복구 능력을 이용하고자 한다면 norecovery 옵션을 마운트 때 사용해서는 안되는데, dm-integrity 에 복구 모드(R)가 켜져있다면 블록 디바이스에 쓰기 작업이 허용되지 않으므로 ronorecovery 옵션이 필수로 요구됩니다.

dm-integrity 의 복구 모드가 켜진 상태에서 마운트에 성공했다면, 살릴 수 있는 데이터를 최대한 확보한 후, dm-integrity 옵션을 R 이 아닌 원래의 것 (가령 JD)로 설정한 후, ronorecovery 옵션을 제외하여 파일시스템 마운트를 시도해봅니다. 이 작업이 성공적이었다면 커널 로그는 아래와 같습니다.

[56413.859119] XFS (dm-4): Mounting V5 Filesystem
[56413.951776] XFS (dm-4): Starting recovery (logdev: internal)
[56413.980213] XFS (dm-4): Ending recovery (logdev: internal)