EC2 远程证明 NitroTPM on Amazon Linux 2023
本文介绍基于 Amazon Linux 2023 构建可证明的 AMI(Attestable AMI),并以 AWS KMS 作为验证方完成 EC2 实例远程证明的过程。产出物包括一个开机自动执行远程证明的 AMI、一个带 PCR4/PCR12(Platform Configuration Register,即平台配置寄存器)条件的 KMS 密钥策略。并演示使用这个可证明 AMI 启动后如何查看证明结果。为简化部署过程、方便理解概念,本文采用标准 UEFI 启动,仅校验 PCR4/PCR12,不启用 UEFI Secure Boot,也不包含 PCR7 校验。
一、什么是 EC2 实例证明(Amazon EC2 instance attestation)
1、远程证明工作原理
EC2 实例证明是远程证明(Remote Attestation)在 Amazon EC2 上的实现:EC2 实例向远端验证方出示由 Nitro 系统签名的证明文档,由验证方判断其运行的软件是否与预期一致,再决定是否向它发放密钥或数据。EC2 实例证明由如下几个部分组成:
- 可证明的 AMI:专为远程证明而构建的镜像,具有特定安全配置,其全部内容可以归结为一组可预先计算的参考 PCR 值(Reference Measurements)。
- nitro-tpm-attest 程序:在可证明 AMI 内部,发起远程证明请求
- NitroTPM:由 Nitro 系统提供、符合 TPM 2.0 规范的虚拟设备。启动过程中,启动链上的每一级组件在加载下一级之前计算其哈希,并扩展进 NitroTPM 的 PCR 中。运行时,
nitro-tpm-attest向 Nitro Hypervisor 请求证明文档,该文档包含当前 PCR 值,并由 AWS Nitro Attestation PKI 签名。 - AWS KMS 服务:通过创建客户自管理密钥,并在密钥策略中写入 PCR 值来进行校验
远程证明整体流程如下:
开发环境构建机(Amazon Linux 2023)
KIWI NG 构建 .raw 镜像 ──> nitro-tpm-pcr-compute 离线计算 PCR4 / PCR12
│ │
│ coldsnap 上传为 EBS 快照 │ 写入 KMS 密钥策略 Condition
▼ ▼
register-image 注册 AMI KMS 客户托管密钥
│ ▲
▼ │
生产环境运行实例(m7i.large/NitroTPM v2.0) │
systemd 开机服务 attest-kms-test │
1. openssl 生成一次性 RSA 密钥对 │
2. nitro-tpm-attest 生成签名证明文档 ─────┤ GenerateDataKey + Recipient
3. KMS 校验签名并比对 PCR4 / PCR12 <──────┘
4. openssl cms 用私钥解开 CiphertextForRecipient
│
▼
证明结果写入串口控制台 ──> 控制台为只读可见(不可登录)
上述流程中,镜像内的 nitro-tpm-attest 只负责生成证明文档,验证发生在 AWS KMS 服务,并非在镜像内完成。因此实例必须能够调用 AWS KMS 服务的 API,才能完成远程证明。
2、远程证明的参考 PCR 值(Reference Measurements)
EC2 实例证明以 PCR 值作为判断依据:构建镜像时离线计算出参考 PCR 值(Reference Measurements),实例运行时由证明文档携带实际 PCR 值,验证方比对二者一致后才放行。
PCR 是 TPM 内部的一组寄存器。软件不能直接写入任意值,只能执行扩展(Extend)操作,即 PCR新值 = Hash(PCR旧值 || 新写入的哈希值)。启动链上的 UEFI 固件、引导程序和内核依次把各组件的哈希值扩展进 PCR,因此最终的 PCR 值同时反映加载了哪些组件以及加载顺序。
常用的 PCR 值含义如下:
| 对比 | PCR4 | PCR7 | PCR12 |
|---|---|---|---|
| TCG 规范名称 | Boot Manager Code and Boot Attempts | Secure Boot Policy | 属于 TCG 留给操作系统使用的 PCR8 至 PCR15,由 UKI 内的 systemd-stub 写入 |
| 记录内容 | 固件启动加载的 EFI 映像。固件直接加载 UKI,因此 PCR4 覆盖 UKI 的全部内容,包括内核、initrd 和内嵌的内核命令行 | Secure Boot 状态与 PK/KEK/db/dbx 密钥库 | 覆盖 UKI 内嵌命令行的手工指定命令行、系统凭据、配置扩展镜像,以及 initrd、微码、设备树附加组件 |
| 本文示例中的值 | 随每次构建镜像内容变化 | 本文没有配置 Secure Boot 密钥 | 96 个 0,本文示例镜像不加载附加内容 |
3、远程证明防护能力以及 PCR 值的作用
通过检查 PCR 值,EC2 远程证明可实现如下防护效果:
| 防护效果 | 实现原理和方式 |
|---|---|
| 启动时候校验 EC2 镜像 | 密钥管理服务 KMS 会检查启动后的 EC2 的 PCR 值与 KMS 策略中的参考值是否一致,一致的返回密钥;不一致的,KMS 拒绝请求;请求中未附带证明文档时,同样被拒绝。 |
| 远程证明的结果只能被发起证明的实例获取 | 远程证明实例发起证明前,在内存中生成一次性 RSA 密钥对,公钥写入证明文档;KMS 校验通过后,用该公钥加密返回结果,放在 CiphertextForRecipient 字段中。即使有人截获了 API 响应,也无法在实例外部解密。 |
| 根文件系统受到远程证明保护 | 文件系统中系统目录的运行期写入不会持久化。本文构建的示例镜像中,整个根目录(含 /etc、/run、/var)的写入保存在内存叠加层中,重启后实例回到初始状态,保证每次启动都能得到与参考值一致的 PCR 值。 本文构建的示例镜像使用 erofs(Enhanced Read-Only File System)作为只读根分区,并用 dm-verity 进行完整性校验。dm-verity 在运行期每次读取数据块时校验哈希,而不只在启动时校验一次;其根哈希写在 UKI(Unified Kernel Image,统一内核镜像)内嵌 initrd 的 /etc/veritytab 文件中。由此形成一条绑定链:erofs 内容 → dm-verity 根哈希 → UKI 内嵌 initrd 中的 /etc/veritytab → UKI 的 PE 哈希 → PCR4。UKI 的任何修改,包括为匹配篡改后的根分区而更新根哈希,都会使 PCR4 与参考值不一致,KMS 随之拒绝请求;只修改根分区而不更新 UKI,则由 dm-verity 在读取数据块时发现并触发内核 panic。 |
| 去除运维人员的交互入口 | 本文构建的示例镜像不包含 OpenSSH Server(无法 SSH 登录)、AWS SSM Agent(无法通过 AWS Systems Manager 登录 Shell 或下发指令)、EC2 Instance Connect(无法通过 AWS EC2 控制台网页 SSH 入口登录)、cloud-init(无法在 EC2 启动时下发 Userdata 脚本)和 update-motd(无法修改登录提示信息及其关联功能),并关闭了 chrony 命令端口(无法在运行实例上执行 chrony 管理命令)。此外,通过内核参数关闭串口控制台登录(无法在 EC2 控制台使用串口方式登录虚拟机 Console)。 |
本文为简化构建过程、方便快速学习理解,仅使用 PCR4 和 PCR12 做校验,不开启 UEFI Secure Boot,也不校验 PCR7。在此配置下,镜像如果被篡改会有如下两种防护效果:
- 攻击者篡改了根分区数据块,但不更新 UKI:dm-verity 读取数据块时发现哈希不一致。由于镜像内核命令行包含
rd.kiwi.verity_options=panic-on-corruption,读取到被篡改的数据块时内核立即 panic,实例无法继续运行。 - 攻击者篡改了根分区,并同步更新了 UKI 中的 dm-verity 根哈希:系统可以启动,但 PCR4 与 KMS 策略中预先写入的参考值不一致,KMS 拒绝发放密钥,防护效果是能开机但拿不到 KMS 管理的密钥和数据。如果希望在这种场景下让镜像无法开机,需要开启 UEFI Secure Boot,并可同时在 KMS 策略中校验 PCR7。
在开启 UEFI Secure Boot 后:该 AMI 启动的实例只执行经 db 密钥签名的 UKI。如果攻击者篡改根分区并更新 UKI,由于无法用 db 私钥对新的 UKI 重新签名,固件会拒绝执行该 UKI,实例无法启动。这样就比之前仅检查 PCR4 和 PCR12 的防护效果更进一步。需要注意,db 签名私钥一旦泄露,攻击者即可对篡改后的 UKI 重新签名,Secure Boot 随之失效,因此签名私钥需要严格保管。
4、支持开启 EC2 远程证明的机型
构建 EC2 远程证明的机型可以是多种机型,但是运行实例必须是支持 NitroTPM 的 Nitro 实例类型,并支持 UEFI 启动模式。本文运行实例使用 m7i.large,构建机使用 m7i.xlarge。构建机只执行镜像构建和离线 PCR 计算,不需要 NitroTPM。
执行如下命令,确认 m7i.large 支持 NitroTPM 和 UEFI:
aws ec2 describe-instance-types --region ap-northeast-1 --instance-types m7i.large \
--query 'InstanceTypes[0].{Type:InstanceType,NitroTpm:NitroTpmSupport,TpmVer:NitroTpmInfo.SupportedVersions,Boot:SupportedBootModes}'
返回结果如下:
{
"Type": "m7i.large",
"NitroTpm": "supported",
"TpmVer": [
"2.0"
],
"Boot": [
"legacy-bios",
"uefi"
]
}
需要注意,NitroTpmSupport=supported 只表示该机型提供 NitroTPM 设备,不能完全代表 nitro-tpm-attest 一定可用。此前在 Ubuntu 环境的测试中发现,部分机型的 TPM NV 索引上限只有 0x800 字节,而 nitro-tpm-attest 需要申请 0x2000 字节的缓冲区来存放约 5 KiB 的证明文档,在这类机型上会以 TPM 错误 0x2d5 失败。m7i.large 已实测可以生成 5208 字节的证明文档。更换机型时,应先在可登录的测试镜像上执行 tpm2_getcap properties-fixed 检查 TPM2_PT_NV_INDEX_MAX,再实际运行一次 nitro-tpm-attest。
5、可证明的 AMI 的要求
EC2 远程证明必须搭配可证明的 AMI 使用。可证明的 AMI 需要满足以下条件:
| 项目 | 标准 |
|---|---|
| 基础操作系统 | Amazon Linux 2023 或 NixOS |
| 处理器架构 | x86_64 或 arm64 |
| TPM 支持 | 注册 AMI 时指定 --tpm-support v2.0 |
| 启动模式 | 必须为 UEFI |
| 系统盘状态 | 不可持久化,实例每次重启都必须回到初始启动状态 |
系统盘不可持久化的要求来自 AWS 文档对“保持可证明状态”的说明:实例的 PCR 值以初始启动状态为准,如果启动后对软件或代码的修改能够跨重启保留,重启后的 PCR 值就会偏离参考值,实例将无法再通过 KMS 的远程证明。
本文构建的示例镜像的实现方式如下:
| 分区或目录 | 文件系统与挂载方式 | 能否持久化写入 |
|---|---|---|
| 分区 1(200 MiB) | vfat 格式的 EFI 系统分区,存放 UKI(EFI/BOOT/BOOTX64.EFI),通过 /etc/fstab 以 ro,nosuid,nodev,noexec,noatime 只读挂载到 /boot/efi |
否 |
| 分区 2(2 GiB) | erofs 只读根文件系统,dm-verity 哈希树存放在同一分区尾部,启动时映射为 /dev/mapper/verityroot |
否 |
/(含 /etc、/var 等全部目录) |
overlayfs 叠加层:下层为分区 2 的只读 erofs,上层为 /run/overlay 下的 tmpfs(默认上限为内存的 50%) |
否,写入保存在内存中,重启后丢失 |
/tmp |
独立的 tmpfs(tmp.mount) |
否 |
镜像描述中 overlayroot_write_partition="false",因此构建时不会创建可写分区,内核命令行中也没有 rd.root.overlay.write 参数,initrd 只会建立临时叠加层。AMI 快照大小为 4 GiB,两个分区之外的约 1.8 GiB 空间未分区,且 oem-resize 已关闭,启动时不会被扩展使用。
注意:业务如需持久化数据,应另外挂载 EBS 数据卷,并用 KMS 发放的密钥加密,例如使用 LUKS 加密数据卷,仅在远程证明通过后才解锁挂载。
6、为何推荐使用 Amazon Linux 2023 作为基础镜像
Amazon Linux 2023 是 AWS 文档中可证明的 AMI 构建流程的参考平台,构建所需的工具链都在官方软件源中提供,无需自行编译:
aws-nitro-tpm-tools:提供nitro-tpm-pcr-compute(构建镜像时计算 PCR 参考值)和nitro-tpm-attest(部署到运行环境时获取签名证明文档)。kiwi-cli、python3-kiwi:KIWI NG(KIWI Next Generation)镜像构建工具,按 XML 镜像描述生成包含 UKI、erofs 和 dm-verity 的原始磁盘镜像。kiwi-image-descriptions-examples:AWS 官方示例镜像描述attestable-image-example,已预置只读根文件系统、去除交互入口和构建时自动计算 PCR 的脚本。
7、基于 Ubuntu 构建可证明的 AMI
如果您希望使用 Ubuntu 24.04 系统,也可以实现完整的 EC2 远程证明功能,但 AWS 尚未面向 Ubuntu 系统提供可直接运行的 NitroTPM 工具二进制包,因此安装部署过程需要从 NitroTPM-Tools 源代码编译,并自行改造 UKI 启动和只读根文件系统。使用 Amazon Linux 2023 省去了这部分适配工作,因此本文以 Amazon Linux 2023 为例。如果您确认需要 Ubuntu 系统,请参考相关文档构建镜像。
下面开始准备开发和部署环境。
二、准备工作 IAM Role 和 KMS 创建
1、创建 IAM Role 角色分别用于镜像构建和生产运行
(1) 独立的两个 IAM 角色
本方案需要两个职责分离的 IAM 角色:构建机角色负责上传快照和注册 AMI,运行实例角色只用于从可证明的 AMI 启动的实例,其 KMS 权限完全来自带 PCR 条件的密钥策略。两个角色都在本节创建,后续章节直接引用。
| 对比 | 构建机角色 | 运行实例角色 |
|---|---|---|
| 角色名称 | nitro-tpm-build-ec2-role |
nitro-tpm-attested-ec2-role |
| 使用位置 | 构建机(标准 Amazon Linux 2023 AMI) | 用可证明的 AMI 启动的实例 |
| 托管策略 | AmazonSSMManagedInstanceCore,用于 Session Manager 登录 |
不挂载任何策略 |
| 内联策略 | 上传 EBS 快照、注册和查询 AMI | 不挂载任何策略 |
(2) 创建构建机角色
在 IAM 控制台,创建一个 EC2 使用的 IAM Role 角色。创建时候,选择挂载 AWS 托管策略 AmazonSSMManagedInstanceCore。然后,再点击附加 Inline 内联策略,粘贴如下策略:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "UploadRawImageToEbsSnapshot",
"Effect": "Allow",
"Action": [
"ebs:StartSnapshot",
"ebs:PutSnapshotBlock",
"ebs:CompleteSnapshot"
],
"Resource": "arn:aws:ec2:*::snapshot/*"
},
{
"Sid": "RegisterAndInspectAmi",
"Effect": "Allow",
"Action": [
"ec2:RegisterImage",
"ec2:DescribeSnapshots",
"ec2:DescribeImages",
"ec2:DescribeImageAttribute"
],
"Resource": "*"
}
]
}
由此使用这个角色作为镜像构建者。
(3) 创建生产环境的运行角色
在 IAM 控制台,创建一个 EC2 使用的 IAM Role 角色。创建时候,不要挂载任何策略。
创建完毕后,将这个运行角色的 ARN 复制下来,后续配置 KMS 会使用。
2、在 KMS 中创建 Customer Managed Key
KMS 客户托管密钥(Customer managed key)是被保护数据的加密密钥,也是远程证明的验证方。现在创建一个新的密钥,并且选择密钥的管理员。
注意:当创建向导进入选择密钥使用者的那一步,不要选择任何角色,直接跳过。后续会专门再编辑密钥使用者策略。
以下步骤在 AWS KMS 控制台完成:
- 打开 AWS KMS 控制台,在右上角将区域切换为 Asia Pacific (Tokyo) ap-northeast-1。
- 在左侧导航栏选择 Customer managed keys,点击 Create key。
- Key type 选择 Symmetric,Key usage 选择 Encrypt and decrypt,点击 Next。
- Alias 填写
amazon-linux-2023-nitro-tpm-test,必须与第三章第 2 节证明脚本中KEY_ID使用的别名一致,点击 Next。 - 在 Define key administrative permissions 页面,选择负责管理该密钥的 IAM 用户或角色,通常是当前登录控制台的身份,点击 Next。
- 在 Define key usage permissions 页面,不要勾选
nitro-tpm-attested-ec2-role或任何其他角色,直接点击 Next。在此页面勾选的角色会获得一条不带任何条件的Allow use of the key授权,实例不经过证明也能使用该密钥,PCR 条件随之失效。 - 在 Review 页面确认无误后,点击 Finish。
由此,创建 KMS Customer managed key 完成,并且当前 Key 会自动生成一条策略。
三、构建可证明的 AMI
1、创建 EC2 构建 AMI 镜像的环境
(1) 启动 EC2 构建机并挂载 IAM Role 角色
在东京区域使用最新的 Amazon Linux 2023 AMI 启动一台构建机,IAM 实例配置文件选择上一步创建的构建者的角色 nitro-tpm-build-ec2-role。AWS 文档要求根卷至少 12 GB,本文使用 m7i.xlarge 和 80 GiB 根卷。构建机通过 SSM Session Manager 登录,以下命令均在构建机上以 ec2-user 身份执行。
注意:构建机使用的 Amazon Linux 2023 标准 AMI 不需要启用 NitroTPM。本文构建机上不存在
/dev/tpm0设备,但这不影响生成镜像时候计算出 PCR 值。
(2) 安装依赖软件包
执行如下命令:
sudo dnf install -y kiwi-cli python3-kiwi kiwi-systemdeps-core python3-poetry-core qemu-img veritysetup erofs-utils git cargo aws-nitro-tpm-tools
(3) 安装 coldsnap
coldsnap 是 AWS Labs 开源的 EBS 快照上传工具,通过 EBS direct API 把原始磁盘镜像直接写为 EBS 快照。执行如下命令:
cd ~
git clone https://github.com/awslabs/coldsnap.git
cd coldsnap
cargo install --locked coldsnap
cd ..
注意:本步骤可能花费 5-10 分钟。
(4) 安装 Amazon Linux 2023 远程证明镜像模板
执行如下命令:
sudo dnf install -y kiwi-image-descriptions-examples
软件安装完成后,接下来修改镜像描述并构建镜像。
2、构建可证明的 AMI 中的远程证明启动脚本
(1) 远程证明脚本回显
前文介绍了 PCR。当开启了 NitroTPM 的 EC2 在启动后,由固件和启动链自动生成 PCR。但是,EC2 和 NitroTPM 不会将当前 PCR 和 KMS 策略中的 PCR 进行对比。这个远程证明的关键一步动作,需要使用可证明 AMI 的 EC2 实例主动调用 nitro-tpm-attest 生成证明文档,并将其提交给 KMS 等校验方,才构成一次完整的远程证明。
nitro-tpm-attest 是 AWS 提供的远程证明发起工具,由 aws-nitro-tpm-tools 软件包提供,安装路径为 /usr/bin/nitro-tpm-attest,Amazon Linux 2023 示例镜像描述会自动将其安装到镜像中。该工具在实例运行期间向 NitroTPM 申请一份签名的证明文档(Attestation Document),文档使用 CBOR(Concise Binary Object Representation)格式编码,并以 COSE(CBOR Object Signing and Encryption)签名,其中包含本次启动的 PCR 值。
为了观测远程证明结果,可增加一个回显脚本。上一步安装的镜像模板去除了 SSH、SSM Agent、EC2 Instance Connect 和 cloud-init,但是因为没有外部登录权限,外部是无法发起远程证明、也无法查看结果。这里通过脚本输出远程证明的验证结果,并做成 systemd 服务固化进镜像。这个结果通过 EC2 的系统控制台打印,为此内核命令行保留了 console=ttyS0。服务输出写入串口控制台后,可以通过 EC2 System Log 读取。本节按这一思路修改镜像描述。
注意:内核参数已经限制 EC2 系统控制台 Console 只能显示打印结果,并不能用于登录。
(2) 复制模板并加入 OpenSSL 软件包
不要直接修改 /usr/share 下的原始文件,先复制一份。执行如下命令:
sudo cp -r /usr/share/kiwi-image-descriptions-examples/al2023/attestable-image-example ~/attest-kms-test
sudo chown -R ec2-user:ec2-user ~/attest-kms-test
mkdir -p ~/attest-kms-test/root/usr/local/bin ~/attest-kms-test/root/etc/systemd/system
运行证明脚本需要 openssl 命令生成 RSA 密钥对并解密 KMS 返回的 CMS 密文。本文的镜像描述 appliance.kiwi 只通过依赖关系带入了 openssl-libs 库,没有安装提供 /usr/bin/openssl 命令的 openssl 软件包。执行如下命令,在 aws-nitro-tpm-tools 之后加入 openssl 软件包:
sed -i 's|<package name="aws-nitro-tpm-tools"/>|<package name="aws-nitro-tpm-tools"/>\n <package name="openssl"/>|' ~/attest-kms-test/appliance.kiwi
grep -n -A1 'aws-nitro-tpm-tools' ~/attest-kms-test/appliance.kiwi
返回结果如下:
61: <package name="aws-nitro-tpm-tools"/>
62- <package name="openssl"/>
软件包加入完成。
(3) 编写发起远程证明的脚本
新建 ~/attest-kms-test/root/usr/local/bin/attest-kms-test.sh,内容如下:
#!/bin/bash
# 开机执行一次 NitroTPM 远程证明,并携带证明文档向 AWS KMS 申请数据密钥
set -uo pipefail
KEY_ID="alias/amazon-linux-2023-nitro-tpm-test"
log() { echo "[ATTEST-TEST] $*"; }
IMDS=http://169.254.169.254
TOKEN=$(curl -s -X PUT "$IMDS/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 300")
REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" "$IMDS/latest/meta-data/placement/region")
WORK=$(mktemp -d)
# 0. 检查依赖命令是否存在
for bin in openssl nitro-tpm-attest aws curl base64; do
command -v "$bin" >/dev/null || { log "RESULT=FAILURE missing command: $bin"; exit 1; }
done
# 1. 在实例内生成一次性 RSA 密钥对
openssl genrsa -out "$WORK/priv.pem" 2048 2>/dev/null || { log "RESULT=FAILURE openssl genrsa failed"; exit 1; }
openssl rsa -in "$WORK/priv.pem" -pubout -outform DER -out "$WORK/pub.der" 2>/dev/null || { log "RESULT=FAILURE openssl rsa failed"; exit 1; }
# 2. 远程证明的核心命令:向 NitroTPM 申请签名的证明文档,公钥被写入文档
nitro-tpm-attest --public-key "$WORK/pub.der" > "$WORK/attestation.cbor" || { log "RESULT=FAILURE nitro-tpm-attest failed"; exit 1; }
[ -s "$WORK/attestation.cbor" ] || { log "RESULT=FAILURE attestation document is empty"; exit 1; }
log "Attestation document generated, size $(stat -c %s "$WORK/attestation.cbor") bytes"
# 3. 携带证明文档调用 KMS,KMS 校验签名并比对 PCR4/PCR12
if OUT=$(aws kms generate-data-key --region "$REGION" --key-id "$KEY_ID" --key-spec AES_256 \
--recipient "KeyEncryptionAlgorithm=RSAES_OAEP_SHA_256,AttestationDocument=$(base64 -w0 "$WORK/attestation.cbor")" \
--query CiphertextForRecipient --output text 2>&1); then
# 4. KMS 用证明文档中的公钥加密返回结果,只有本实例的私钥能解开
echo "$OUT" | base64 -d > "$WORK/recipient.der"
if openssl cms -decrypt -inform DER -in "$WORK/recipient.der" -inkey "$WORK/priv.pem" -out "$WORK/datakey.bin" 2>/dev/null; then
log "RESULT=SUCCESS KMS accepted attestation, data key decrypted locally ($(stat -c %s "$WORK/datakey.bin") bytes)"
else
log "RESULT=PARTIAL KMS accepted attestation, but local CMS decryption failed"
fi
else
log "RESULT=FAILURE $OUT"
fi
rm -rf "$WORK"
注意:以上脚本中,
KEY_ID使用 KMS 别名alias/amazon-linux-2023-nitro-tpm-test,该别名需要与 KMS 中创建的 Customer Key 的别名一致,也可以使用这个密钥的 ARN。
(4) 将发起远程证明的脚本加入 systemd 服务并设置开机启用
新建 ~/attest-kms-test/root/etc/systemd/system/attest-kms-test.service,内容如下:
[Unit]
Description=NitroTPM attestation test against AWS KMS
Wants=network-online.target
After=network-online.target time-sync.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/attest-kms-test.sh
StandardOutput=journal+console
StandardError=journal+console
[Install]
WantedBy=multi-user.target
在 ~/attest-kms-test/config.sh 末尾追加如下两行,KIWI NG 构建时会在镜像根文件系统内执行该脚本:
chmod 0755 /usr/local/bin/attest-kms-test.sh
systemctl enable attest-kms-test.service
脚本和服务配置完成。
3、构建镜像
每次构建使用一个新的目标目录,避免与旧构建产物混淆。执行如下命令:
cd ~
sudo kiwi-ng --color-output --loglevel 0 system build \
--description ~/attest-kms-test \
--target-dir ./image-v3
ls ./image-v3
返回结果如下:
al2023-attestable-image-example.x86_64-1.0.0.changes
al2023-attestable-image-example.x86_64-1.0.0.packages
al2023-attestable-image-example.x86_64-1.0.0.raw
al2023-attestable-image-example.x86_64-1.0.0.verified
build
kiwi.result
kiwi.result.json
pcr_measurements.json
此时屏幕上会打印出 PCR4/PCR7/PCR12 的值,将其复制下来,下一步要使用。另外,PCR4 会随镜像内容变化,PCR7 和 PCR12 保持不变。每次修改镜像描述并重新构建后,都必须用新的 PCR4 更新 KMS 策略。
注意:镜像生成的 PCR12 虽然全为 0,仍然需要写入策略,用于拒绝在启动时追加了内核命令行参数或其他扩展内容的实例。
4、注册为 EC2 AMI
执行如下命令:
SNAPSHOT=$(.cargo/bin/coldsnap upload --wait --workers 16 ./image-v3/al2023*.raw)
echo "Created snapshot: $SNAPSHOT"
aws ec2 register-image --region ap-northeast-1 \
--name "attestable_al2023_kms_test_v3" \
--virtualization-type hvm --boot-mode uefi --architecture x86_64 \
--root-device-name /dev/xvda \
--block-device-mappings DeviceName=/dev/xvda,Ebs={SnapshotId=${SNAPSHOT}} \
--tpm-support v2.0 --ena-support
返回结果如下:
{
"ImageId": "ami-094c46b0bfab94ed9"
}
请将 AMI ID 复制下来,后续用于启动。另外 AMI 名称在同一区域内不能重复,每次重新构建时需要更换名称。或者加版本号的后缀。
注意:以上命令在传递参数时候,需要区分
--boot-mode uefi和--uefi-data是register-image的两个独立参数。前者实现用 UEFI 固件启动即仅校验 PCR4/PCR12,后者决定带入 UEFI 变量存储的数据,这意味着开启 Secure Boot 的 PK、KEK、db、dbx 密钥库,即 PCR7 校验。
5、更新 KMS Customer Key 中密钥 Policy 策略
找到本文前边步骤创建的 Key,不要删除原来的密钥管理员策略,而是增加新的密钥使用者策略如下。方法如下:
- 在 AWS KMS 控制台的 Customer managed keys 列表中,点击别名为
amazon-linux-2023-nitro-tpm-test的密钥,进入详情页。 - 选择 Key policy 选项卡。如果显示的是默认视图,点击 Switch to policy view。
- 点击 Edit,在现有
Statement数组最后一个元素后加一个逗号,粘贴下方的语句,并把<account-id>和 PCR4 替换为实际值。 - 点击 Save changes。
以下是要粘贴的策略内容。以下策略中,替换 <account-id>,确认 EC2 运行环境使用的 IAM Role 的名称,最后替换上一步构建镜像时候生成的 PCR4/PCR12 的值。
{
"Sid": "Allow requests from instances with attested AMI only",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<account-id>:role/nitro-tpm-attested-ec2-role"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:GenerateRandom"
],
"Resource": "*",
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:NitroTPMPCR4": "e19290729e125ee4b006dd1e795f30cb954da4cb1cf6aec4e880a605d28a5c3778aa90637c9adaa34501a3e810e3c6d7",
"kms:RecipientAttestation:NitroTPMPCR12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
}
}
注意:编辑时不要删除前两条语句。删除
Enable IAM User Permissions或密钥管理员语句,可能导致任何身份都无法再管理该密钥。此外,JSON 格式错误或 Principal 中的角色不存在时,控制台会直接报错,策略不会生效。
至此镜像就绪,可以启动新实例了。
四、用可证明的 AMI 启动新的 EC2
1、开机时候选择的机型、AMI、IAM Role注意事项
在 EC2 控制台点击 Launch instances,各项参数按下表选择:
| 参数 | 取值 | 注意事项 |
|---|---|---|
| AMI | My AMIs 中的前文指定的名称 attestable_al2023_kms_test_v3,或者搜索上一步生成的 AMI ID |
必须与 KMS 策略中 PCR4 对应的构建版本一致 |
| 实例类型 | m7i.large | 需要支持 NitroTPM 和 UEFI |
| 密钥对 | 选择 Proceed without a key pair | 镜像中没有 SSH 服务,因此即便您选了密钥,也无法登录 |
| 网络 | 可访问 KMS 的子网 | 公有子网需分配公有 IP;私有子网需要 NAT 网关或 KMS 接口 VPC 终端节点 |
| 安全组 | 出站允许 TCP 443 | 入站规则不需要开放任何端口,只出站,默认走 KMS 公网 API 入口,或者额外配置为 KMS API 内网入口 |
| IAM 实例配置文件 | nitro-tpm-attested-ec2-role |
必须与 KMS 策略的 Principal 一致,注意不要错误选择为构建机的角色 |
| 元数据服务 | 保持启用,IMDSv2 设为 Required | 证明脚本通过 IMDSv2 获取区域和角色凭证,不能关闭元数据服务 |
注意:由于 AMI 已在注册时指定 --tpm-support v2.0 和 --boot-mode uefi,因此在创建实例并启动时,不需要再单独开启 NitroTPM。
2、开机后从 AWS EC2 控制台上查看 System Log 确认远程证明生效方法
以下方法任选一种即可。
(1) 通过 EC2 控制台的 System Log 查看
实例状态变为 Running 后等待 1 到 2 分钟,按以下步骤操作:
- 打开 Amazon EC2 控制台,区域选择 Asia Pacific (Tokyo)。
- 在左侧导航栏选择 Instances,勾选运行实例。
- 依次选择 Actions、Monitor and troubleshoot、Get system log。
System Log 显示的是实例串口控制台的输出。本文构建的镜像的内核命令行包含 console=ttyS0,证明服务的输出会出现在这里;同时内核参数 systemd.getty_auto=false 和 rd.shell=0 关闭了控制台登录,System Log 只能查看,不能用于登录实例。
在 System Log 中查找 attest-kms-test 和 ATTEST-TEST,本文运行实例的日志如下:
Starting attest-kms-test.s…ttestation test against AWS KMS...
......
[ 18.112080] attest-kms-test.sh[2418]: [ATTEST-TEST] Attestation document generated, size 5208 bytes
[ 19.742702] attest-kms-test.sh[2418]: [ATTEST-TEST] RESULT=SUCCESS KMS accepted attestation, data key decrypted locally (32 bytes)
[ OK ] Finished attest-kms-test.s… attestation test against AWS KMS.
各行含义如下:
Attestation document generated, size 5208 bytes:nitro-tpm-attest已从 NitroTPM 取得签名证明文档,文档中包含本次启动的 PCR 值和实例生成的 RSA 公钥。这一步只代表证明文档生成成功,还不代表验证通过。RESULT=SUCCESS KMS accepted attestation:KMS 校验了证明文档的签名,比对 PCR4 和 PCR12 与密钥策略一致,返回了CiphertextForRecipient。这一行是远程证明通过的标志。data key decrypted locally (32 bytes):实例用内存中的私钥解开了 KMS 返回的 CMS 密文,得到 32 字节的 AES-256 数据密钥,证明返回结果只能由该实例本身解开。
证明脚本的每种失败都以 RESULT=FAILURE 开头,常见情况及其含义如下:
| 日志特征 | 失败阶段 | 原因与处理 |
|---|---|---|
RESULT=FAILURE missing command: openssl |
实例本地,未生成证明文档 | 镜像缺少依赖命令,按第三章第 2 节加入软件包并验证 |
RESULT=FAILURE nitro-tpm-attest failed |
实例本地,证明文档生成失败 | 机型 NitroTPM 不可用或 NV 缓冲区不足,按第一章第 4 节核对机型 |
ValidationException ... recipient.attestationDocument |
KMS 参数校验 | 证明文档为空,通常由前序步骤失败导致,本文第一次构建即属于这种情况 |
AccessDeniedException ... GenerateDataKey |
KMS 策略评估 | PCR 不匹配,或角色不在策略 Principal 中;这是镜像未通过远程证明的结果 |
Could not connect to the endpoint URL 或超时 |
网络 | 安全组出站规则、路由或 VPC 终端节点配置有误 |
(2) 用 AWS CLI 在本机查询 System Log 确认远程证明生效的方法
在安装有 AWS CLI 且配置了 AKSK 密钥的环境上执行如下命令:
aws ec2 get-console-output --region ap-northeast-1 --instance-id i-07670ee7f9b7e29a6 \
--latest --output text | grep -E 'ATTEST-TEST|attest-kms-test'
返回结果如下:
Starting attest-kms-test.s…ttestation test against AWS KMS...
[ 18.112080] attest-kms-test.sh[2418]: [ATTEST-TEST] Attestation document generated, size 5208 bytes
[ 19.742702] attest-kms-test.sh[2418]: [ATTEST-TEST] RESULT=SUCCESS KMS accepted attestation, data key decrypted locally (32 bytes)
[ OK ] Finished attest-kms-test.s… attestation test against AWS KMS.
返回 RESULT=SUCCESS 表示远程证明通过,返回 RESULT=FAILURE 时按上一节的表格判断原因。
(3) 在 CloudTrail 中核对 KMS 收到的 PCR 值
CloudTrail 是 AWS 的操作日志审计服务,其中会记录所有 AWS API 的调用,这其中包含对 KMS 的 API 调用,因此可以从 KMS 验证方视角确认 KMS 实际比对的 PCR 值。CloudTrail 事件通常会延迟几分钟才能查到。
在安装有 AWS CLI 且配置了 AKSK 密钥的环境上,执行如下命令:
aws cloudtrail lookup-events --region ap-northeast-1 \
--lookup-attributes AttributeKey=Username,AttributeValue=i-07670ee7f9b7e29a6 \
--max-results 20 --query 'Events[?EventName==`GenerateDataKey`]|[0].CloudTrailEvent' --output text \
| python3 -c 'import json,sys; e=json.loads(sys.stdin.read()); print(json.dumps({k:e.get(k) for k in ["eventName","errorCode","requestParameters","additionalEventData"]}, indent=2))'
lookup-events 按时间倒序返回事件,|[0] 取该实例最近一次的 GenerateDataKey 记录。返回结果如下(节选,账号 ID 和密钥 ID 已替换为占位符,部分全 0 的 PCR 已省略):
{
"eventName": "GenerateDataKey",
"errorCode": null,
"requestParameters": {
"keyId": "arn:aws:kms:ap-northeast-1:<account-id>:key/<key-id>",
"keySpec": "AES_256"
},
"additionalEventData": {
"recipient": {
"attestationDocumentModuleId": "i-07670ee7f9b7e29a6-tpm0000000000000000",
"attestationDocumentNitroTpmPCR0": "6e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec",
"attestationDocumentNitroTpmPCR4": "e19290729e125ee4b006dd1e795f30cb954da4cb1cf6aec4e880a605d28a5c3778aa90637c9adaa34501a3e810e3c6d7",
"attestationDocumentNitroTpmPCR7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35",
"attestationDocumentNitroTpmPCR9": "d254f5b780503f2f020c775a726f2a91f3ec2e9fd0b33cdcfa1408c7ddb4f1c433b1b90745f92b72c9108f1e6f187208",
"attestationDocumentNitroTpmPCR12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
}
}
从这条记录可以确认以下事实:
attestationDocumentModuleId包含实例 IDi-07670ee7f9b7e29a6,说明证明文档来自该实例的 NitroTPM。attestationDocumentNitroTpmPCR4与第三章第 3 节离线计算的 PCR4 完全一致,说明构建时计算的参考值与实例实际启动后的 PCR4 相符。attestationDocumentNitroTpmPCR7与离线计算的 PCR7 一致,印证了该值对应“没有 Secure Boot 密钥”的固定状态,不具备区分镜像的意义。errorCode为null,表示 KMS 接受了该请求。PCR 不匹配时,该字段为AccessDenied。
至此,从镜像构建、PCR 参考值计算、KMS 策略配置到实例开机自动完成远程证明的流程已全部打通。下一章介绍如何将业务应用软件封装进可证明的 AMI。
五、如何将应用软件封装进可证明的 AMI
1、应用软件必须在构建镜像时写入的原因
在 EC2 远程证明方案中,应用软件不在实例启动后安装,而是在构建镜像时由 KIWI NG 写入只读根文件系统,与操作系统一起成为可证明的 AMI 的一部分。可证明的 AMI 的以下三项特性,决定了运行期注入应用的方式既不可行,也不具备证明意义:
- 根文件系统只读且不可持久化:根分区为 erofs,
/为上层是 tmpfs 的 overlayfs,运行期写入的文件在重启后丢失。AWS 文档要求实例重启后必须回到初始启动状态。 - 不存在运行期注入通道:示例镜像已去除 SSH、SSM Agent、EC2 Instance Connect 和 cloud-init,User Data 不会被执行,也无法登录实例安装软件。
- 只有绑定到 PCR 的内容才受证明保护:运行期从外部下载的程序不在 dm-verity 的覆盖范围内,KMS 无法据此判断其是否被篡改。
EC2 远程证明采用操作系统与应用一体化的模型,应用固化在镜像中,PCR4 同时代表操作系统和应用。应用的任何变化都意味着重新构建镜像、得到新的 PCR4,并同步更新 KMS 密钥策略。
2、应用文件计入 PCR4 的绑定链
应用文件通过 dm-verity 根哈希间接计入 PCR4,绑定关系如下:
应用文件(二进制、配置、systemd 单元)
│ KIWI NG 构建时写入根文件系统
▼
erofs 只读根分区(分区 2)
│ veritysetup 计算哈希树
▼
dm-verity 根哈希
│ 写入 initrd 内的 /etc/veritytab
▼
UKI(内核 + initrd + 内嵌命令行)
│ 固件加载 UKI 时扩展哈希
▼
PCR4 ──> 写入 KMS 密钥策略 Condition
本文构建的示例镜像描述中的 edit_boot_install.sh,是在引导程序安装完成后,再执行 nitro-tpm-pcr-compute 命令。此时应用文件已经写入 erofs 包含在文件系统中,因此离线计算得到的当前镜像的 PCR4 值已包含应用软件。
3、KIWI NG 提供的四种封装方式
(1) 四种封装方式的对比
AWS 官方文档列出了四种定制镜像描述的方式,均在 appliance.kiwi 所在的镜像描述目录中完成:
| 对比 | 软件仓库 | 软件包 | 覆盖目录 root/ |
自定义脚本 |
|---|---|---|---|---|
| 配置位置 | <repository> 元素 |
<packages> 元素 |
镜像描述目录下的 root/ |
按 KIWI NG 命名约定放置的脚本,例如 config.sh |
| 作用 | 增加 RPM 软件来源,例如企业自建仓库 | 安装或删除软件包 | 软件包安装完成后,将目录内容原样复制进根文件系统,同名文件被覆盖 | 在构建过程中执行,用于启用服务、设置权限、创建用户等 |
| 适用内容 | 以 RPM 形式发布的业务应用 | 运行时依赖,例如 openssl、python3 |
自研二进制、脚本、配置文件、systemd 单元文件 | 无法通过声明方式完成的配置 |
| 注意事项 | 应固定软件版本,否则两次构建的 PCR4 可能不同 | 同左 | 文件属主与权限需要由脚本修正 | 尽量不在脚本中联网下载或编译,避免构建结果不可复现 |
对于自研应用,推荐以覆盖目录或自建 RPM 仓库为主,以自定义脚本完成服务启用等收尾配置。
(2) 本文示例中的封装实践
本文第三章第 2 节已经完成了一次完整的应用封装,其中 attest-kms-test 即为一个最小化的应用:
- 通过
<packages>元素加入运行依赖openssl。 - 通过覆盖目录写入
root/usr/local/bin/attest-kms-test.sh和root/etc/systemd/system/attest-kms-test.service。 - 通过
config.sh执行chmod和systemctl enable,使其开机自动运行。
正式业务应用沿用相同的方法,将示例脚本和服务替换为实际的应用程序与 systemd 单元即可。
4、正式业务应用的推荐结构
将一个需要使用密钥的业务应用封装进可证明的 AMI 时,建议按如下方式组织:
- 应用本体:以 RPM 软件包或
root/opt/<your-app>/下的二进制文件形式写入 erofs 只读根分区。 - 证明门控服务:一个
Type=oneshot的 systemd 服务,负责执行nitro-tpm-attest并携带证明文档向 KMS 请求密钥。业务服务通过Requires=和After=依赖该门控服务。PCR 不匹配时门控服务状态为failed,业务服务不会启动,但 EC2 实例本身仍然运行,可用于排查。 - 应用的敏感数据:不能以明文形式写入镜像。erofs 不提供加密,任何有权读取该 AMI 快照的身份都能看到镜像内容。可行的做法是在镜像中只存放 AWS KMS 加密后的密文,运行时通过调用 AWS KMS 的解密
DecryptAPI 加Recipient参数解密,或者在证明通过后再从外部获取。 - 持久化数据:另外挂载 EC2 的 EBS 数据卷,并使用 KMS 发放的密钥通过 LUKS 加密,只有远程证明通过后才能解锁和挂载。
5、封装应用前需要规划的问题
封装应用前,需要预先考虑以下问题:
- 应用更新即镜像更新:修改任何应用文件都会改变 PCR4,更新流程为重新构建镜像、注册新 AMI、更新 KMS 密钥策略、替换实例。滚动升级期间,可以在密钥策略的 PCR4 条件中同时写入新旧两个值,待旧实例全部下线后再删除旧值。
- 按环境变化的配置:镜像中没有 cloud-init,无法通过 User Data 传入配置。写入镜像的配置会进入 PCR4,因此不同环境的配置会得到不同的 PCR4。如果在证明通过后从 Amazon S3 或 AWS Systems Manager Parameter Store 读取配置,这部分内容不受 PCR 保护,需要自行校验签名或进行加密。
- 分区容量:本文 v3 镜像的 erofs 根分区只有 2 GiB,较大的应用需要在镜像描述中调整磁盘和根分区大小,该调整本文尚未实测。
- 运行期写入的目录:应用写入
/var等目录的内容保存在内存叠加层中,重启后丢失,同时占用 tmpfs 空间(默认上限为内存的 50%)。需要长期保存的日志和数据,应写入加密数据卷或外部服务。
六、参考文档
Amazon EC2 实例证明概述:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nitrotpm-attestation.html
可证明的 AMI 的概念与要求:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/attestable-ami.html
构建 Amazon Linux 2023 示例镜像描述:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/build-sample-ami.html
为工作负载定制示例镜像描述(软件仓库、软件包、覆盖目录与自定义脚本):
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/customize-sample-ami.html
示例镜像描述的隔离特性(去除 SSH、SSM、cloud-init 等入口):
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/al2023-isolated-compute-recipe.html
为自定义 AMI 计算 PCR 值:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-pcr-compute.html
为证明准备 AWS KMS:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/prepare-attestation-service.html
获取 NitroTPM 证明文档:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/attestation-get-doc.html
与 AWS KMS 集成:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/attestation-attest.html
为 EC2 实例使用 IAM 角色与实例配置文件:
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html
NitroTPM 的 KMS 条件键:
https://docs.aws.amazon.com/kms/latest/developerguide/conditions-nitro-tpm.html
NitroTPM 的实例与 AMI 要求:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html
UEFI Secure Boot 工作原理与 SetupMode:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-uefi-secure-boot-works.html
获取实例控制台输出(System Log):
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/troubleshoot-unreachable-instance.html
coldsnap 源代码仓库:
https://github.com/awslabs/coldsnap
最后修改于 2026-10-26