为加密模拟虚拟 TPM 添加支持

https://blueprints.launchpad.net/nova/+spec/add-emulated-virtual-tpm

有一类应用程序期望使用 TPM 设备来存储密钥。为了在虚拟机中运行这些应用程序,将虚拟 TPM 设备暴露给客户机将会很有用。因此,建议添加 flavor/image 属性,这些属性 a) 转换为用于调度的 placement traits,并且 b) 导致相关 virt 驱动程序将此类设备添加到 VM 中。

问题描述

目前,nova 中创建虚拟机时,无法向客户机提供虚拟 TPM 设备。

用例

支持虚拟化现有应用程序和操作系统,这些应用程序期望使用物理 TPM 设备。至少有一个 hypervisor (libvirt/qemu) 当前支持创建与主机上的每个 VM 的 swtpm 进程关联的模拟 TPM 设备,但没有办法告诉 nova 启用它。

提议的变更

最近的 libvirt 和 qemu (以及可能其他 hypervisor) 支持模拟的 vTPM 设备。我们建议修改 nova 以利用此功能。

此 spec 仅描述 libvirt 实现。

XML

所需的 libvirt XML 参数如下所示 (来源)

...
<devices>
  <tpm model='tpm-tis'>
    <backend type='emulator' version='2.0'>
      <encryption secret='6dd3e4a5-1d76-44ce-961f-f119f5aad935'/>
    </backend>
  </tpm>
</devices>
...

先决条件

支持加密模拟 TPM 至少需要

  • libvirt 版本 5.6.0 或更高版本。

  • qemu 2.11 作为最低版本,但推荐 qemu 2.12。virt 驱动程序代码应添加合适的版本检查(对于 LibvirtDriver,这将包括对 libvirt 和 qemu 的检查)。目前,模拟 TPM 仅支持 x86,但这是一种实现细节,而不是架构限制。

  • 主机上的 swtpm 二进制文件和库。

  • 访问与 castellan 兼容的密钥管理器,例如 barbican,用于存储用于加密虚拟设备数据的密码短语。(密钥管理器实现的公共方法必须能够从 context 参数(这是接口的一部分)消耗用户的 auth token。)

  • 访问对象存储服务,例如 swift,用于存储主机在 shelve 等操作期间用于虚拟设备数据的文件。

配置

以下所有内容均适用于 compute(而非 conductor/scheduler/API)配置

  • 将引入一个新的配置选项,作为启用 vTPM 的“主开关”。此配置选项也适用于未来驱动程序的实现,但由于此 spec 和当前实现特定于 libvirt,因此它位于 libvirt 而不是 compute 组中

    [libvirt]
    vtpm_enabled = $bool (default False)
    
  • 为了支持移动操作(涉及在新的主机上重建 vTPM 的任何操作),nova 必须能够使用正确的 ownership(即 libvirt 将创建的 swtpm 进程的 ownership)来放置 vTPM 数据,但我们无法事先检测到该 ownership 将是什么。因此,我们需要在 compute 上指定两个配置选项,指示应拥有主机上 vTPM 数据的用户和组

    [libvirt]
    swtpm_user = $str (default 'tss')
    swtpm_group = $str (default 'tss')
    
  • (现有,已知)选项用于 [key_manager]

  • 将引入新的标准 keystoneauth1 auth/session/adapter 选项用于 [swift]

Traits、Extra Specs、Image Meta

为了支持此功能,我们建议

  • 使用现有的 COMPUTE_SECURITY_TPM_1_2COMPUTE_SECURITY_TPM_2_0 traits。这些代表当前支持的 TPM spec 的两个不同版本。(请注意,2.0 与 1.2 不向后兼容,因此我们不能简单地忽略 1.2。两种版本之间的差异摘要目前可在 此处获得。)当满足所有 先决条件并且 配置开关打开时,libvirt compute 驱动程序将在 compute 节点资源提供程序上设置这两个 traits。

  • 支持以下新的 flavor extra_specs 及其对应的 image metadata 属性(这些属性只是下面内容的 s/:/_/

    • hw:tpm_version={1.2|2.0}。这将是

      • 转换为 allocation candidate request 中的相应 required=COMPUTE_SECURITY_TPM_{1_2|2_0},以确保实例落在能够以请求版本使用 vTPM 的主机上

      • 由 libvirt compute 驱动程序用于注入适当的客户机 XML

      注意

      虽然可以将 trait:COMPUTE_SECURITY_TPM_{1_2|2_0}=required 直接在 flavor extra_specs 或 image metadata 中指定,但这只会将实例放在一个能够的主机上;它不会触发 libvirt 驱动程序创建虚拟 TPM 设备。因此,为了避免混淆,将不会将此作为可能性记录下来。

    • hw:tpm_model={TIS|CRB}。指示要使用的模拟模型。如果省略,则默认值为 TIS(这对应于 libvirt 默认值)。CRB 仅与 TPM 版本 2.0 兼容;如果请求版本 1.2 时使用 CRB,API 将引发错误。

总而言之,所有且仅支持以下组合,并且它们是互斥的(不能相互兼容)

  • 版本 1.2,模型 TIS

  • 版本 2.0,模型 TIS

  • 版本 2.0,模型 CRB

请注意,由于 TPM 是模拟的(主机上的进程/文件),因此“库存”实际上是无限的。因此,此功能没有关联的资源类。

如果 flavor 和 image 都指定了 TPM trait 或设备模型,并且这两个值不匹配,API 将引发异常。

实例生命周期操作

以下描述是 libvirt 驱动程序特定的。但是,由实现决定哪些部分由 compute manager 执行,哪些部分由 libvirt ComputeDriver 本身执行。

注意

在决定是否支持给定的操作时,我们以“这在裸机上如何工作”作为起点。如果我们可以支持 VM 操作而不会引入过度的复杂性或用户面临的奇怪情况,我们就会这样做。

生成

  1. 即使 swift 对于生成不是必需的,也要确保服务目录中存在 swift 端点(并且可访问?版本发现?实现细节),以便将来的 unshelve 不会破坏实例。

  2. Nova 生成一个随机密码短语并将其存储在配置的密钥管理器中,产生一个 UUID,以下称为 $secret_uuid

  3. Nova 将 $secret_uuid 存储在实例的 system_metadata 下的键 tpm_secret_uuid 中。

  4. Nova 使用 virSecretDefineXML API 定义一个私有(无法列出值)的、短暂的(状态仅存储在内存中,从不存储在磁盘上)secret,其 name 是实例 UUID,其 UUID 是 $secret_uuid。然后使用 virSecretSetValue API 将其值设置为生成的密码短语。

  5. Nova 将 XML 注入到实例的 domain 中。modelversion 从 flavor/image 属性中获取,并且 secret$secret_uuid

  6. 一旦 libvirt 创建了客户机,nova 使用 virSecretUndefine API 删除 secret。实例的模拟 TPM 仍然可以正常工作。

注意

从通过快照创建具有 vTPM 的 VM 创建的镜像生成实例将导致新的、空的 vTPM,即使该快照是由 shelve 创建的。相比之下,spawn during unshelve 将恢复此类 vTPM 数据。

冷启动

……以及任何其他从头开始启动客户机的操作。(根据 密钥管理器 安全模型,这些操作可能仅限于实例所有者。)

  1. 从实例的 system_metadatatpm_secret_uuid 中提取 $secret_uuid

  2. 通过配置的密钥管理器 API 检索与 $secret_uuid 关联的密码短语。

然后执行 生成 下的步骤 4-6。

迁移及其类似操作

对于 libvirt 实现,模拟 TPM 数据存储在 /var/lib/libvirt/swtpm/<instance> 中。某些生命周期操作要求将该目录逐字复制到“目标”。对于(冷/实时)迁移,只有 nova-compute 运行的用户才能保证已设置用于无密码访问的 SSH 密钥,并且它只能保证能够将文件复制到目标节点上的实例目录。因此,我们建议对相关生命周期操作执行以下过程

  • 将目录复制到本地实例目录中,更改 ownership 以匹配它。

  • 执行移动,这将自动携带数据。

  • 更改 ownership 并将目录移到目标上的 /var/lib/libvirt/swtpm/<instance>

  • 在确认/撤销时,分别从源/目标删除目录。(这在 libvirt 拆除客户机时自动完成。)

  • 在撤销时,必须恢复数据目录(具有适当的权限)到源。

由于目标上的预期 ownership 可能与源不同,并且(我们认为)无法检测到,管理员必须通过新的 [libvirt]swtpm_user[libvirt]swtpm_group 配置 选项告知我们,如果与默认值 tss 不同。

这应该允许支持冷/实时迁移和不更改设备的调整大小。

待办事项

确认上述“手动”复制操作实际上对于迁移是必要的。从阅读 https://github.com/qemu/qemu/blob/6a5d22083d50c76a3fdc0bffc6658f42b3b37981/docs/specs/tpm.txt#L324-L383 来看并不清楚。

调整大小有可能将 vTPM 添加到没有 vTPM 的实例,或者从具有 vTPM 的实例中删除 vTPM,这些操作应该“正常工作”。在从一个版本/模型调整大小到另一个版本时,数据无法携带(对于同主机调整大小,我们必须 *删除* 旧的备份文件)。如果旧的和新的 flavor 具有相同的模型/版本,我们必须确保我们像上面描述的那样传递虚拟设备数据(对于同主机调整大小,我们必须 *保留* 现有的备份文件)。

Shelve(卸载)和 Unshelve

恢复 shelve 卸载的服务器上的 vTPM 数据需要将 vTPM 数据持久化到某个地方。我们不能将其与镜像本身一起放置,因为它与实例磁盘外部的数据。因此,我们建议将其放入对象存储(swift)并在实例的 system_metadata 中维护对 swift 对象的引用。

shelve 操作需要

  1. 将 vTPM 数据目录保存到 swift。

  2. 将 swift 对象 ID 和目录的数字签名(sha256)保存到实例的 system_metadata 下的(新的)tpm_object_idtpm_object_sha256 键下。

  3. 在镜像上创建适当的 hw_tpm_version 和/或 hw_tpm_model metadata 属性。(这是为了弥补原始 VM 上的 vTPM 是应镜像属性的要求而创建的差距,而不是 flavor 属性。它确保了 unshelve 上的正确调度,并且目标上创建了正确的版本/模型。)

对已 shelve(但未卸载)的实例执行 unshelve 操作应该“正常工作”(不包括删除 swift 对象;参见下文)。用于卸载实例的代码路径需要

  1. 确保我们落在能够满足必要 vTPM 版本和模型的 host 上(我们通过通用的调度代码路径免费获得此信息,因为我们在 shelve 期间执行了步骤 3)。

  2. 查找实例的 system_metadata 中的 tpm_object_{id|sha256}tpm_secret_uuid

  3. 下载 swift 对象。验证其校验和,如果校验和不匹配则失败。

  4. 根据 host 上的 [libvirt]swtpm_{user|group} 分配数据目录的 ownership。

  5. 检索 secret 并将其提供给 libvirt;并生成适当的 domain XML(我们通过 spawn() 免费获得此信息)。

  6. 从 swift 中删除对象,以及实例 system_metadata 中的 tpm_object_{id|sha256}。必须从两个代码路径(即,无论 shelve 的实例是否已卸载)执行此步骤。

注意

用户仍然可以通过一些方法“绕过”我们的检查,在取消保存时造成可怕的后果。例如

  • 该配置指定了没有 vTPM 属性。

  • 原始镜像指定了版本 2.0。

  • 在保存和取消保存之间,编辑快照以指定版本 1.2。

我们会 happily 创建一个 v1.2 vTPM,并将 (v2.0) 数据恢复到其中。虚拟机 (可能) 可以正常启动,但当访问 vTPM 时会发生不可预测的事情。

我们无法阻止所有愚蠢行为。

注意

安全影响 中所述,如果保存操作由管理员执行,则只有管理员才能执行相应的取消保存操作。并且,根据 密钥管理器 的安全模型,如果保存操作由用户执行,管理员可能无法执行相应的取消保存操作。

由于底层设备数据是 virt 驱动程序特定的,因此必须由 virt 驱动程序管理;但我们希望对象存储交互由计算管理器完成。因此,我们提出以下计算管理器和 virt 驱动程序之间的交互方式

当前的 ComputeDriver.snapshot() 合同没有指定返回值。它将被更改为允许返回一个类文件对象,其中包含 (预打包的) 底层设备数据。libvirt 驱动程序实现将打开一个 tar 流,并返回该句柄。计算管理器负责从该句柄读取内容并将其推送到 swift 对象存储。(实现细节:我们仅在保存期间对快照执行 swift 操作,因此 a) virt 驱动程序不应在虚拟机处于 SHELVE[_OFFLOADED] 状态时才生成句柄;和/或计算管理器应显式关闭来自 snapshot() 其他调用的句柄。)

卸载实例的计算驱动程序触点是 spawn()。此方法将获得一个新的关键字参数,该参数是一个类文件对象。如果不是 None,virt 驱动程序实现负责从该句柄流式传输数据,并反转在 snapshot() 期间所做的事情(在本例中,解压缩 tar 文件)。对于卸载实例的取消保存路径,计算管理器将从 swift 对象存储中拉取对象,并通过此关键字参数将其流式传输到 spawn()

createImage 和 createBackup

由于 vTPM 数据与实例相关,而不是与镜像相关,因此 createImagecreateBackup 流程不会被更改。特别是,它们不会尝试将 vTPM 底层设备保存到 swift 对象存储。

这与新的 Spawn 不会尝试恢复 vTPM 数据(即使给定通过 shelve 创建的镜像)的事实一起,也防止了“克隆” vTPM。

这类似于裸机情况,从“干净”系统上的镜像/备份生成会得到一个“干净”(或没有)TPM。

重建

由于实例停留在同一主机上,因此我们可以保留现有的 vTPM 底层文件不变。这类似于裸机行为,在现有系统上恢复备份不会触碰 TPM(或任何其他设备),因此您将获得已经存在的 TPM。但是,通过使用不同的镜像和/或具有不同元数据的镜像重建,也可能会将实例锁定在其 vTPM 之外。用户有责任避免使用 TPM 创建主密钥而不将该主密钥保存(在重建镜像中,或其它地方)的情况。

也就是说,重建将涵盖以下场景

  • 如果没有现有的 vTPM 底层数据,并且重建镜像请求 vTPM,则创建一个新的 vTPM,就像 Spawn 一样。

  • 如果存在现有的 vTPM,并且 flavor 或镜像都不请求 vTPM,则删除它。

  • 如果存在现有的 vTPM,并且 flavor 或镜像请求 vTPM,则保留底层文件不变。但是,如果旧镜像和新镜像与 flavor 组合请求了不同的版本/型号,我们将使重建失败。

撤离

由于 vTPM 数据属于 libvirt 而不是存储在实例磁盘中,因此即使实例是 volume-backed,vTPM 也会在 evacuate 时丢失。这类似于裸机行为,即使通过共享存储将状态复活到另一个系统,硬件 TPM 也会被留下。

(可以通过将 /var/lib/libvirt/swtpm/ 挂载到共享存储来缓解此问题,但 libvirt 在创建/拆除来宾时对该目录的管理可能会阻碍此类尝试。无论如何,这将是一项管理员的工作;nova 不会执行任何操作来支持或阻止它。)

销毁

  1. 删除与 system_metadata['tpm_secret_uuid'] 关联的密钥管理器 secret。

  2. libvirt 在来宾拆除过程中删除 vTPM 数据目录。

  3. 如果存在 system_metadata['tpm_object_id'],则 API 侧 将删除它标识的 swift 对象。由于此元数据仅在实例保存时存在,因此这仅适用于诸如

    • 在保存和卸载之间执行 destroy() 的情况。

    • 清理处于 ERROR 状态的 VM,来自保存、卸载或取消保存失败(在恰当的时间)的情况。

    • 清理主机关闭时被删除的 VM。

限制

这是此设计导致的奇怪或意外行为的总结。

  • 除了迁移和保存-卸载之外,vTPM 数据与实例+主机保持一致。特别是

    • vTPM 数据在 Evacuate 时丢失。

    • vTPM 数据不会随“可重用快照”(createBackup/createImage)一起携带。

  • 根据 密钥管理器安全模型,实例所有者或管理员执行某些实例生命周期操作的能力可能会受到限制。

  • 由于 virt 驱动程序完成 secret 管理,因此当计算主机关闭时删除实例可能会使 secret 孤立。如果主机恢复启动,则当计算调用 virt 驱动程序的 destroy 时,secret 将被回收。但如果主机从未恢复启动,则必须手动删除它。

备选方案

  • 与其使用 trait,我们可以使用任意大的 1_2/2_0 资源类库存。除非可以证明存在我们可以发现的实际限制,否则这并不是我们做事的方式。

  • 与其使用专门的 hw:tpm* extra_spec/image_meta 属性,而是隐式配置基于 placement-ese 语法(resources:COMPUTE_SECURITY_TPM_*)。被拒绝,因为我们正在尝试摆脱这种做事方式,而是支持特定于该功能的语法,而不是要求管理员了解该功能如何映射到 placement 语法。此外,在某些情况下映射可能很简单,但在其他情况下,virt 驱动程序级别需要额外的配置,无法从 placement 语法推断出来,这将需要混合和匹配 placement 和非 placement 语法。

  • 鉴于此,禁止使用 resources[$S]:COMPUTE_SECURITY_TPM_* placement-ese 语法。被拒绝主要是由于(不必要的)额外复杂性,以及因为我们不想假设存在“让我登陆到具有 vTPM 功能的主机,但不要设置一个(现在)”的用例。

  • 使用物理直通(<backend type='passthrough'>)一个真实的(硬件)TPM 设备。由于(除其他原因)更改 secret 的所有权需要主机重新启动,因此这在当前 TPM 硬件上不可行。

  • 阻止需要对象存储的操作。这被认为不可行,特别是由于跨单元格调整大小在底层使用保存。

  • 使用 glance 或密钥管理器而不是 swift 来存储 vTPM 数据用于这些操作。被否决,因为这些服务实际上不适合该目的,并且(至少 glance)可能会在将来阻止此类用法。

  • 在任何快照操作(包括 createImagecreateBackup)上保存 vTPM 数据。这增加了复杂性以及一些意想不到的行为,例如“克隆” vTPM 的能力。在这种情况下,用户在 vTPM 表现得像(硬件)TPM 时会感到不那么惊讶。

  • 与其在 spawn 时检查 swift,不如添加一个额外的 spec / image prop,例如 vtpm_I_promise_I_will_never_shelve_offload=Truevtpm_is_totally_ephemeral=True,这将导致在保存-卸载时出错或根本不备份 vTPM。

数据模型影响

需要添加新的版本到 ImageMetaPropsImageMetaPropsPayload 对象

  • hw_tpm_version

  • hw_tpm_model

  • tpm_object_id

  • tpm_object_sha256

REST API 影响

镜像/flavor 验证器将获得新的属性一致性检查。不需要新的微版本。

安全影响

来宾将能够使用模拟的 TPM 来实现物理 TPM 提供的所有安全增强功能,以保护自身免受来宾内部的攻击。

假设 密钥管理器对象存储 服务已充分加固,以防止外部攻击。但是,部署必须考虑对这些服务的授权访问问题,如下所述。

数据盗窃

vTPM 数据文件在磁盘上加密,因此在(加密范围内)是“安全的”,可以防止简单的数据盗窃。

我们将使用 384 字节的密码短语,这是 SSH 密钥的默认大小,由 /dev/urandom 生成。将来可能需要使此大小可配置。

受损的 root

假设计算节点上的 root 用户能够通过检查内存等方式获取 vTPM 的内容和/或密码短语。除了在 libvirt 中使用私有+临时 secret 之外,没有采取进一步措施来防止受损的 root 用户。

对象存储

对象存储服务允许管理员用户完全访问对象,无论谁创建了该对象。目前没有限制管理员只能删除对象等设施。因此,如果执行了 shelve,vTPM 设备的完整内容将可供管理员使用。它们已加密,因此如果没有访问密钥,我们仍然信任加密的强度来保护数据。但是,这会增加攻击面,假设对象存储管理员与访问计算主机上的原始文件的人员不同。

同样,如果 shelve 由管理员执行,vTPM 数据对象将由管理员创建和拥有,因此只有管理员才能 unshelve 该实例。

密钥管理器

存储在密钥管理器中的 secret 更加敏感,因为它可以用于解密 vTPM 设备的内容。barbican 实现对项目级别的 secret 进行范围划分,因此部署必须注意将项目限制为所有应该信任一组通用 secret 的用户。此外,请注意,项目范围的管理员默认允许访问和解密任何项目拥有的 secret;如果管理员不可信,则应通过策略限制此权限。

但是,castellan 后端负责自己的身份验证机制。因此,部署可能希望使用仅允许创建 secret 的个人用户解密的后端。(无论如何,重要的是允许管理员删除 secret,以便可以由管理员执行 VM 删除等操作,而不会留下 secret。)

请注意,如果管理员被限制解密密钥,则管理员执行的生命周期操作将无法产生正在运行的虚拟机。这包括重启宿主机:即使设置了 resume_guests_state_on_host_boot,具有 vTPM 的实例也不会自动启动,而是必须由其所有者手动启动。其他默认情况下仅限管理员操作的生命周期操作只有在由虚拟机所有者执行时才有效,这意味着必须授予所有者适当的策略角色才能执行;否则,这些操作将实际上被禁用。

…除了实时迁移,因为 vTPM 的(已解密)运行状态会随之传递到目标位置。(为了澄清:由于上述原因,如果由管理员执行,实时迁移与其他操作不同,实际上会起作用。)

通知影响

其他最终用户影响

性能影响

  • 在创建(spawn)期间(用于注册密码短语)、冷启动(用于检索密码短语)和销毁(用于删除密码短语)期间,需要向密钥管理器进行额外的 API 调用。

  • 在创建和其他类似启动的操作期间,需要向 libvirt 进行额外的 API 调用,以定义、设置值和取消定义 libvirt 中的 vTPM 密钥。

  • 需要向对象存储(swift)进行额外的 API 调用,以创建(在 shelve 期间)、检索(unshelve)和删除(unshelve/destroy)vTPM 设备数据对象。

其他部署者影响

开发人员影响

各个 virt 驱动程序将能够根据需要实现模拟 vTPM。

升级影响

实现

负责人

主要负责人

efried

其他贡献者

cfriesen

功能联络人

efried

工作项

  • 对 flavor 和 image 属性进行预验证的 API 变更。

  • 调度器变更,用于将 flavor/image 属性转换为 placement-isms。

  • Libvirt 驱动程序变更,用于

    • 检测 先决条件配置,并将 traits 报告给 placement。

    • 与密钥管理器 API 通信。

    • 通过 libvirt API 管理 libvirt 密钥。

    • 将 flavor/image 属性转换为 domain XML

    • 复制 vTPM 文件在相关的 实例生命周期操作 上。

    • 与对象存储通信,以在(其他)相关的 实例生命周期操作 上保存/恢复 vTPM 文件。

  • 测试

依赖项

测试

将添加单元和功能测试。可能需要为对象存储和密钥管理器服务添加新的 fixtures。

由于 a) 访问加密密钥的用户身份验证的特殊性,以及 b) 一些操作的虚拟设备文件管理,将为以下内容添加 CI 覆盖:

  • 实时迁移

  • 冷迁移

  • 宿主机重启(如何?)

  • Shelve(卸载)和 unshelve

  • 备份和重建

文档影响

操作指南和最终用户指南将相应更新。功能支持矩阵将更新。

参考资料

历史

修订版

发布名称

描述

Stein

引入

Train

重新提出

Ussuri

重新提出,包含加密部分等改进