- 虚拟化
- 硬件仿真
【免费下载链接】qemu
Official QEMU mirror. Please see https://www.qemu.org/contribute/ for how to submit changes to QEMU. Pull Requests are disabled. Please only use release tarballs from the QEMU website.
LUKS 分离卷头(detached header)允许将卷头与密钥材料存放在独立于负载数据的磁盘/文件上,是 QEMU 自 9.0 起正式支持的能力(对应 QAPI 字段detached-header)。本文以 QEMU 仓库中的设计文档 docs/devel/luks-detached-header.rst 为主体,结合 crypto/、block/crypto.c 与 iotests 测试实现,系统讲解其设计动机、块设备图架构,并给出qemu-img创建、-blockdev启动配置与 libvirtvirsh qemu-monitor-command热插拔三条完整可运行的实操路径。读完本文,你将掌握分离卷头与普通 LUKS 卷的差异、QEMU 块层如何用header子节点关联两块存储,以及如何在创建和运行两个阶段把加密盘接入虚拟机。
背景:为什么需要把 LUKS 卷头单独存放
LUKS 格式本身支持将卷头存放在与负载(payload)不同的卷上。QEMU 的 LUKS 驱动(block/crypto.c + crypto/block-luks.c)扩展了这一能力,使 qemu-img 和 QEMU 块层都能读写这种"分离卷头"的加密卷。
普通 LUKS 卷在单块磁盘上的布局如下:
+-----------------------------------------------+ | | | | disk | header | key material | disk payload data | | | | | +-----------------------------------------------+使用分离卷头后,需要两块磁盘:disk1存放卷头和密钥材料,disk2存放纯负载数据:
+--------------------------+ disk1 | header | key material | +--------------------------+ +---------------------+ disk2 | disk payload data | +---------------------+这样做能带来四类实际收益(设计文档原文归纳):
- 保密性(Secrecy):由于卷上没有卷头,
disk2无法被识别为包含 LUKS 卷,不会暴露加密盘的身份。 - 访问控制(Control):如果
disk1的访问受到限制,即使有人拿到disk2也无法解锁。典型场景是把负载盘放在 NFS 上,但只向被指定的宿主机动态提供卷头访问权,从而限制哪些宿主机能从中启动 VM 实例。 - 灵活性(Flexibility):应用数据卷往往已有固定大小,扩容来加加密层很不方便。此时可以单独存放 LUKS 卷头,直接复用现有存储卷作为负载。
- 可恢复性(Recovery):卷头中一个比特的损坏可能让整个负载无法访问。分离卷头便于单独备份卷头;主卷头损坏时,仍可通过指向备份的分离卷头来解锁数据。
架构:块设备图上的两层父子关系
以 qcow2 加密为例,分离卷头在 QEMU 块设备图(block graph)中的拓扑如下(来自设计文档):
+-----------------------------+ Root node | foo[luks] | +-----------------------------+ | | file | header | | | +---------------------+ +------------------+ Child node |payload-format[qcow2]| |header-format[raw]| +---------------------+ +------------------+ | | file | file | | | +----------------------+ +---------------------+ Child node |payload-protocol[file]| |header-protocol[file]| +----------------------+ +---------------------+ | | Host storage Host storage根节点foo[luks]有两个子节点:file(负载数据所在的 qcow2 格式节点,其下再挂 file 协议节点访问宿主存储)与header(LUKS 卷头和密钥材料所在的 raw 格式节点,同样经 file 协议节点落到另一份宿主存储)。
源码中这一拓扑的落点如下:
- 在 block/crypto.c 的
block_crypto_open_generic()中,驱动通过bdrv_open_child(..., "header", ...)以BDRV_CHILD_METADATA角色打开header子节点,并将引用保存在BlockCrypto结构的BdrvChild *header字段(block/crypto.c)。 - 一旦检测到
header子节点存在,就会置位QCRYPTO_BLOCK_OPEN_DETACHED标志(block/crypto.c),再调用qcrypto_block_open()打开加密卷。 - 在加密层内部,
QCryptoBlock结构以bool detached_header字段记录这一状态(crypto/blockpriv.h)。
这一设计意味着:QEMU 块层并不要求负载必须是 qcow2——raw、qcow2 等任何可作为file子节点的格式都可以作为加密负载,卷头侧则统一以 raw 格式承载。
使用一:用 qemu-img 创建带分离卷头的 LUKS 盘
创建分离卷头与负载
第一步用qemu-img create以detached-header=true创建只含卷头的镜像文件;第二步创建负载镜像;第三步用 JSON 语法把它们组装成一个 LUKS 卷来查看信息:
# qemu-img create --object secret,id=sec0,data=abc123 -f luks \ -o cipher-alg=aes-256,cipher-mode=xts -o key-secret=sec0 \ -o detached-header=true test-header.img # qemu-img create -f qcow2 test-payload.qcow2 200G # qemu-img info 'json:{"driver":"luks","file":{"filename": \ "test-payload.img"},"header":{"filename":"test-header.img"}}'三个命令的要点:
- 第一个命令的
-f luks -o detached-header=true是关键:block_crypto_co_create_luks()在解析选项时通过qemu_opt_get_bool_del(opts, "detached-header", false)读取该布尔选项(block/crypto.c),若为真则向加密层传递QCRYPTO_BLOCK_CREATE_DETACHED标志(block/crypto.c)。 - 在加密层创建流程中,
block->detached_header = flags & QCRYPTO_BLOCK_CREATE_DETACHED(crypto/block.c)。随后 crypto/block-luks.c 会做两件事:把payload_offset_sector置 0,表示卷头镜像本身从 0 扇区开始读写;并调用initfunc()为卷头镜像预留(header_sectors + QCRYPTO_BLOCK_LUKS_NUM_KEY_SLOTS * split_key_sectors) * sector_size的空间,容纳卷头与全部密钥槽。 - 第三个命令中
file指向负载镜像、header指向卷头镜像,QEMU 通过qemu-img info的 JSON 协议描述即可同时解析两块镜像。qemu-img info的format-specific输出中会包含detached-header字段(见下文测试章节),用于确认卷是否处于分离状态。
打开时如何判定"分离"状态
打开(而非创建)时,判定逻辑藏在负载偏移上:crypto/block-luks.c 中block->detached_header = (block->payload_offset == 0)。也就是说,LUKS 卷头中记录的payload_offset若为 0,则视为分离卷头——此时负载数据从文件起始处读写,而正常内嵌卷头情形下负载偏移等于卷头+密钥材料的长度。
QAPI 层面对齐
qemu-img info输出中的detached-header字段来自 QAPI 结构QCryptoBlockInfoLUKS,其定义位于 qapi/crypto.json,注释明确标注 "whether the LUKS header is detached (Since 9.0)"。该信息最终由 crypto/block-luks.c 的info->u.luks.detached_header = block->detached_header填充。设计文档与 QAPI 均将分离卷头能力标记为 QEMU 9.0 起引入。
使用二:启动 VM 时用 -blockdev 组装分离卷头 LUKS 盘
在qemu-system-x86_64启动参数中,分离卷头盘需要四个块设备节点(存储层 ×2、格式层 ×2)再加一个 luks 格式层,共五个节点:
# qemu-system-x86_64 ... \ -object '{"qom-type":"secret","id":"libvirt-3-format-secret", \ "data":"abc123"}' \ -blockdev '{"driver":"file","filename":"/path/to/test-header.img", \ "node-name":"libvirt-1-storage"}' \ -blockdev '{"node-name":"libvirt-1-format","read-only":false, \ "driver":"raw","file":"libvirt-1-storage"}' \ -blockdev '{"driver":"file","filename":"/path/to/test-payload.qcow2", \ "node-name":"libvirt-2-storage"}' \ -blockdev '{"node-name":"libvirt-2-format","read-only":false, \ "driver":"qcow2","file":"libvirt-2-storage"}' \ -blockdev '{"node-name":"libvirt-3-format","driver":"luks", \ "file":"libvirt-2-format","header":"libvirt-1-format","key-secret": \ "libvirt-3-format-secret"}' \ -device '{"driver":"virtio-blk-pci","bus":XXX,"addr":YYY,"drive": \ "libvirt-3-format","id":"virtio-disk1"}'节点组织与架构图的对应关系:
| 节点 | 驱动 | 作用 | 对应架构图角色 |
|---|---|---|---|
libvirt-1-storage | file | 打开卷头镜像文件 | header-protocol[file] |
libvirt-1-format | raw | 以 raw 格式解析卷头 | header-format[raw] |
libvirt-2-storage | file | 打开负载镜像文件 | payload-protocol[file] |
libvirt-2-format | qcow2 | 以 qcow2 格式解析负载 | payload-format[qcow2] |
libvirt-3-format | luks | 通过file+header绑定两者并解密 | foo[luks](根节点) |
关键点:
- luks 节点的
file指向 qcow2 格式节点,header指向 raw 格式节点,key-secret指向解密用的 secret 对象。这个header参数正是 block/crypto.c 中bdrv_open_child(..., "header", ...)所消费的选项。 - secret 对象通过
data直接给出口令;生产环境建议改用file指向密钥文件以避免口令出现在命令行中。 bus、addr需按实际 PCI 拓扑填写,示例中用XXX/YYY占位。
使用三:运行时向运行中的 VM 热插拔分离卷头盘(libvirt QMP)
若 VM 已由 libvirt 管理,可在运行期间通过virsh qemu-monitor-command以 QMP 消息逐步完成热插拔。整个过程与-blockdev参数一一对应,共 7 步。
第 1 步:注入解密 secret
# virsh qemu-monitor-command vm '{"execute":"object-add", \ "arguments":{"qom-type":"secret", "id": \ "libvirt-4-format-secret", "data":"abc123"}}'第 2 步:添加卷头协议节点(file)
# virsh qemu-monitor-command vm '{"execute":"blockdev-add", \ "arguments":{"node-name":"libvirt-1-storage", "driver":"file", \ "filename": "/path/to/test-header.img" }}'第 3 步:添加卷头 raw 格式节点
# virsh qemu-monitor-command vm '{"execute":"blockdev-add", \ "arguments":{"node-name":"libvirt-1-format", "driver":"raw", \ "file":"libvirt-1-storage"}}'第 4 步:添加负载协议节点(file)
# virsh qemu-monitor-command vm '{"execute":"blockdev-add", \ "arguments":{"node-name":"libvirt-2-storage", "driver":"file", \ "filename":"/path/to/test-payload.qcow2"}}'第 5 步:添加负载 qcow2 格式节点
# virsh qemu-monitor-command vm '{"execute":"blockdev-add", \ "arguments":{"node-name":"libvirt-2-format", "driver":"qcow2", \ "file":"libvirt-2-storage"}}'第 6 步:添加 luks 格式节点,用 header 字段关联卷头
# virsh qemu-monitor-command vm '{"execute":"blockdev-add", \ "arguments":{"node-name":"libvirt-3-format", "driver":"luks", \ "file":"libvirt-2-format", "header":"libvirt-1-format", \ "key-secret":"libvirt-2-format-secret"}}'注意:设计文档第 6 步示例中的
key-secret写作libvirt-2-format-secret,与第 1 步创建的libvirt-4-format-secret名称不一致,实操时应保持 secret id 前后统一(例如统一改为libvirt-4-format-secret),否则解锁会因找不到 secret 而失败。这也提醒我们:header参数指定的是卷头格式节点,而key-secret指定的是口令对象,两者职责不同。
第 7 步:热插拔 virtio-blk 设备
# virsh qemu-monitor-command vm '{"execute":"device_add", \ "arguments": {"driver":"virtio-blk-pci", \ "drive": "libvirt-3-format", "id":"virtio-disk2"}}至此,运行中的 VM 内会新增一块由分离卷头解密、qcow2 负载的加密 virtio 磁盘。要撤销可依次device_del、blockdev-del逆序拆除节点。
源码级佐证:iotests 测试如何验证整个链路
仓库自带的集成测试 tests/qemu-iotests/tests/luks-detached-header(归入rw auto测试组,随qemu-iotests运行)覆盖了与本文完全一致的三种场景,可作为实操模板:
- 创建阶段:测试同时使用两条路径创建分离卷头:一是
blockdev-create("driver": imgfmt, "header": "luks-2-header-storage", "file": "luks-2-payload-storage"),二是qemu_img_create("-f", "luks", ..., "-o", "detached-header=true", ...),验证了 CLI 与 QMP 两条创建路径等价。 qemu-img info校验:test_img_creation()断言普通 LUKS 盘的format-specific.data["detached-header"]为False,而 raw 负载与 qcow2 负载两种分离卷头盘均为True,正好印证上文 QAPI 字段与打开判定逻辑。- I/O 验证:
test_detached_luks_header()分别对普通盘、raw 负载分离盘、qcow2 负载分离盘执行write -P/read -P模式写读,并在tearDown()中检查日志里是否出现 "Pattern verification failed"。测试里 qemu-io 的write -P 41(raw 负载)、write -P 42(qcow2 负载)等模式写读全部通过,说明分离卷头下的加解密路径与普通卷完全一致。
此外,打开分离卷头时加密层会做针对性优化:include/crypto/block.h中QCRYPTO_BLOCK_OPEN_DETACHED标志的注释明确说明 "the open process will be optimized to skip the LUKS payload overlap check"(跳过 LUKS 负载重叠检查),即分离场景下无需校验负载是否与卷头区域重叠。
已知限制与展望
设计文档在 TODO 一节中记录了一项待办:
- 支持 VM 内共享同一分离 LUKS 卷头:目前一个卷头节点对应一个 luks 节点,多个盘共享同一卷头的场景(例如同一密钥加密的多个负载盘共用一个卷头文件)尚未实现,属于后续演进方向。
实际使用时还需注意:分离卷头盘的备份策略要分别对待——负载盘可以随意备份/复制,但卷头文件必须妥善保管并单独备份(这正是分离带来的恢复性优势);若卷头丢失,即使拥有负载盘也无法解密数据。
小结
- 概念:分离卷头把 LUKS 卷头与密钥材料从负载中剥离,获得保密性、访问控制、灵活性与可恢复性四类收益。
- 架构:QEMU 块层以
luks根节点 +file(负载)/header(卷头)两个子节点组织,header子节点以BDRV_CHILD_METADATA角色挂载(block/crypto.c)。 - 判定:创建侧由
detached-header=true选项驱动(block/crypto.c),打开侧以payload_offset == 0判定(crypto/block-luks.c),qemu-img info通过detached-header字段(QEMU 9.0+)对外暴露(qapi/crypto.json)。 - 实操:创建用
qemu-img create -o detached-header=true,启动用 5 节点-blockdev链,热插拔用 7 步 QMP 消息,全部流程可对照 tests/qemu-iotests/tests/luks-detached-header 验证。
相关参考文件:设计文档、块层驱动、加密层 LUKS 实现、QAPI 定义、加密层标志、集成测试。
- 虚拟化
- 硬件仿真
【免费下载链接】qemu
Official QEMU mirror. Please see https://www.qemu.org/contribute/ for how to submit changes to QEMU. Pull Requests are disabled. Please only use release tarballs from the QEMU website.
相关推荐
QEMU 的 LUKS 分离头(Detached Header)加密卷:架构原理与实战配置
QEMU 的 LUKS 分离头(Detached Header)加密卷:架构原理与实战配置 本文基于 QEMU 仓库中的 Cryptography in QEM
虚拟化硬件仿真QEMU 虚拟 CPU(vCPU)热插拔完整实战:基于 QMP device_add / device_del 的在线增删 CPU 指南
QEMU 虚拟 CPU(vCPU)热插拔完整实战:基于 QMP device_add / device_del 的在线增删 CPU 指南 导读 本文以 QEMU
虚拟化硬件仿真QEMU qdev 设备模型 API 完全指南:从设备创建、realize 到 GPIO 与热插拔
QEMU qdev 设备模型 API 完全指南:从设备创建、realize 到 GPIO 与热插拔 本文是 QEMU 开源项目内部 API 参考文档( docs
虚拟化硬件仿真
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考