EC2 远程证明 NitroTPM on Ubuntu 24.04:从源代码编译工具并构建可证明的 AMI
本文介绍了如何构建 Ubuntu 24.04 可证明 AMI。由于 AWS 官方没有提供针对 Ubuntu 系统的 Nitro TPM Tools 二进制包,所以本文从源代码开始编译,并定制 Ubuntu 操作系统模版,最终达到可证明 AMI 的构建标准。最终经测试,确认了与官方给出的 Amazon Linux 2023 的参考镜像 Sample Image 实现了一样的可证明过程。
一、背景
1、Ubuntu 与 Amazon Linux 2023 在工具和可证明的 AMI 构建上的差异
上一篇文章 《EC2远程证明 Nitro TPM on Amazon Linux 2023》 介绍了在 Amazon Linux 2023 上构建可证明的 AMI(Attestable AMI),并以 AWS KMS 作为验证方完成 EC2 实例远程证明的过程。该流程依赖两个工具:
nitro-tpm-attest:在实例运行时生成签名的证明文档;nitro-tpm-pcr-compute:在构建镜像时离线计算参考 PCR(Platform Configuration Register,平台配置寄存器)值。
AWS 文档中可证明的 AMI 的构建方法以 Amazon Linux 2023 和 KIWI NG(KIWI Next Generation)为基础,两个工具由官方软件源中的 aws-nitro-tpm-tools 软件包提供,并有可参考的 Sample Image。AWS 尚未面向 Ubuntu 提供可直接运行的二进制包和示例镜像描述,因此在 Ubuntu 24.04 上需要从 NitroTPM-Tools 源代码编译工具,并使用 Ubuntu 自身的工具链实现只读根文件系统、完整性校验和 UKI(Unified Kernel Image,统一内核镜像)构建。
二者的差异如下:
| 对比 | Amazon Linux 2023 | Ubuntu 24.04 |
|---|---|---|
| NitroTPM 工具来源 | 官方软件源 aws-nitro-tpm-tools 软件包 |
从 NitroTPM-Tools 源代码编译 |
| Rust 编译器 | 不需要 | 需要 Rust 1.92 或更高版本,Ubuntu 软件源最高只提供 1.91,需要通过 rustup 安装 |
| 软件包格式 | RPM,由官方维护 | 自行使用 dpkg-deb 打包为 deb |
| 镜像构建工具 | KIWI NG 与官方示例镜像描述 | debootstrap、initramfs-tools、ukify 与本文编写的脚本 |
| 打开 dm-verity 的组件 | KIWI 的 dracut 模块 | 本文编写的 initramfs-tools 脚本 |
| dm-verity 根哈希的位置 | UKI 内嵌 initrd 的 /etc/veritytab |
UKI 内核命令行中的 verity.roothash= 参数 |
| 内存叠加层 | KIWI 的 overlayroot 机制 | Ubuntu 的 overlayroot 软件包 |
| 磁盘镜像上传 | coldsnap 直接上传为 EBS 快照 | 写入新建的 EBS 卷后创建快照 |
| KMS 策略检查的 PCR | PCR4 与 PCR12 | PCR4 与 PCR12,与 Amazon Linux 2023 版本一致 |
本文验证 Ubuntu 24.04 构建与 Amazon Linux 2023 Sample Image 防护能力相同的可证明的 AMI,并以 AWS KMS 作为验证方完成远程证明。远程证明的工作原理、PCR 含义和 KMS 密钥策略设计,请参考 Amazon Linux 2023 版本文档,本文不再重复。与 Amazon Linux 2023 版本相同,本文 KMS 策略只检查 PCR4 和 PCR12,不启用 UEFI Secure Boot,也不检查 PCR7。
2、本文的测试目标、构建流程与产出物
本文的测试目标如下:
- 在 Ubuntu 24.04 上从源代码编译 NitroTPM-Tools,并打包为 deb 软件包。
- 不使用 Canonical 发布的 AMI 作为镜像基础,而是从最小根文件系统开始构建可证明的 AMI,使其防护能力与 Amazon Linux 2023 示例镜像一致。
- 构建机使用 Canonical 发布的 Ubuntu 24.04.5 LTS AMI(名称为
ubuntu-noble-24.04-amd64-server-20261023),镜像内核为7.0.0-1014-aws,构建机与测试实例的实例类型均为 m7i.large。 - 验证三种结果:批准的镜像通过远程证明;只篡改根分区时实例无法继续运行;篡改根分区并同步更新 UKI 时 KMS 拒绝发放密钥。
整体流程如下:
构建机(Canonical 标准 Ubuntu 24.04 AMI,m7i.large)
rustup 安装 Rust 1.93.0 -> cargo build 编译 NitroTPM-Tools v1.1.2
|
v
dpkg-deb 打包 aws-nitro-tpm-tools 与 nitro-tpm-attest-gate(保存在构建机本地)
|
v
debootstrap 生成最小根文件系统 -> chroot 安装内核、systemd 与本地 deb 软件包
|
v
mkfs.erofs + veritysetup -> ukify 构建 UKI(内核命令行含 dm-verity 根哈希)
nitro-tpm-pcr-compute 计算 PCR4 / PCR12 -> sgdisk 组装 disk.raw
|
v
dd 写入新建 EBS 卷 -> 创建快照 -> register-image --tpm-support v2.0
|
v
测试实例(m7i.large,无任何登录入口)
开机门控服务:nitro-tpm-attest 生成证明文档 -> KMS GenerateDataKey
|
+-- 批准的镜像:KMS 返回 CiphertextForRecipient,受保护服务启动
+-- 只篡改根分区:dm-verity 发现数据块哈希不一致,内核 panic
+-- 篡改根分区并更新 UKI:PCR4 改变,KMS 返回 AccessDeniedException
本文的产出物如下:
| 产出物 | 说明 |
|---|---|
aws-nitro-tpm-tools_1.1.2-1_amd64.deb |
包含 nitro-tpm-attest 与 nitro-tpm-pcr-compute,包名与 Amazon Linux 2023 的软件包一致 |
nitro-tpm-attest-gate_1.0.0-1_all.deb |
包含开机远程证明脚本、受保护服务占位程序、两个 systemd 服务和配置文件 |
disk.raw |
4 GiB 的 GPT 磁盘镜像,包含只放置 UKI 的 ESP 分区和 erofs + dm-verity 根分区 |
ami-04f27bf2212494092 |
由 disk.raw 注册、启用 NitroTPM v2.0 的可证明的 AMI |
alias/ubuntu-2404-nitro-tpm-test |
带 PCR4 与 PCR12 条件的 KMS 客户托管密钥 |
注意:本文是可行性测试,验证的是技术路线能否走通,不是经过生产验证的发布流程。门控服务和受保护服务占位程序只用于展示远程证明结果;构建过程中的软件包来源校验、镜像签名、内核补丁更新流程等生产环境需要的环节,本文没有覆盖。
3、Ubuntu 可证明的 AMI 与 Amazon Linux 2023 示例镜像的能力对标
本文构建的 Ubuntu 镜像逐项对标 Amazon Linux 2023 示例镜像的防护能力,对照如下:
| 对比 | Amazon Linux 2023 示例镜像 | 本文 Ubuntu 24.04 镜像 |
|---|---|---|
| 只读根文件系统 | erofs | erofs,由 mkfs.erofs 生成 |
| 根分区完整性校验 | dm-verity,参数 rd.kiwi.verity_options=panic-on-corruption |
dm-verity,参数 verity.options=panic-on-corruption,由 initrd 脚本转换为 veritysetup open --panic-on-corruption |
| 根哈希受 PCR4 保护 | 根哈希位于 UKI 内嵌 initrd,UKI 计入 PCR4 | 根哈希位于 UKI 内核命令行,UKI 计入 PCR4 |
根目录 / |
overlayfs,上层为内存中的 tmpfs | overlayfs,上层为内存中的 tmpfs,由 overlayroot=tmpfs 实现 |
/tmp |
独立 tmpfs | 独立 tmpfs |
| ESP 分区 | 只放置 UKI,以 ro,nosuid,nodev,noexec,noatime 挂载到 /boot/efi |
相同 |
| OpenSSH Server | 不包含 | 不包含 |
| AWS SSM Agent | 不包含 | 不包含 |
| EC2 Instance Connect | 不包含 | 不包含 |
| cloud-init 与 User Data | 不包含 | 不包含 |
| update-motd | 不包含 | 不包含 |
| chrony 命令端口 | 关闭 | 关闭(cmdport 0),只使用 Amazon Time Sync Service |
| 控制台登录 | rd.shell=0、systemd.getty_auto=false |
panic=0、systemd.getty_auto=false,屏蔽 getty@ 与 serial-getty@,锁定 root 密码 |
| 远程证明结果输出 | 串口控制台 console=ttyS0,通过 EC2 System Log 读取 |
相同 |
磁盘与运行时的挂载布局如下:
disk.raw / EBS 卷(4 GiB,GPT 分区表)
p1 200 MiB vfat ESP:只有 EFI/BOOT/BOOTX64.EFI(UKI),运行时只读挂载到 /boot/efi
p2 2 GiB erofs 根文件系统数据(591101952 字节)+ 同分区内的 dm-verity 哈希树
其余约 1.8 GiB 未分区
运行时
/ overlayfs:下层为 /dev/mapper/verityroot(erofs,只读),上层为 tmpfs
/tmp 独立 tmpfs
由此形成一条与 Amazon Linux 2023 示例镜像相同性质的绑定链:erofs 内容 → dm-verity 根哈希 → UKI 内核命令行中的 verity.roothash= → UKI 的 PE 哈希 → PCR4。Amazon Linux 2023 示例镜像把根哈希写在 UKI 内嵌 initrd 的 /etc/veritytab 中,本文按照 AWS 文档“根哈希存放在内核命令行中”的描述写在内核命令行里;两种位置都在 UKI 内部,都被 PCR4 覆盖。下面开始准备构建环境。
二、准备 Ubuntu 24.04 构建机
1、启动作为 Ubuntu 24.04 构建机的 EC2
构建机只负责编译和打包,不需要 NitroTPM,使用 Canonical 发布的标准 Ubuntu 24.04 AMI 即可。Canonical 通过 Systems Manager 公共参数发布最新 AMI ID,执行如下命令查询东京区域最新的 Ubuntu 24.04 AMI:
aws ssm get-parameters-by-path --region ap-northeast-1 \
--path /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3 \
--query 'Parameters[].[Name,Value]' --output text
返回结果如下:
/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id ami-09123d7cb03420818
执行如下命令确认 AMI 的名称和启动属性:
aws ec2 describe-images --region ap-northeast-1 --image-ids ami-09123d7cb03420818 \
--query 'Images[0].[ImageId,Name,BootMode,TpmSupport,BlockDeviceMappings[0].Ebs.SnapshotId]' --output text
返回结果如下:
ami-09123d7cb03420818 ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-20261023 uefi-preferred None snap-0e801640b17bd8558
TpmSupport 为 None,说明 Canonical 发布的标准 AMI 没有启用 NitroTPM。这不影响构建机编译软件和构建镜像。但是,第五章从最小根文件系统开始构建启用 NitroTPM 的可证明的 AMI,不以该 AMI 作为镜像基础。
构建机使用的 IAM 实例配置文件只需要挂载 AWS 托管策略 AmazonSSMManagedInstanceCore,用于通过 Session Manager 登录。软件包在构建机本地生成并直接安装进镜像,构建机不需要 Amazon S3 权限;第五章第 8 节的 EBS 卷操作在本机执行,构建机也不需要 EC2 权限。安全组不需要任何入站规则,出站放行 TCP 80 和 443,用于访问 Ubuntu 软件源、GitHub、rustup、AWS CLI 安装包下载地址和 Systems Manager。
执行如下命令启动构建机(当然您也可以在 AWS EC2 Console 图形控制台上手工启动这个机器),根卷设为 30 GiB,用于存放 Rust 工具链、编译缓存,以及第五章的根文件系统和磁盘镜像:
aws ec2 run-instances --region ap-northeast-1 \
--image-id ami-09123d7cb03420818 \
--instance-type m7i.large \
--iam-instance-profile Name=<build-instance-profile> \
--network-interfaces 'DeviceIndex=0,SubnetId=<subnet-id>,Groups=<security-group-id>,AssociatePublicIpAddress=true,DeleteOnTermination=true' \
--metadata-options 'HttpTokens=required,HttpEndpoint=enabled,HttpPutResponseHopLimit=1' \
--block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=30,VolumeType=gp3,DeleteOnTermination=true}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=nitro-tpm-ubuntu-build}]' \
--query 'Instances[0].[InstanceId,State.Name]' --output text
返回结果如下:
i-0b84ac9db7e50906e pending
注意:Ubuntu AMI 的根设备名称为
/dev/sda1,与 Amazon Linux 2023 的/dev/xvda不同,--block-device-mappings中的DeviceName必须与之一致,否则会额外挂载一块空卷。
实例启动后,通过 Session Manager 登录,并执行 sudo -iu ubuntu 切换为 ubuntu 用户。本文后续在构建机上的命令均以 ubuntu 用户执行。
2、安装编译与镜像构建依赖软件包
nitro-tpm-attest 通过 tss-esapi 调用 TPM2 软件栈(TPM2 Software Stack,TSS),源代码中的 build.rs 要求 TSS 版本不低于 4.0.0。执行如下命令安装编译依赖:
sudo apt-get update
sudo apt-get install -y \
build-essential pkg-config git curl \
libtss2-dev libssl-dev tpm2-tools dpkg-dev
pkg-config --modversion tss2-esys
dpkg -l libtss2-dev gcc pkg-config | awk '/^ii/{print $2, $3}'
返回结果如下:
4.0.1
gcc 4:13.2.0-7ubuntu1
libtss2-dev:amd64 4.0.1-7.1ubuntu5.1
pkg-config:amd64 1.8.1-2build1
Ubuntu 24.04 提供的 libtss2-dev 为 4.0.1,满足不低于 4.0.0 的要求,不需要从源代码编译 tpm2-tss。dpkg-dev 提供第三章打包使用的 dpkg-shlibdeps。
第五章构建镜像还需要以下工具:debootstrap 生成最小根文件系统,erofs-utils 提供 mkfs.erofs,cryptsetup-bin 提供 veritysetup,systemd-ukify 与 systemd-boot-efi 构建 UKI,gdisk 提供 sgdisk,dosfstools 提供 mkfs.vfat,unzip 用于解压第五章第 2 节下载的 AWS CLI v2 安装包。Canonical 发布的标准 Ubuntu 24.04 AMI 没有预装 unzip。执行如下命令安装:
sudo apt-get install -y debootstrap erofs-utils cryptsetup-bin systemd-ukify systemd-boot-efi gdisk dosfstools unzip
dpkg -l debootstrap erofs-utils cryptsetup-bin systemd-ukify systemd-boot-efi gdisk dosfstools unzip | awk '/^ii/{print $2, $3}'
返回结果如下:
cryptsetup-bin 2:2.7.0-1ubuntu4.2
debootstrap 1.0.134ubuntu2
dosfstools 4.2-1.1build1
erofs-utils 1.7.1-1build2
gdisk 1.0.10-1build1
systemd-boot-efi:amd64 255.4-1ubuntu8.18
systemd-ukify 255.4-1ubuntu8.18
unzip 6.0-28ubuntu4.1
systemd-boot-efi 提供 UKI 使用的 EFI stub,ukify 与 stub 的版本均为 Ubuntu 24.04 的 systemd 255。
3、安装满足版本要求的 Rust 工具链
首先查看 Ubuntu 软件源提供的 Rust 编译器版本。执行如下命令:
apt-cache search --names-only '^rustc(-1\.[0-9]+)?$' | sort -V
apt-cache policy rustc-1.91 | sed -n '1,3p'
返回结果如下:
rustc - Rust systems programming language
rustc-1.74 - Rust systems programming language
rustc-1.76 - Rust systems programming language
rustc-1.77 - Rust systems programming language
rustc-1.78 - Rust systems programming language
rustc-1.79 - Rust systems programming language
rustc-1.80 - Rust systems programming language
rustc-1.81 - Rust systems programming language
rustc-1.82 - Rust systems programming language
rustc-1.83 - Rust systems programming language
rustc-1.84 - Rust systems programming language
rustc-1.85 - Rust systems programming language
rustc-1.89 - Rust systems programming language
rustc-1.91 - Rust systems programming language
rustc-1.91:
Installed: (none)
Candidate: 1.91.1+dfsg~24.04-0ubuntu0.24.04.3
Ubuntu 软件源中版本最高的是 rustc-1.91。使用该版本编译 NitroTPM-Tools v1.1.2 进行验证,执行如下命令:
sudo apt-get install -y rustc-1.91 cargo-1.91
git clone --branch v1.1.2 --depth 1 https://github.com/aws/NitroTPM-Tools.git /tmp/ntt-191
cd /tmp/ntt-191
RUSTC=rustc-1.91 cargo-1.91 build --release --locked --bins
返回结果如下:
Downloaded thiserror-impl v2.0.18
Downloaded thiserror v2.0.18
error: rustc 1.91.1 is not supported by the following package:
aws-nitro-enclaves-nsm-api@0.5.1 requires rustc 1.92
依赖库 aws-nitro-enclaves-nsm-api 0.5.1 要求 Rust 不低于 1.92,Ubuntu 软件源中的编译器无法完成编译。NitroTPM-Tools 的 CHANGELOG 记录 nitro-tpm-attest 1.0.2 起更新到 Rust 1.93,上游持续集成(CI)也以 Rust 1.93 作为最低测试版本,因此本文通过 rustup 安装固定版本 1.93.0。执行如下命令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain 1.93.0
. "$HOME/.cargo/env"
rustc --version
cargo --version
返回结果如下:
rustc 1.93.0 (254b59607 2026-01-19)
cargo 1.93.0 (083ac5135 2025-12-15)
注意:rustup 安装在
ubuntu用户的~/.cargo与~/.rustup目录下,不会替换系统的rustc软件包。新开的终端需要重新执行. "$HOME/.cargo/env"才能找到cargo。验证用的rustc-1.91软件包可以执行sudo apt-get remove -y rustc-1.91 cargo-1.91卸载。
构建环境准备完成,接下来编译源代码。
三、从源代码编译并打包 NitroTPM-Tools
由于最终运行的生产环境上不建议安装开发构建工具,因此我们将在构建机上将编译好的 Nitro TPM Tools 打包为二进制的 deb 格式,这样可以复制到操作系统版本相同的生产环境上直接安装。
1、获取指定版本的源代码
为了让每次构建的结果可追溯,应检出发布标签而不是直接使用主分支。NitroTPM-Tools 的 v1.1.2 标签对应 nitro-tpm-pcr-compute 1.1.2 与 nitro-tpm-attest 1.0.3,与 Amazon Linux 2023 软件源中的 aws-nitro-tpm-tools 1.1.2 版本一致。执行如下命令:
git clone --branch v1.1.2 --depth 1 https://github.com/aws/NitroTPM-Tools.git ~/NitroTPM-Tools
cd ~/NitroTPM-Tools
git log -1 --format='%H %cd'
返回结果如下:
828dfe8e8f237cfc4657d93b49751637d64a9df4 Fri May 22 09:20:09 2026 +0000
请记录该提交 ID,后续发布软件包时作为源代码版本的依据。
2、编译 nitro-tpm-attest 与 nitro-tpm-pcr-compute
使用 --locked 参数要求 Cargo 严格按照仓库中的 Cargo.lock 解析依赖版本,避免不同时间编译时依赖版本漂移;--bins 只编译两个命令行程序,不编译示例程序。执行如下命令:
cd ~/NitroTPM-Tools
cargo build --release --locked --bins
返回结果如下(节选最后三行):
Compiling uuid v1.23.1
Compiling nitro-tpm-pcr-compute v1.1.2 (/home/ubuntu/NitroTPM-Tools/nitro-tpm-pcr-compute)
Finished `release` profile [optimized] target(s) in 2m 34s
在 m7i.large(2 vCPU、8 GiB 内存)上首次编译约需 2 分 34 秒,其中包括下载依赖库的时间。
3、检查编译产物与动态库依赖
执行如下命令查看编译产物:
ls -l target/release/nitro-tpm-attest target/release/nitro-tpm-pcr-compute
file target/release/nitro-tpm-attest target/release/nitro-tpm-pcr-compute
返回结果如下:
-rwxrwxr-x 2 ubuntu ubuntu 4425920 Oct 26 10:40 target/release/nitro-tpm-attest
-rwxrwxr-x 2 ubuntu ubuntu 4121120 Oct 26 10:40 target/release/nitro-tpm-pcr-compute
target/release/nitro-tpm-attest: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=3a3f0ae8b22c5eadd53c5e1df64830f7434404b6, not stripped
target/release/nitro-tpm-pcr-compute: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=63bf93a586bed99fa009733c1757a93057c38824, not stripped
执行如下命令查看两个程序依赖的动态库:
ldd target/release/nitro-tpm-attest
ldd target/release/nitro-tpm-pcr-compute
返回结果如下:
linux-vdso.so.1 (0x0000796df8636000)
libtss2-esys.so.0 => /lib/x86_64-linux-gnu/libtss2-esys.so.0 (0x0000796df8178000)
libtss2-tctildr.so.0 => /lib/x86_64-linux-gnu/libtss2-tctildr.so.0 (0x0000796df861d000)
libtss2-mu.so.0 => /lib/x86_64-linux-gnu/libtss2-mu.so.0 (0x0000796df85d8000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x0000796df814a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000796df7e00000)
/lib64/ld-linux-x86-64.so.2 (0x0000796df8638000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x0000796df7800000)
libtss2-sys.so.1 => /lib/x86_64-linux-gnu/libtss2-sys.so.1 (0x0000796df85b5000)
linux-vdso.so.1 (0x000078e7d3caf000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x000078e7d3c72000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000078e7d3400000)
/lib64/ld-linux-x86-64.so.2 (0x000078e7d3cb1000)
nitro-tpm-attest 依赖 Ubuntu 的 libtss2 系列动态库,目标实例必须安装这些库;nitro-tpm-pcr-compute 只依赖 C 运行库,可在任意 Ubuntu 24.04 上运行。本章第 5 节打包时由 dpkg-shlibdeps 根据这些依赖自动生成 deb 软件包的 Depends 字段。
两个程序都不支持 --version 参数,执行 --help 检查程序可以正常运行:
./target/release/nitro-tpm-attest --help
./target/release/nitro-tpm-pcr-compute --help
返回结果如下:
Request a TPM attestation document from NitroTPM
Usage: nitro-tpm-attest [OPTIONS]
Options:
-u, --user-data <USER_DATA> Path of user data to optional include
-n, --nonce <NONCE> Path of a nonce to optional include
-p, --public-key <PUBLIC_KEY> Path of a public key to optional include
-h, --help Print help
Precompute selected TPM PCRs of an unified kernel image (UKI)
Usage: nitro-tpm-pcr-compute [OPTIONS]
Options:
-i, --image <IMAGE>
Path of an EFI image file
When multiple images are provided, the argument order has to match the load order.
--PK <PLATFORM_KEY>
Path of the platform key (PK) database file
--KEK <KEY_EXCHANGE_KEY>
Path of the key exchange key (KEK) database file
--db <SIGNATURE_DATABASE>
Path of the signature (db) database file
--dbx <SIGNATURE_DENYLIST_DATABASE>
Path of the signature denylist (dbx) database file
-h, --help
Print help (see a summary with '-h')
编译完成,接下来把编译产物打包为 deb 软件包。
4、准备软件包目录结构
为了便于在生产环境上最快速部署安装,现在将以上构建好的程序打包为 Debian 和 Ubuntu 使用 dpkg 包管理软件安装的 deb 包。
deb 软件包由 DEBIAN/control 等控制文件和按安装路径排列的文件组成。本文使用 Debian 自带的 dpkg-deb 直接打包,不需要安装额外的打包工具。软件包为 aws-nitro-tpm-tools,版本号为上游版本 1.1.2 加打包修订号 1。执行如下命令创建打包目录,复制去除调试符号的二进制文件和许可证文件:
umask 022
cd ~/NitroTPM-Tools
PKG=aws-nitro-tpm-tools
VER=1.1.2
REV=1
ARCH=$(dpkg --print-architecture)
STAGE=~/pkgbuild/${PKG}_${VER}-${REV}_${ARCH}
mkdir -p "$STAGE/DEBIAN" "$STAGE/usr/bin" "$STAGE/usr/share/doc/$PKG"
install -m 0755 -s target/release/nitro-tpm-attest target/release/nitro-tpm-pcr-compute "$STAGE/usr/bin/"
install -m 0644 LICENSE NOTICE README.md CHANGELOG.md "$STAGE/usr/share/doc/$PKG/"
install -m 0644 LICENSES/APACHEv2-LICENSE LICENSES/THIRD_PARTY_LICENSES_RUST_CRATES.html "$STAGE/usr/share/doc/$PKG/"
ls -l "$STAGE/usr/bin"
返回结果如下:
total 6868
-rwxr-xr-x 1 ubuntu ubuntu 3865160 Oct 26 10:43 nitro-tpm-attest
-rwxr-xr-x 1 ubuntu ubuntu 3164160 Oct 26 10:43 nitro-tpm-pcr-compute
install -s 去除调试符号后,两个程序分别从 4.4 MB 和 4.1 MB 缩小到 3.9 MB 和 3.2 MB。
注意:打包前必须执行
umask 022。Ubuntu 普通用户的默认 umask 为002,新建的目录权限为775,打包后安装到系统目录会使同组用户获得写权限。
5、生成依赖声明与 control 文件
dpkg-shlibdeps 根据二进制文件链接的动态库,查询提供这些库的软件包,生成带最低版本的依赖声明。该工具要求当前目录下存在 debian/control 文件,因此先生成一个临时文件。执行如下命令:
mkdir -p ~/pkgbuild/debian
printf 'Source: %s\n\nPackage: %s\nArchitecture: any\n' "$PKG" "$PKG" > ~/pkgbuild/debian/control
DEPS=$(cd ~/pkgbuild && dpkg-shlibdeps -O "$STAGE/usr/bin/nitro-tpm-attest" "$STAGE/usr/bin/nitro-tpm-pcr-compute" 2>/dev/null | sed 's/^shlibs:Depends=//')
rm -rf ~/pkgbuild/debian
echo "$DEPS"
返回结果如下:
libc6 (>= 2.38), libgcc-s1 (>= 4.2), libtss2-esys-3.0.2-0t64 (>= 2.3.1), libtss2-mu-4.0.1-0t64 (>= 3.0.1), libtss2-tctildr0t64 (>= 3.0.1)
执行如下命令生成 DEBIAN/control 文件,其中 Maintainer 请替换为实际维护者:
cat > "$STAGE/DEBIAN/control" <<EOF
Package: $PKG
Version: $VER-$REV
Architecture: $ARCH
Maintainer: <maintainer-name> <maintainer-email>
Installed-Size: $(du -sk "$STAGE/usr" | cut -f1)
Depends: $DEPS
Section: utils
Priority: optional
Homepage: https://github.com/aws/NitroTPM-Tools
Description: NitroTPM attestation tools built from source for Ubuntu
nitro-tpm-attest retrieves a signed attestation document from NitroTPM.
nitro-tpm-pcr-compute precomputes PCR4, PCR7 and PCR12 values of a UKI.
Built from aws/NitroTPM-Tools tag v$VER.
EOF
6、构建并检查 deb 软件包
执行如下命令构建软件包。--root-owner-group 参数使包内文件的属主统一为 root:root,不需要以 root 身份打包:
dpkg-deb --build --root-owner-group "$STAGE" ~/pkgbuild/
ls -l ~/pkgbuild/*.deb
返回结果如下:
dpkg-deb: building package 'aws-nitro-tpm-tools' in '/home/ubuntu/pkgbuild//aws-nitro-tpm-tools_1.1.2-1_amd64.deb'.
-rw-r--r-- 1 ubuntu ubuntu 1818672 Oct 26 10:43 /home/ubuntu/pkgbuild/aws-nitro-tpm-tools_1.1.2-1_amd64.deb
执行如下命令检查软件包的控制信息和文件清单:
dpkg-deb --info ~/pkgbuild/aws-nitro-tpm-tools_1.1.2-1_amd64.deb
dpkg-deb --contents ~/pkgbuild/aws-nitro-tpm-tools_1.1.2-1_amd64.deb
返回结果如下:
new Debian package, version 2.0.
size 1818672 bytes: control archive=479 bytes.
632 bytes, 13 lines control
Package: aws-nitro-tpm-tools
Version: 1.1.2-1
Architecture: amd64
Maintainer: <maintainer-name> <maintainer-email>
Installed-Size: 7296
Depends: libc6 (>= 2.38), libgcc-s1 (>= 4.2), libtss2-esys-3.0.2-0t64 (>= 2.3.1), libtss2-mu-4.0.1-0t64 (>= 3.0.1), libtss2-tctildr0t64 (>= 3.0.1)
Section: utils
Priority: optional
Homepage: https://github.com/aws/NitroTPM-Tools
Description: NitroTPM attestation tools built from source for Ubuntu
nitro-tpm-attest retrieves a signed attestation document from NitroTPM.
nitro-tpm-pcr-compute precomputes PCR4, PCR7 and PCR12 values of a UKI.
Built from aws/NitroTPM-Tools tag v1.1.2.
drwxr-xr-x root/root 0 2026-10-26 10:43 ./
drwxr-xr-x root/root 0 2026-10-26 10:43 ./usr/
drwxr-xr-x root/root 0 2026-10-26 10:43 ./usr/bin/
-rwxr-xr-x root/root 3865160 2026-10-26 10:43 ./usr/bin/nitro-tpm-attest
-rwxr-xr-x root/root 3164160 2026-10-26 10:43 ./usr/bin/nitro-tpm-pcr-compute
drwxr-xr-x root/root 0 2026-10-26 10:43 ./usr/share/
drwxr-xr-x root/root 0 2026-10-26 10:43 ./usr/share/doc/
drwxr-xr-x root/root 0 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/
-rw-r--r-- root/root 10141 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/APACHEv2-LICENSE
-rw-r--r-- root/root 1176 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/CHANGELOG.md
-rw-r--r-- root/root 226 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/LICENSE
-rw-r--r-- root/root 67 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/NOTICE
-rw-r--r-- root/root 1081 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/README.md
-rw-r--r-- root/root 385585 2026-10-26 10:43 ./usr/share/doc/aws-nitro-tpm-tools/THIRD_PARTY_LICENSES_RUST_CRATES.html
以上两个程序安装到 /usr/bin,与前文在 Amazon Linux 2023 实现 EC2 Attestation 可证明过程的软件的安装路径一致,因此 Amazon Linux 2023 版本文档中的命令脚本可以直接复用。
执行如下命令在构建机上安装软件包,并确认命令路径:
sudo apt-get install -y ~/pkgbuild/aws-nitro-tpm-tools_1.1.2-1_amd64.deb
command -v nitro-tpm-attest nitro-tpm-pcr-compute
返回结果如下:
Selecting previously unselected package aws-nitro-tpm-tools.
Unpacking aws-nitro-tpm-tools (1.1.2-1) ...
Setting up aws-nitro-tpm-tools (1.1.2-1) ...
/usr/bin/nitro-tpm-attest
/usr/bin/nitro-tpm-pcr-compute
构建机上的 nitro-tpm-pcr-compute 在第五章第 6 节用于离线计算 UKI 的参考 PCR 值;构建机没有 NitroTPM,nitro-tpm-attest 只在测试实例上运行。
aws-nitro-tpm-tools 软件包构建完成,接下来编写开机时执行的远程证明脚本与服务门控脚本,并将其打包。
四、编写远程证明脚本与服务门控脚本并再次打包
上文是构建标准的 Nitro TPM 工具,但并未加入执行远程证明的脚本、还缺少业务应用启动的门控脚本。这一章节完成这两个脚本,并重新进行 deb 打包。
1、编写远程证明脚本
执行如下命令设置变量并创建打包目录:
umask 022
PKG=nitro-tpm-attest-gate
VER=1.0.0
REV=1
STAGE=~/gatebuild/${PKG}_${VER}-${REV}_all
mkdir -p "$STAGE/DEBIAN" "$STAGE/usr/sbin" "$STAGE/usr/lib/systemd/system" "$STAGE/etc"
新建 $STAGE/usr/sbin/nitro-tpm-attest-gate,即远程证明门控脚本。在 Ubuntu 系统上本脚本的流程与 Amazon Linux 2023 操作系统版本的 attest-kms-test.sh 一致;成功后写入 /run/nitro-tpm-attest-gate/pass 标志文件,失败时以非零状态退出:
#!/bin/bash
# 开机执行一次 NitroTPM 远程证明,并携带证明文档向 AWS KMS 申请数据密钥
set -uo pipefail
[ -r /etc/nitro-tpm-attest-gate.conf ] && . /etc/nitro-tpm-attest-gate.conf
KEY_ID="${KEY_ID:-}"
STATE_DIR=/run/nitro-tpm-attest-gate
log() { echo "[ATTEST-TEST] $*"; }
fail() { log "RESULT=FAILURE $*"; exit 1; }
rm -rf "$STATE_DIR"
mkdir -p -m 0700 "$STATE_DIR"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
[ -n "$KEY_ID" ] || fail "KEY_ID is not set in /etc/nitro-tpm-attest-gate.conf"
# 0. 检查依赖命令是否存在
for bin in openssl nitro-tpm-attest aws curl base64; do
command -v "$bin" >/dev/null || fail "missing command: $bin"
done
# 通过 IMDSv2 获取区域
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")
[ -n "$REGION" ] || fail "cannot read region from IMDSv2"
# 1. 在实例内生成一次性 RSA 密钥对
openssl genrsa -out "$WORK/priv.pem" 2048 2>/dev/null || fail "openssl genrsa failed"
openssl rsa -in "$WORK/priv.pem" -pubout -outform DER -out "$WORK/pub.der" 2>/dev/null || fail "openssl rsa failed"
# 2. 远程证明的核心命令:向 NitroTPM 申请签名的证明文档,公钥被写入文档
nitro-tpm-attest --public-key "$WORK/pub.der" > "$WORK/attestation.cbor" || fail "nitro-tpm-attest failed"
[ -s "$WORK/attestation.cbor" ] || fail "attestation document is empty"
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
fail "$OUT"
fi
# 4. KMS 用证明文档中的公钥加密返回结果,只有本实例的私钥能解开
echo "$OUT" | base64 -d > "$WORK/recipient.der"
openssl cms -decrypt -inform DER -in "$WORK/recipient.der" -inkey "$WORK/priv.pem" -out "$WORK/datakey.bin" 2>/dev/null \
|| fail "KMS accepted attestation, but local CMS decryption failed"
touch "$STATE_DIR/pass"
log "RESULT=SUCCESS KMS accepted attestation, data key decrypted locally ($(stat -c %s "$WORK/datakey.bin") bytes)"
原理讲解:
- 以上脚本中执行远程证明的命令是注释标为第 2 步的
nitro-tpm-attest --public-key,它向 NitroTPM 申请签名的证明文档; - 注释标为第 3 步的命令将证明文档提交给 AWS KMS,由 KMS 校验签名并比对密钥策略中的 PCR4 和 PCR12;
- 脚本在 KMS 返回结果并在本地解密成功后才写入标志文件,任何一步失败都会输出
RESULT=FAILURE并以非零状态退出。
新建配置文件 $STAGE/etc/nitro-tpm-attest-gate.conf,写入做远程证明的 KMS 密钥别名。当前我们还没有在 AWS KMS 密钥管理服务中创建相应密钥,因此这里需要记录这个名字,后续步骤创建密钥时必须使用和脚本中一致的名字。
密钥别名定义如下:
# AWS KMS 客户托管密钥的别名或 ARN
KEY_ID="alias/ubuntu-2404-nitro-tpm-test"
执行如下命令设置远程证明脚本的执行权限:
chmod 0755 "$STAGE/usr/sbin/nitro-tpm-attest-gate"
远程证明脚本与配置文件编写完成,接下来编写服务门控脚本。
2、编写启动服务的门控脚本
门控的实现分为三部分:门控服务在开机时运行远程证明脚本;受保护服务通过 Requires= 与 After= 依赖门控服务,并通过 ConditionPathExists= 要求标志文件存在;受保护业务程序本身只在上述条件全部满足时才会被 systemd 启动。远程证明失败时,门控服务以失败状态结束,标志文件不存在,受保护服务不会启动。
新建 $STAGE/usr/sbin/nitro-tpm-protected-workload,作为受保护业务的占位程序。实际业务中,将其替换为应用的启动命令:
#!/bin/bash
# 受保护业务的占位程序,正式环境替换为实际应用的启动命令
echo "[PROTECTED-WORKLOAD] STARTED after remote attestation"
新建 $STAGE/usr/lib/systemd/system/nitro-tpm-attest-gate.service,即门控服务。输出同时写入日志和串口控制台,以便通过 EC2 System Log 查看:
[Unit]
Description=NitroTPM attestation gate against AWS KMS
Wants=network-online.target
After=network-online.target time-sync.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nitro-tpm-attest-gate
TimeoutStartSec=180
StandardOutput=journal+console
StandardError=journal+console
[Install]
WantedBy=multi-user.target
新建 $STAGE/usr/lib/systemd/system/nitro-tpm-protected-workload.service,即受保护服务。该服务通过 Requires= 和 After= 依赖门控服务,并要求标志文件存在:
[Unit]
Description=Protected workload gated by NitroTPM attestation
Requires=nitro-tpm-attest-gate.service
After=nitro-tpm-attest-gate.service
ConditionPathExists=/run/nitro-tpm-attest-gate/pass
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nitro-tpm-protected-workload
StandardOutput=journal+console
StandardError=journal+console
[Install]
WantedBy=multi-user.target
执行如下命令设置受保护业务程序的执行权限:
chmod 0755 "$STAGE/usr/sbin/nitro-tpm-protected-workload"
两个脚本与两个服务文件编写完成,接下来重新打包。
3、加入门控脚本后再次打包 deb 软件包
回到上一个章节的打包 deb 的过程,新加入脚本。
第 1、2 节编写的文件仍位于 $STAGE 目录,本节在同一目录中增加控制文件并打包。执行如下命令编写控制文件:
echo "/etc/nitro-tpm-attest-gate.conf" > "$STAGE/DEBIAN/conffiles"
cat > "$STAGE/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
systemctl daemon-reload || true
systemctl enable nitro-tpm-attest-gate.service nitro-tpm-protected-workload.service || true
fi
EOF
cat > "$STAGE/DEBIAN/prerm" <<'EOF'
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
systemctl disable nitro-tpm-attest-gate.service nitro-tpm-protected-workload.service || true
fi
EOF
cat > "$STAGE/DEBIAN/postrm" <<'EOF'
#!/bin/sh
set -e
systemctl daemon-reload || true
EOF
chmod 0755 "$STAGE/DEBIAN/postinst" "$STAGE/DEBIAN/prerm" "$STAGE/DEBIAN/postrm"
cat > "$STAGE/DEBIAN/control" <<EOF
Package: $PKG
Version: $VER-$REV
Architecture: all
Maintainer: <maintainer-name> <maintainer-email>
Installed-Size: $(du -sk "$STAGE/usr" "$STAGE/etc" | awk '{s+=$1} END {print s}')
Depends: aws-nitro-tpm-tools (>= 1.1.2), openssl, curl, coreutils
Section: admin
Priority: optional
Description: NitroTPM remote attestation gate for Ubuntu
Requests a NitroTPM attestation document at boot, submits it to AWS KMS
GenerateDataKey with the Recipient parameter, and starts the protected
workload only when KMS accepts the attestation.
EOF
以上配置文件中,一些关键动作的定义如下:
conffiles:把配置文件声明为配置文件,升级软件包时保留管理员的修改;postinst:即 post-installation,安装后要做的动作,即在安装后启用两个服务;prerm:即 pre-remove,在卸载前要做的动作,即需要禁用服务;postrm:即 post-remove 在卸载后要做的动作,即重新加载 systemd 配置。
执行如下命令构建软件包并检查:
find "$STAGE" -type f | sed "s|$STAGE||" | sort
dpkg-deb --build --root-owner-group "$STAGE" ~/gatebuild/
dpkg-deb --contents ~/gatebuild/nitro-tpm-attest-gate_1.0.0-1_all.deb
返回结果如下:
/DEBIAN/conffiles
/DEBIAN/control
/DEBIAN/postinst
/DEBIAN/postrm
/DEBIAN/prerm
/etc/nitro-tpm-attest-gate.conf
/usr/lib/systemd/system/nitro-tpm-attest-gate.service
/usr/lib/systemd/system/nitro-tpm-protected-workload.service
/usr/sbin/nitro-tpm-attest-gate
/usr/sbin/nitro-tpm-protected-workload
dpkg-deb: building package 'nitro-tpm-attest-gate' in '/home/ubuntu/gatebuild//nitro-tpm-attest-gate_1.0.0-1_all.deb'.
drwxr-xr-x root/root 0 2026-10-26 11:06 ./
drwxr-xr-x root/root 0 2026-10-26 11:06 ./etc/
-rw-r--r-- root/root 87 2026-10-26 11:06 ./etc/nitro-tpm-attest-gate.conf
drwxr-xr-x root/root 0 2026-10-26 11:06 ./usr/
drwxr-xr-x root/root 0 2026-10-26 11:06 ./usr/lib/
drwxr-xr-x root/root 0 2026-10-26 11:06 ./usr/lib/systemd/
drwxr-xr-x root/root 0 2026-10-26 11:06 ./usr/lib/systemd/system/
-rw-r--r-- root/root 339 2026-10-26 11:06 ./usr/lib/systemd/system/nitro-tpm-attest-gate.service
-rw-r--r-- root/root 387 2026-10-26 11:06 ./usr/lib/systemd/system/nitro-tpm-protected-workload.service
drwxr-xr-x root/root 0 2026-10-26 11:06 ./usr/sbin/
-rwxr-xr-x root/root 2449 2026-10-26 11:06 ./usr/sbin/nitro-tpm-attest-gate
-rwxr-xr-x root/root 157 2026-10-26 11:06 ./usr/sbin/nitro-tpm-protected-workload
门控软件包保存在构建机的 ~/gatebuild 目录下,与 ~/pkgbuild 下的 aws-nitro-tpm-tools 软件包一起,在第五章第 2 节直接安装进镜像,不需要上传到其他位置。两个软件包都已构建完成,接下来构建可证明的 Ubuntu AMI。
五、构建可证明的 Ubuntu AMI
可证明的 AMI 需要尽可能小的对外暴露,根据官方推荐的参考镜像,其中不建议安装任何可远程登录的工具,以隔离对生产环境的修改。因此本章节先重构 Ubuntu 系统镜像,再另存为可启动的 AMI,然后用这个 AMI 启动 EC2。
1、使用 debootstrap 生成最小根文件系统并安装软件包
Canonical 发布的 Ubuntu AMI 预装了 snapd、以 snap 形式运行的 SSM Agent、cloud-init 和 OpenSSH Server,根文件系统为可写的 ext4,并通过 shim 与 GRUB 启动。在这样的镜像上逐个卸载交互入口,容易遗漏依赖、残留配置,存在操作系统还有对外暴露入口的隐患。
本文改用 debootstrap 从最小根文件系统开始构建,只安装运行所需的软件包,软件包清单本身就是镜像内容的边界,作用相当于 KIWI NG 镜像描述中的软件包列表。执行如下命令生成 Ubuntu 24.04(代号 noble)的最小根文件系统:
export DEBIAN_FRONTEND=noninteractive
WORK=/home/ubuntu/attestable-image
ROOTFS=$WORK/rootfs
MIRROR=http://ap-northeast-1.ec2.archive.ubuntu.com/ubuntu/
mkdir -p "$WORK"
debootstrap --variant=minbase --arch=amd64 noble "$ROOTFS" "$MIRROR" > "$WORK/debootstrap.log" 2>&1
tail -2 "$WORK/debootstrap.log"
du -shx "$ROOTFS"
返回结果如下:
I: Configuring libc-bin...
I: Base system installed successfully.
130M /home/ubuntu/attestable-image/rootfs
--variant=minbase 只安装 Essential 优先级的软件包和 apt。但是 debootstrap 生成的软件源只包含 noble main,还需要补充 noble-updates 与 noble-security,才能安装到包含安全补丁的内核。
如下命令补充软件源,并禁止软件包安装时在 chroot 内启动服务:
: > "$ROOTFS/etc/apt/sources.list"
cat > "$ROOTFS/etc/apt/sources.list.d/ubuntu.sources" <<EOF
Types: deb
URIs: $MIRROR
Suites: noble noble-updates
Components: main universe
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main universe
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF
cat > "$ROOTFS/usr/sbin/policy-rc.d" <<'EOF'
#!/bin/sh
exit 101
EOF
chmod 0755 "$ROOTFS/usr/sbin/policy-rc.d"
mount -t proc proc "$ROOTFS/proc"
mount -t sysfs sys "$ROOTFS/sys"
mount --bind /dev "$ROOTFS/dev"
mount --bind /dev/pts "$ROOTFS/dev/pts"
以上脚本中,policy-rc.d 返回 101 时,软件包的安装脚本不会启动服务,避免在构建机上误启动 chrony 等守护进程。镜像需要的软件包及其作用如下:
| 软件包 | 作用 |
|---|---|
systemd systemd-sysv udev dbus kmod |
初始化系统、设备管理和内核模块加载 |
systemd-resolved iproute2 |
DNS 解析与网络管理,配合 systemd-networkd 使用 |
ca-certificates curl openssl |
门控脚本访问 IMDSv2 与 AWS KMS,并解密 KMS 返回结果 |
chrony |
与 Amazon Time Sync Service 同步时间 |
unzip |
解压 AWS CLI v2 安装包 |
zstd |
initramfs 的默认压缩工具;缺少时 update-initramfs 提示 W: No zstd in /usr/bin:/sbin:/bin, using gzip 并改用 gzip |
linux-image-aws |
面向 EC2 优化的 Ubuntu 内核 |
initramfs-tools |
生成 initrd |
cryptsetup-bin |
提供 veritysetup |
overlayroot |
在 initrd 阶段把根目录配置为 tmpfs 上层的 overlayfs |
执行如下命令安装软件包,并检查关键软件包的版本:
chroot "$ROOTFS" apt-get update -qq
chroot "$ROOTFS" apt-get install -y -q --no-install-recommends \
systemd systemd-sysv systemd-resolved udev dbus kmod iproute2 \
ca-certificates curl openssl chrony unzip zstd \
linux-image-aws initramfs-tools cryptsetup-bin overlayroot > "$WORK/apt.log" 2>&1
tail -3 "$WORK/apt.log"
chroot "$ROOTFS" dpkg-query -W -f='${Package} ${Version}\n' \
linux-image-aws systemd initramfs-tools cryptsetup-bin overlayroot chrony
ls "$ROOTFS/boot"
echo "packages: $(chroot "$ROOTFS" dpkg-query -W -f='.\n' | wc -l)"
du -shx "$ROOTFS"
返回结果如下:
Processing triggers for linux-image-7.0.0-1014-aws (7.0.0-1014.14~24.04.1) ...
/etc/kernel/postinst.d/initramfs-tools:
update-initramfs: Generating /boot/initrd.img-7.0.0-1014-aws
chrony 4.5-1ubuntu4.2
cryptsetup-bin 2:2.7.0-1ubuntu4.2
initramfs-tools 0.142ubuntu25.8
linux-image-aws 7.0.0-1014.14~24.04.1
overlayroot 0.49~24.04.1
systemd 255.4-1ubuntu8.18
System.map-7.0.0-1014-aws
config-7.0.0-1014-aws
initrd.img
initrd.img-7.0.0-1014-aws
initrd.img.old
microcode.cpio
vmlinuz
vmlinuz-7.0.0-1014-aws
vmlinuz.old
packages: 166
910M /home/ubuntu/attestable-image/rootfs
--no-install-recommends 不安装推荐依赖,内核软件包因此不会带入 GRUB 等引导程序。安装内核时会自动生成一次 initrd,第 4 节加入 dm-verity 支持后会重新生成。根文件系统共 166 个软件包,其中内核模块占用了大部分空间。
由此基础镜像构建完成。
2、安装本机构建的 deb 软件包与 AWS CLI
本文第三章构建的 Nitro TPM 工具和第四章构建的远程证明、门控脚本的 deb 软件包保存在构建机的 ubuntu 用户目录下,需要将其复制过来。执行如下命令:
cp /home/ubuntu/pkgbuild/aws-nitro-tpm-tools_1.1.2-1_amd64.deb \
/home/ubuntu/gatebuild/nitro-tpm-attest-gate_1.0.0-1_all.deb "$ROOTFS/tmp/"
chroot "$ROOTFS" apt-get install -y -q --no-install-recommends \
/tmp/aws-nitro-tpm-tools_1.1.2-1_amd64.deb /tmp/nitro-tpm-attest-gate_1.0.0-1_all.deb 2>&1 \
| grep -E '^(Setting up (aws-nitro|nitro-tpm|libtss2)|Created symlink)'
chroot "$ROOTFS" systemctl is-enabled nitro-tpm-attest-gate.service nitro-tpm-protected-workload.service
返回结果如下:
Created symlink /etc/systemd/system/paths.target.wants/tpm-udev.path → /usr/lib/systemd/system/tpm-udev.path.
Setting up libtss2-mu-4.0.1-0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tcti-libtpms0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-sys1t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tcti-device0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tcti-mssim0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tcti-cmd0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tcti-swtpm0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tcti-spi-helper0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-tctildr0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up libtss2-esys-3.0.2-0t64:amd64 (4.0.1-7.1ubuntu5.1) ...
Setting up aws-nitro-tpm-tools (1.1.2-1) ...
Setting up nitro-tpm-attest-gate (1.0.0-1) ...
Created symlink /etc/systemd/system/multi-user.target.wants/nitro-tpm-attest-gate.service → /usr/lib/systemd/system/nitro-tpm-attest-gate.service.
Created symlink /etc/systemd/system/multi-user.target.wants/nitro-tpm-protected-workload.service → /usr/lib/systemd/system/nitro-tpm-protected-workload.service.
enabled
enabled
apt 根据 aws-nitro-tpm-tools 的 Depends 字段自动安装了 libtss2 运行库。门控软件包的 postinst 在 chroot 中执行 systemctl enable,systemd 检测到处于 chroot 环境后直接创建启用链接,两个服务因此在镜像中处于开机启用状态。
门控脚本通过 AWS CLI 调用 KMS。由于最小化的 Ubuntu 系统软件包清单中是不包含 AWS CLI 的,因此还需要为镜像安装 AWS CLI。执行如下命令在 chroot 中运行官方安装程序:
curl -fsSLo "$WORK/awscliv2.zip" https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip
unzip -q "$WORK/awscliv2.zip" -d "$ROOTFS/tmp"
chroot "$ROOTFS" /tmp/aws/install --update >/dev/null
chroot "$ROOTFS" aws --version
返回结果如下:
aws-cli/2.39.0 Python/3.14.8 Linux/7.0.0-1014-aws exe/x86_64.ubuntu.24
AWS CLI 的版本随官方发布更新,回显中的版本号以实际安装时为准。
至此,镜像中已经包含远程证明脚本、服务门控脚本、用于调用 AWS KMS 服务的 AWS CLI。
3、配置网络、时间同步、挂载点并去除交互入口
(1) 配置网络与 DNS
镜像中没有 cloud-init 和 netplan,使用 systemd-networkd 通过 DHCP 获取地址,systemd-resolved 提供 DNS 解析。执行如下命令写入配置:
echo nitro-tpm-ubuntu > "$ROOTFS/etc/hostname"
cat > "$ROOTFS/etc/systemd/network/10-ec2.network" <<'EOF'
[Match]
Type=ether
[Network]
DHCP=ipv4
EOF
ln -sf ../run/systemd/resolve/stub-resolv.conf "$ROOTFS/etc/resolv.conf"
/etc/resolv.conf 指向 /run 下由 systemd-resolved 运行时生成的文件,根分区只读不影响 DNS 配置。
(2) 配置时间同步并关闭 chrony 命令端口
与 AWS 其他官方镜像一致,chrony 只使用 Amazon Time Sync Service(169.254.169.123),并通过 cmdport 0 关闭命令端口。执行如下命令替换 chrony 配置,并启用网络、DNS 和时间同步服务:
cat > "$ROOTFS/etc/chrony/chrony.conf" <<'EOF'
# 仅使用 Amazon Time Sync Service,并关闭 chrony 命令端口
server 169.254.169.123 prefer iburst minpoll 4 maxpoll 4
driftfile /var/lib/chrony/chrony.drift
makestep 1.0 3
rtcsync
cmdport 0
EOF
chroot "$ROOTFS" systemctl enable systemd-networkd.service systemd-resolved.service chrony.service
返回结果如下:
Synchronizing state of chrony.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable chrony
Created symlink /etc/systemd/system/dbus-org.freedesktop.network1.service → /usr/lib/systemd/system/systemd-networkd.service.
Created symlink /etc/systemd/system/multi-user.target.wants/systemd-networkd.service → /usr/lib/systemd/system/systemd-networkd.service.
Created symlink /etc/systemd/system/sockets.target.wants/systemd-networkd.socket → /usr/lib/systemd/system/systemd-networkd.socket.
Created symlink /etc/systemd/system/sysinit.target.wants/systemd-network-generator.service → /usr/lib/systemd/system/systemd-network-generator.service.
Created symlink /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service → /usr/lib/systemd/system/systemd-networkd-wait-online.service.
systemd-networkd-wait-online.service 随之启用,门控服务依赖的 network-online.target 会等待网卡获取地址后才到达。
(3) 配置挂载点与 machine-id
先为两个分区生成固定的 PARTUUID。ESP 的 PARTUUID 写入 /etc/fstab,根分区的 PARTUUID 写入第 6 节的内核命令行,第 7 节创建分区表时使用同样的值。执行如下命令:
ESP_UUID=$(uuidgen)
ROOT_UUID=$(uuidgen)
echo "$ESP_UUID" > "$WORK/esp.partuuid"
echo "$ROOT_UUID" > "$WORK/root.partuuid"
mkdir -p "$ROOTFS/boot/efi"
cat > "$ROOTFS/etc/fstab" <<EOF
PARTUUID=$ESP_UUID /boot/efi vfat ro,nosuid,nodev,noexec,noatime,umask=0077 0 0
tmpfs /tmp tmpfs nosuid,nodev,mode=1777 0 0
EOF
: > "$ROOTFS/etc/machine-id"
ln -sf /etc/machine-id "$ROOTFS/var/lib/dbus/machine-id"
cat "$ROOTFS/etc/fstab"
返回结果如下:
PARTUUID=8c7b1b13-eaa3-4970-a4e7-804851b81c24 /boot/efi vfat ro,nosuid,nodev,noexec,noatime,umask=0077 0 0
tmpfs /tmp tmpfs nosuid,nodev,mode=1777 0 0
fstab 中没有根目录条目,根目录由 initrd 根据内核命令行挂载。ESP 以只读方式挂载,/tmp 为独立 tmpfs,与 Amazon Linux 2023 示例镜像一致。
/etc/machine-id 清空后,systemd 每次开机为实例生成新的 machine-id。dbus 安装时还会生成 /var/lib/dbus/machine-id,systemd 发现 /etc/machine-id 为空时会优先采用该文件,使所有由该 AMI 启动的实例拥有相同的 machine-id。实测未处理时,System Log 显示 Initializing machine ID from D-Bus machine ID;改为指向 /etc/machine-id 的符号链接后,显示 Initializing machine ID from VM UUID,每个实例的 machine-id 各不相同。
(4) 去除交互入口并确认
执行如下命令锁定 root 密码,并屏蔽不需要或会提供登录入口的 systemd 单元:
chroot "$ROOTFS" passwd -l root
chroot "$ROOTFS" systemctl mask systemd-firstboot.service systemd-tpm2-setup-early.service systemd-tpm2-setup.service \
getty@.service serial-getty@.service apt-daily.timer apt-daily-upgrade.timer
返回结果如下:
passwd: password changed.
Created symlink /etc/systemd/system/systemd-firstboot.service → /dev/null.
Created symlink /etc/systemd/system/systemd-tpm2-setup-early.service → /dev/null.
Created symlink /etc/systemd/system/systemd-tpm2-setup.service → /dev/null.
Created symlink /etc/systemd/system/getty@.service → /dev/null.
Created symlink /etc/systemd/system/serial-getty@.service → /dev/null.
Created symlink /etc/systemd/system/apt-daily.timer → /dev/null.
Created symlink /etc/systemd/system/apt-daily-upgrade.timer → /dev/null.
屏蔽这些单元的原因如下:
| 单元 | 屏蔽原因 |
|---|---|
getty@.service serial-getty@.service |
内核参数 systemd.getty_auto=false 只关闭 getty 生成器,不影响默认启用的 getty@tty1.service;实测只使用该参数时,System Log 中仍出现 agetty 进程。屏蔽模板单元后,所有虚拟终端和串口都不再提供登录提示 |
systemd-firstboot.service |
machine-id 为空时 systemd 视为首次开机,该服务可能在控制台上交互式询问区域、时区和 root 密码 |
systemd-tpm2-setup-early.service systemd-tpm2-setup.service |
systemd 255 尝试在 TPM 中创建存储根密钥,镜像缺少 libtss2-rc 时报 Failed to create TPM2 context: Operation not supported 并启动失败;本文不使用 systemd 的 TPM 功能 |
apt-daily.timer apt-daily-upgrade.timer |
根分区只读,自动更新没有意义 |
执行如下命令确认镜像中没有交互入口相关的软件包,并检查 root 账户状态:
chroot "$ROOTFS" dpkg-query -W -f='${Package}\n' \
| grep -E '^(openssh-server|openssh-client|cloud-init|snapd|amazon-ssm-agent|ec2-instance-connect|update-motd|sudo)$' \
|| echo "none of openssh-server/cloud-init/snapd/amazon-ssm-agent/ec2-instance-connect/sudo installed"
grep '^root:' "$ROOTFS/etc/shadow" | cut -d: -f1,2
返回结果如下:
none of openssh-server/cloud-init/snapd/amazon-ssm-agent/ec2-instance-connect/sudo installed
root:!*
镜像中没有 OpenSSH、cloud-init、snapd、SSM Agent、EC2 Instance Connect 和 sudo,root 密码字段为 !*,即已锁定且没有设置密码。
4、在 initrd 中加入 dm-verity 与 overlayroot
在之前构建的 Amazon Linux 2023 镜像中,参考了 AWS 官方示例镜像的生成方式,使用 dracut 生成 initrd,由 KIWI 的 dracut 模块打开 dm-verity 设备。
Ubuntu 镜像于此有所不同,Ubuntu 默认使用 initramfs-tools,它没有内置打开 dm-verity 根分区的脚本,因此本文编写一个钩子和一个启动脚本。initramfs-tools 的启动顺序与本文用到的阶段如下:
init-top(启动 udev)
-> local-top:nitrotpm-verity 脚本按内核命令行打开 /dev/mapper/verityroot
-> 挂载根分区:以只读方式把 /dev/mapper/verityroot(erofs)挂载为根目录
-> init-bottom:overlayroot 脚本在只读根目录之上叠加 tmpfs,形成可写的 overlayfs
-> switch_root 切换到 systemd
执行如下命令编写钩子。钩子在生成 initrd 时运行,把 veritysetup 及其依赖库、dm_verity、erofs、overlay 内核模块复制进 initrd:
cat > "$ROOTFS/etc/initramfs-tools/hooks/nitrotpm-verity" <<'EOF'
#!/bin/sh
# 把 veritysetup 和 dm-verity、erofs、overlay 内核模块打包进 initrd
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in prereqs) prereqs; exit 0;; esac
. /usr/share/initramfs-tools/hook-functions
copy_exec /usr/sbin/veritysetup /sbin
manual_add_modules dm_verity erofs overlay
EOF
执行如下命令编写 local-top 阶段的启动脚本。脚本从内核命令行读取数据分区、根哈希、哈希树偏移量和校验选项,打开 dm-verity 设备;任何一步失败都调用 initramfs-tools 的 panic 函数:
cat > "$ROOTFS/etc/initramfs-tools/scripts/local-top/nitrotpm-verity" <<'EOF'
#!/bin/sh
# 按内核命令行参数打开 dm-verity 设备 /dev/mapper/verityroot,作为只读根分区
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in prereqs) prereqs; exit 0;; esac
. /scripts/functions
DATA= ROOTHASH= HASHOFF= OPTS=
for x in $(cat /proc/cmdline); do
case "$x" in
verity.data=*) DATA="${x#verity.data=}" ;;
verity.roothash=*) ROOTHASH="${x#verity.roothash=}" ;;
verity.hash_offset=*) HASHOFF="${x#verity.hash_offset=}" ;;
verity.options=panic-on-corruption) OPTS="--panic-on-corruption" ;;
esac
done
[ -n "$ROOTHASH" ] || exit 0
case "$DATA" in
PARTUUID=*) DEV="/dev/disk/by-partuuid/${DATA#PARTUUID=}" ;;
*) DEV="$DATA" ;;
esac
i=0
while [ ! -b "$DEV" ] && [ "$i" -lt 30 ]; do sleep 1; i=$((i + 1)); done
[ -b "$DEV" ] || panic "nitrotpm-verity: data device $DEV not found"
modprobe dm_verity
echo "nitrotpm-verity: opening $DEV as verityroot, root hash $ROOTHASH"
veritysetup open "$DEV" verityroot "$DEV" "$ROOTHASH" --hash-offset="$HASHOFF" $OPTS \
|| panic "nitrotpm-verity: veritysetup open failed"
veritysetup status verityroot | sed 's/^/nitrotpm-verity: /'
EOF
chmod 0755 "$ROOTFS/etc/initramfs-tools/hooks/nitrotpm-verity" "$ROOTFS/etc/initramfs-tools/scripts/local-top/nitrotpm-verity"
echo "RESUME=none" > "$ROOTFS/etc/initramfs-tools/conf.d/resume"
RESUME=none 关闭休眠恢复设备的查找,避免 initrd 在开机时等待交换分区。overlayroot 软件包已经自带 init-bottom 脚本和钩子,只需在内核命令行中加入 overlayroot=tmpfs:recurse=0 即可启用,recurse=0 表示只对根目录叠加 tmpfs,不处理 fstab 中的其他挂载点。
注意:initramfs-tools 的
panic函数在内核命令行带有panic=参数时不会进入救援 Shell。第 6 节的内核命令行设置panic=0,initrd 阶段的任何错误都会使系统停机,作用与 Amazon Linux 2023 示例镜像的rd.shell=0相同。
执行如下命令重新生成 initrd,并检查脚本和模块是否已打包进去:
KVER=$(ls "$ROOTFS/lib/modules")
echo "KVER=$KVER"
chroot "$ROOTFS" update-initramfs -u -k "$KVER"
chroot "$ROOTFS" lsinitramfs "/boot/initrd.img-$KVER" \
| grep -E 'sbin/veritysetup$|dm-verity\.ko|/erofs\.ko|/overlay\.ko|local-top/nitrotpm-verity|init-bottom/overlayroot$'
返回结果如下:
KVER=7.0.0-1014-aws
update-initramfs: Generating /boot/initrd.img-7.0.0-1014-aws
usr/lib/modules/7.0.0-1014-aws/kernel/drivers/md/dm-verity.ko.zst
usr/lib/modules/7.0.0-1014-aws/kernel/fs/erofs/erofs.ko.zst
usr/lib/modules/7.0.0-1014-aws/kernel/fs/overlayfs/overlay.ko.zst
scripts/init-bottom/overlayroot
scripts/local-top/nitrotpm-verity
usr/sbin/veritysetup
5、生成 erofs 根分区与 dm-verity 哈希树
UKI 需要使用根文件系统中的内核和 initrd,因此先把它们复制到工作目录,再清理根文件系统中构建期使用的文件。执行如下命令:
OUT=$WORK/out
mkdir -p "$OUT"
cp "$ROOTFS/boot/vmlinuz-$KVER" "$WORK/vmlinuz"
cp "$ROOTFS/boot/initrd.img-$KVER" "$WORK/initrd.img"
cp "$ROOTFS/usr/lib/os-release" "$WORK/os-release"
for m in dev/pts dev sys proc; do umount "$ROOTFS/$m"; done
rm -f "$ROOTFS/usr/sbin/policy-rc.d"
rm -rf "$ROOTFS"/tmp/* "$ROOTFS"/var/lib/apt/lists/* "$ROOTFS"/var/cache/apt/*.bin "$ROOTFS"/var/cache/apt/archives/*.deb
find "$ROOTFS/boot" -mindepth 1 -maxdepth 1 ! -name efi -exec rm -rf {} +
ls -A "$ROOTFS/boot"
du -shx "$ROOTFS"
返回结果如下:
efi
626M /home/ubuntu/attestable-image/rootfs
内核和 initrd 已经进入 UKI,根文件系统中的 /boot 只保留 ESP 的挂载点。清理后根文件系统为 626 MiB。
执行如下命令生成 erofs 镜像:
mkfs.erofs -L ubuntu-root "$OUT/root-part.img" "$ROOTFS" 2>&1 | tail -4
DATA_SIZE=$(stat -c %s "$OUT/root-part.img")
echo "DATA_SIZE=$DATA_SIZE blocks=$((DATA_SIZE / 4096)) remainder=$((DATA_SIZE % 4096))"
返回结果如下:
Filesystem total blocks: 144312 (of 4096-byte blocks)
Filesystem total inodes: 26223
Filesystem total metadata blocks: 8714
Filesystem total deduplicated bytes (of source files): 0
DATA_SIZE=591101952 blocks=144312 remainder=0
erofs 镜像为 591101952 字节,正好是 144312 个 4 KiB 数据块。本文不启用 erofs 压缩,文件内容以原样存放在块设备上,便于第六章第 3 节直接在块设备上定位并修改文件内容。
与 Amazon Linux 2023 示例镜像相同,dm-verity 哈希树与 erofs 数据存放在同一个分区中:数据在前,哈希树从 --hash-offset 指定的位置开始。执行如下命令把镜像文件扩展为 2 GiB 的分区大小,在数据之后生成哈希树,并校验:
truncate -s 2048M "$OUT/root-part.img"
veritysetup format --data-blocks=$((DATA_SIZE / 4096)) --hash-offset="$DATA_SIZE" \
"$OUT/root-part.img" "$OUT/root-part.img" | tee "$OUT/verity.txt"
ROOTHASH=$(awk '/^Root hash:/{print $3}' "$OUT/verity.txt")
veritysetup verify --hash-offset="$DATA_SIZE" "$OUT/root-part.img" "$OUT/root-part.img" "$ROOTHASH" \
&& echo "veritysetup verify: OK"
返回结果如下:
VERITY header information for /home/ubuntu/attestable-image/out/root-part.img
UUID: e3488eaf-0d36-4adf-aa49-923bf7efe998
Hash type: 1
Data blocks: 144312
Data block size: 4096
Hash blocks: 1138
Hash block size: 4096
Hash algorithm: sha256
Salt: 230e4982a9f1d49a39e007ec1790ecc80e6af790e4c0c4691c159582ddd5efdb
Root hash: bd39fe0bda5d5c3d2443cf8048268b23d8e313cbf000e04978933cca2d97f695
veritysetup verify: OK
--data-blocks 限定只对 erofs 数据计算哈希,哈希树共 1138 个块。Root hash 是整棵哈希树的根,根文件系统中任何一个字节发生变化,根哈希都会不同。veritysetup format 每次生成随机的 Salt,即使根文件系统内容不变,重新构建也会得到不同的根哈希和 PCR4,因此每次构建都需要重新计算参考 PCR 值。
6、构建 UKI 并离线计算参考 PCR 值
执行如下命令组装内核命令行:
CMDLINE="root=/dev/mapper/verityroot rootfstype=erofs verity.data=PARTUUID=$ROOT_UUID verity.hash_offset=$DATA_SIZE verity.roothash=$ROOTHASH verity.options=panic-on-corruption overlayroot=tmpfs:recurse=0 console=ttyS0 nvme_core.io_timeout=4294967295 panic=0 fsck.mode=skip systemd.getty_auto=false systemd.gpt_auto=0"
echo "$CMDLINE" > "$OUT/cmdline"
cat "$OUT/cmdline"
返回结果如下:
root=/dev/mapper/verityroot rootfstype=erofs verity.data=PARTUUID=bf5ef7f0-7b94-4060-bda8-122d2d3adb4c verity.hash_offset=591101952 verity.roothash=bd39fe0bda5d5c3d2443cf8048268b23d8e313cbf000e04978933cca2d97f695 verity.options=panic-on-corruption overlayroot=tmpfs:recurse=0 console=ttyS0 nvme_core.io_timeout=4294967295 panic=0 fsck.mode=skip systemd.getty_auto=false systemd.gpt_auto=0
各参数的作用如下:
| 参数 | 作用 |
|---|---|
root=/dev/mapper/verityroot rootfstype=erofs |
根分区为 dm-verity 映射设备,文件系统类型为 erofs |
verity.data= verity.hash_offset= verity.roothash= |
由第 4 节的 initrd 脚本读取,指定数据分区、哈希树偏移量和根哈希 |
verity.options=panic-on-corruption |
读取到校验失败的数据块时内核立即 panic |
overlayroot=tmpfs:recurse=0 |
由 overlayroot 在只读根目录之上叠加 tmpfs |
console=ttyS0 |
内核和 systemd 输出写入串口控制台,可通过 EC2 System Log 读取 |
nvme_core.io_timeout=4294967295 |
与 Canonical 发布的 AMI 相同,避免 EBS 卷 I/O 超时 |
panic=0 |
initrd 阶段出错时停机,不进入救援 Shell |
fsck.mode=skip |
只读文件系统不需要检查 |
systemd.getty_auto=false |
关闭 getty 生成器,不在控制台上自动启动登录程序 |
systemd.gpt_auto=0 |
关闭 GPT 分区自动挂载,ESP 只按 fstab 只读挂载 |
注意:内核命令行中不能带
ro。overlayroot 发现命令行中有ro时,会把整个 overlayfs 根目录也挂载为只读,开机后不会自动改为可写。实测带ro时,System Log 中 systemd-logind 以238/STATE_DIRECTORY失败,chrony 以240/LOGS_DIRECTORY失败,日志提示Read-only file system;systemd-logind 不运行时无法处理 EC2 发出的关机信号,停止实例需要等待 3 到 4 分钟。erofs 本身是只读文件系统,dm-verity 映射设备也是只读设备,去掉ro不影响根分区的只读属性,只影响上层 overlayfs 能否写入内存。
执行如下命令构建 UKI,并离线计算参考 PCR 值:
ukify build --linux="$WORK/vmlinuz" --initrd="$WORK/initrd.img" --cmdline=@"$OUT/cmdline" \
--os-release=@"$WORK/os-release" --uname="$KVER" --output="$OUT/BOOTX64.EFI"
ls -l "$OUT/BOOTX64.EFI"
nitro-tpm-pcr-compute --image "$OUT/BOOTX64.EFI" | tee "$OUT/pcr.json"
返回结果如下:
Wrote unsigned /home/ubuntu/attestable-image/out/BOOTX64.EFI
-rw-r--r-- 1 root root 73416192 Oct 27 07:04 /home/ubuntu/attestable-image/out/BOOTX64.EFI
{
"Measurements": {
"HashAlgorithm": "SHA384",
"PCR4": "0f74fdc8fc227354b9f45b5986fbfcf6fcdbb016477c795a625362517b2f015a843e804b6c669642527d3ba394833ba8",
"PCR7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35",
"PCR12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
}
nitro-tpm-pcr-compute 使用第三章第 6 节安装在构建机上的软件包。请记录 PCR4 和 PCR12,第六章第 1 节写入 KMS 密钥策略。PCR7 与 Amazon Linux 2023 版本未配置 Secure Boot 时的值相同,本文 KMS 策略不检查 PCR7。
7、组装 GPT 磁盘镜像
执行如下命令创建 200 MiB 的 FAT32 格式 ESP 镜像,只放入 UKI。UEFI 固件在没有启动项时默认加载 EFI/BOOT/BOOTX64.EFI,因此不需要 shim、GRUB,也不需要设置 UEFI 启动项:
truncate -s 200M "$OUT/esp.img"
mkfs.vfat -F 32 -n ESP "$OUT/esp.img" >/dev/null
mkdir -p /mnt/esp && mount -o loop "$OUT/esp.img" /mnt/esp
mkdir -p /mnt/esp/EFI/BOOT && cp "$OUT/BOOTX64.EFI" /mnt/esp/EFI/BOOT/BOOTX64.EFI
find /mnt/esp -type f -exec ls -l {} +
umount /mnt/esp
返回结果如下:
-rwxr-xr-x 1 root root 73416192 Oct 27 07:04 /mnt/esp/EFI/BOOT/BOOTX64.EFI
执行如下命令创建 4 GiB 的磁盘镜像,写入 GPT 分区表,并把 ESP 镜像和根分区镜像写入对应分区:
truncate -s 4G "$OUT/disk.raw"
sgdisk --clear \
-n 1:2048:+200M -t 1:ef00 -c 1:ESP -u 1:"$ESP_UUID" \
-n 2:0:+2048M -t 2:8300 -c 2:root -u 2:"$ROOT_UUID" "$OUT/disk.raw" >/dev/null
sgdisk -p "$OUT/disk.raw" | sed -n '/^Number/,$p'
P1=$(sgdisk -i 1 "$OUT/disk.raw" | awk '/^First sector/{print $3}')
P2=$(sgdisk -i 2 "$OUT/disk.raw" | awk '/^First sector/{print $3}')
dd if="$OUT/esp.img" of="$OUT/disk.raw" bs=4M oflag=seek_bytes seek=$((P1 * 512)) conv=notrunc status=none
dd if="$OUT/root-part.img" of="$OUT/disk.raw" bs=4M oflag=seek_bytes seek=$((P2 * 512)) conv=notrunc status=none
返回结果如下:
Number Start (sector) End (sector) Size Code Name
1 2048 411647 200.0 MiB EF00 ESP
2 411648 4605951 2.0 GiB 8300 root
执行如下命令把磁盘镜像映射为回环设备,检查两个分区的文件系统,并在分区设备上再次校验 dm-verity:
LOOP=$(losetup -fP --show "$OUT/disk.raw")
blkid "${LOOP}p1" "${LOOP}p2"
veritysetup verify --hash-offset="$DATA_SIZE" "${LOOP}p2" "${LOOP}p2" "$ROOTHASH" && echo "disk p2 verity verify: OK"
losetup -d "$LOOP"
ls -lhs "$OUT"
sha256sum "$OUT/disk.raw" "$OUT/BOOTX64.EFI"
返回结果如下:
/dev/loop4p1: LABEL_FATBOOT="ESP" LABEL="ESP" UUID="D0B4-698C" BLOCK_SIZE="512" TYPE="vfat" PARTLABEL="ESP" PARTUUID="8c7b1b13-eaa3-4970-a4e7-804851b81c24"
/dev/loop4p2: LABEL="ubuntu-root" UUID="07e262bb-570b-4f1a-ba16-4672468d710b" BLOCK_SIZE="4096" TYPE="erofs" PARTLABEL="root" PARTUUID="bf5ef7f0-7b94-4060-bda8-122d2d3adb4c"
disk p2 verity verify: OK
total 2.9G
71M -rw-r--r-- 1 root root 71M Oct 27 07:04 BOOTX64.EFI
4.0K -rw-r--r-- 1 root root 389 Oct 27 07:04 cmdline
2.2G -rw-r--r-- 1 root root 4.0G Oct 27 07:04 disk.raw
74M -rw-r--r-- 1 root root 200M Oct 27 07:04 esp.img
4.0K -rw-r--r-- 1 root root 395 Oct 27 07:04 pcr.json
569M -rw-r--r-- 1 root root 2.0G Oct 27 07:04 root-part.img
4.0K -rw-r--r-- 1 root root 437 Oct 27 07:04 verity.txt
a9bd590e3f84f916d60b7fd3deed2d9e4e1408998d2e0e5b7b124bdcdc864db7 /home/ubuntu/attestable-image/out/disk.raw
b56aa8c56d9ef7e0e74cacc3b837ebf7da28768f582acc9bae93561d2028ad4a /home/ubuntu/attestable-image/out/BOOTX64.EFI
两个分区的 PARTUUID 与 fstab 和内核命令行中的值一致,根分区可识别为 erofs,dm-verity 校验通过。磁盘镜像是稀疏文件,名义大小为 4 GiB,实际占用 2.2 GiB。
8、写入 EBS 卷、创建快照并注册 AMI
磁盘镜像需要先写入一个 EBS 卷,再从该卷创建快照并注册 AMI。以下卷操作在本机以具有 EC2 权限的管理员身份执行,构建机角色不需要 EC2 权限。执行如下命令在构建机所在的可用区创建 4 GiB 的卷,并挂载到构建机:
VOL=$(aws ec2 create-volume --region ap-northeast-1 --availability-zone ap-northeast-1a \
--size 4 --volume-type gp3 \
--tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=ubuntu-2404-attestable}]' \
--query VolumeId --output text)
echo $VOL
aws ec2 wait volume-available --region ap-northeast-1 --volume-ids $VOL
aws ec2 attach-volume --region ap-northeast-1 --volume-id $VOL \
--instance-id i-0b84ac9db7e50906e --device /dev/sdf --query State --output text
返回结果如下:
vol-0a9059cbfd95656b0
attaching
在构建机上执行如下命令,按卷 ID 找到对应的 NVMe 设备,写入整个磁盘镜像,并与磁盘镜像逐字节比对:
VOL_ID=vol-0a9059cbfd95656b0
DEV=$(readlink -f /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${VOL_ID/-/})
echo $DEV
dd if="$OUT/disk.raw" of="$DEV" bs=4M conv=fsync status=progress 2>&1 | tail -1
partprobe "$DEV"
lsblk -o NAME,SIZE,FSTYPE,PARTUUID "$DEV"
echo 3 > /proc/sys/vm/drop_caches
cmp "$OUT/disk.raw" "$DEV" && echo "cmp whole disk: identical"
返回结果如下:
/dev/nvme2n1
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 32.1432 s, 134 MB/s
NAME SIZE FSTYPE PARTUUID
nvme2n1 4G
├─nvme2n1p1 200M vfat 8c7b1b13-eaa3-4970-a4e7-804851b81c24
└─nvme2n1p2 2G erofs bf5ef7f0-7b94-4060-bda8-122d2d3adb4c
cmp whole disk: identical
注意:不要为
dd加conv=sparse参数。该参数会跳过全零的数据块,实测新建 EBS 卷中未写入区域读出的内容并非全零,使用该参数写入后cmp报告大量差异。本文写入完整的 4 GiB,确保卷内容与磁盘镜像完全一致。
在本机执行如下命令从构建机卸载卷,并创建快照:
aws ec2 detach-volume --region ap-northeast-1 --volume-id $VOL --query State --output text
aws ec2 wait volume-available --region ap-northeast-1 --volume-ids $VOL
aws ec2 create-snapshot --region ap-northeast-1 --volume-id $VOL \
--description 'Ubuntu 24.04 attestable image (erofs + dm-verity + UKI)' \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=Name,Value=ubuntu-2404-attestable}]' \
--query SnapshotId --output text
返回结果如下:
detaching
snap-048b85c31c985e8a1
执行如下命令等待快照完成。本文实测 4 GiB 卷的快照需要约 6 到 10 分钟:
aws ec2 wait snapshot-completed --region ap-northeast-1 --snapshot-ids snap-048b85c31c985e8a1
快照完成后,执行如下命令注册 AMI。--tpm-support v2.0 启用 NitroTPM,--boot-mode uefi 指定 UEFI 启动。该 AMI 由本文自行构建,根设备名称使用 /dev/xvda,与 Amazon Linux 2023 版本一致:
aws ec2 register-image --region ap-northeast-1 \
--name ubuntu-2404-attestable-v3 \
--description 'Ubuntu 24.04 attestable image: erofs + dm-verity root, UKI, NitroTPM' \
--boot-mode uefi --architecture x86_64 --virtualization-type hvm \
--root-device-name /dev/xvda \
--block-device-mappings 'DeviceName=/dev/xvda,Ebs={SnapshotId=snap-048b85c31c985e8a1,VolumeType=gp3,DeleteOnTermination=true}' \
--tpm-support v2.0 --ena-support --imds-support v2.0
返回结果如下:
{
"ImageId": "ami-04f27bf2212494092"
}
执行如下命令确认 AMI 属性:
aws ec2 describe-images --region ap-northeast-1 --image-ids ami-04f27bf2212494092 \
--query 'Images[0].{ImageId:ImageId,Name:Name,State:State,BootMode:BootMode,TpmSupport:TpmSupport,ImdsSupport:ImdsSupport,RootDeviceName:RootDeviceName,SnapshotId:BlockDeviceMappings[0].Ebs.SnapshotId,VolumeSize:BlockDeviceMappings[0].Ebs.VolumeSize}' \
--output json
返回结果如下:
{
"ImageId": "ami-04f27bf2212494092",
"Name": "ubuntu-2404-attestable-v3",
"State": "available",
"BootMode": "uefi",
"TpmSupport": "v2.0",
"ImdsSupport": "v2.0",
"RootDeviceName": "/dev/xvda",
"SnapshotId": "snap-048b85c31c985e8a1",
"VolumeSize": 4
}
AMI 名称中的 v3 表示本次测试的第三次构建,前两次构建分别暴露了第 6 节内核命令行带 ro 和第 3 节 machine-id 重复的问题。注册完成后,写入用的 EBS 卷可以删除,AMI 只依赖快照。AMI 构建完成,接下来进行远程证明验证。
六、远程证明验证
1、创建 KMS 密钥并写入参考 PCR 值
(1) 创建 KMS 客户托管密钥并配置别名和标签
在本机执行如下命令创建对称加密密钥,并添加标签:
aws kms create-key --region ap-northeast-1 \
--description 'NitroTPM remote attestation test key for Ubuntu 24.04' \
--key-usage ENCRYPT_DECRYPT --key-spec SYMMETRIC_DEFAULT \
--tags TagKey=Name,TagValue=ubuntu-2404-nitro-tpm-test \
TagKey=Project,TagValue=NitroTPM-Attestation \
TagKey=OS,TagValue=Ubuntu-24.04 \
TagKey=Environment,TagValue=test \
--query 'KeyMetadata.{KeyId:KeyId,Arn:Arn,KeyState:KeyState,KeySpec:KeySpec,KeyUsage:KeyUsage}' --output json
返回结果如下:
{
"KeyId": "<key-id>",
"Arn": "arn:aws:kms:ap-northeast-1:<account-id>:key/<key-id>",
"KeyState": "Enabled",
"KeySpec": "SYMMETRIC_DEFAULT",
"KeyUsage": "ENCRYPT_DECRYPT"
}
执行如下命令创建别名,并查看别名和标签。别名必须与第四章第 1 节写入门控软件包配置文件的 KEY_ID 一致:
aws kms create-alias --region ap-northeast-1 \
--alias-name alias/ubuntu-2404-nitro-tpm-test --target-key-id <key-id>
aws kms list-aliases --region ap-northeast-1 --key-id <key-id> \
--query 'Aliases[].[AliasName,TargetKeyId]' --output text
aws kms list-resource-tags --region ap-northeast-1 --key-id <key-id> \
--query 'Tags[].[TagKey,TagValue]' --output text
返回结果如下:
alias/ubuntu-2404-nitro-tpm-test <key-id>
Environment test
Name ubuntu-2404-nitro-tpm-test
OS Ubuntu-24.04
Project NitroTPM-Attestation
使用 AWS CLI 创建且不指定策略时,密钥只有默认的 Enable IAM User Permissions 语句。第 (2) 步会在保留该语句的基础上增加 PCR 条件。
(2) 将参考 PCR 值写入 KMS 密钥策略
在本机新建 kms-policy.json,保留默认的 Enable IAM User Permissions 语句,并增加带 PCR4 和 PCR12 条件的语句。替换 <account-id> 和运行实例角色名称,PCR4 与 PCR12 使用第五章第 6 节的计算结果:
{
"Version": "2012-10-17",
"Id": "key-default-1",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<account-id>:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow requests from attested Ubuntu instances only",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<account-id>:role/<runtime-instance-role>"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*",
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:NitroTPMPCR4": "0f74fdc8fc227354b9f45b5986fbfcf6fcdbb016477c795a625362517b2f015a843e804b6c669642527d3ba394833ba8",
"kms:RecipientAttestation:NitroTPMPCR12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
}
}
]
}
执行如下命令更新密钥策略,并确认策略中的语句和 PCR4:
aws kms put-key-policy --region ap-northeast-1 --key-id <key-id> \
--policy-name default --policy file://kms-policy.json
aws kms get-key-policy --region ap-northeast-1 --key-id <key-id> --policy-name default \
--query Policy --output text \
| python3 -c 'import json,sys;p=json.load(sys.stdin);print([s["Sid"] for s in p["Statement"]]);print(p["Statement"][1]["Condition"])'
返回结果如下:
['Enable IAM User Permissions', 'Allow requests from attested Ubuntu instances only']
{'StringEqualsIgnoreCase': {'kms:RecipientAttestation:NitroTPMPCR4': '0f74fdc8fc227354b9f45b5986fbfcf6fcdbb016477c795a625362517b2f015a843e804b6c669642527d3ba394833ba8', 'kms:RecipientAttestation:NitroTPMPCR12': '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'}}
注意:
put-key-policy的--key-id不接受别名,传入alias/ubuntu-2404-nitro-tpm-test会返回InvalidArnException: Key Aliases are not supported for this operation,必须使用密钥 ID 或密钥 ARN。另外,运行实例角色不能挂载任何包含kms:Decrypt或kms:GenerateDataKey的 IAM 策略,否则实例不经过远程证明也能使用该密钥。镜像中没有 SSM Agent,运行实例角色也不需要AmazonSSMManagedInstanceCore。
2、启动实例并查看远程证明成功结果
在本机执行如下命令,使用可证明的 AMI 启动 m7i.large 测试实例。镜像中没有 SSH 和 cloud-init,因此不指定密钥对,也不传入 User Data。安全组不需要任何入站规则,出站放行 TCP 443,用于访问 AWS KMS:
aws ec2 run-instances --region ap-northeast-1 \
--image-id ami-04f27bf2212494092 \
--instance-type m7i.large \
--iam-instance-profile Name=<runtime-instance-profile> \
--network-interfaces 'DeviceIndex=0,SubnetId=<subnet-id>,Groups=<security-group-id>,AssociatePublicIpAddress=true,DeleteOnTermination=true' \
--metadata-options 'HttpTokens=required,HttpEndpoint=enabled,HttpPutResponseHopLimit=1' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=nitro-tpm-ubuntu-attestable-test}]' \
--query 'Instances[0].[InstanceId,State.Name]' --output text
返回结果如下:
i-0998950bf053d2de3 pending
实例启动约 1 分钟后,执行如下命令从 EC2 System Log 中查看 dm-verity、overlayroot 和远程证明的输出:
aws ec2 get-console-output --region ap-northeast-1 --instance-id i-0998950bf053d2de3 \
--latest --output text \
| grep -aE 'nitrotpm-verity|overlayroot|Welcome|machine ID|ATTEST-TEST|PROTECTED-WORKLOAD|FAILED|DEPEND'
返回结果如下:
Begin: Mounting root file system ... Begin: Running /scripts/local-top ... nitrotpm-verity: opening /dev/disk/by-partuuid/bf5ef7f0-7b94-4060-bda8-122d2d3adb4c as verityroot, root hash bd39fe0bda5d5c3d2443cf8048268b23d8e313cbf000e04978933cca2d97f695
nitrotpm-verity: /dev/mapper/verityroot is active.
nitrotpm-verity: type: VERITY
nitrotpm-verity: status: verified
nitrotpm-verity: hash type: 1
nitrotpm-verity: data block: 4096
nitrotpm-verity: hash block: 4096
nitrotpm-verity: hash name: sha256
nitrotpm-verity: salt: 230e4982a9f1d49a39e007ec1790ecc80e6af790e4c0c4691c159582ddd5efdb
nitrotpm-verity: data device: /dev/nvme0n1p2
nitrotpm-verity: size: 1154496 sectors
nitrotpm-verity: mode: readonly
nitrotpm-verity: hash device: /dev/nvme0n1p2
nitrotpm-verity: hash offset: 1154504 sectors
nitrotpm-verity: root hash: bd39fe0bda5d5c3d2443cf8048268b23d8e313cbf000e04978933cca2d97f695
nitrotpm-verity: flags: panic_on_corruption
Begin: Running /scripts/init-bottom ... Warning: overlayroot: configuring overlayroot with driver=overlay mode=tmpfs opts='recurse=0' per kernel cmdline
Success: overlayroot: configured root with 'tmpfs:recurse=0' using overlay per kernel cmdline
Welcome to Ubuntu 24.04 LTS!
[ 8.018668] systemd[1]: Initializing machine ID from VM UUID.
[ 11.155650] nitro-tpm-attest-gate[379]: [ATTEST-TEST] Attestation document generated, size 5209 bytes
[ 15.805603] nitro-tpm-attest-gate[379]: [ATTEST-TEST] RESULT=SUCCESS KMS accepted attestation, data key decrypted locally (32 bytes)
[ 15.816022] nitro-tpm-protected-workload[404]: [PROTECTED-WORKLOAD] STARTED after remote attestation
判断远程证明成功的依据如下:
nitrotpm-verity的输出表明 initrd 用第五章第 6 节的根哈希打开了 dm-verity 设备,状态为verified,标志为panic_on_corruption。数据区为 1154496 个扇区,即 591101952 字节;哈希区从第 1154504 个扇区开始,比数据区末尾多出的 8 个扇区是 4 KiB 的 dm-verity 超级块。overlayroot ... configured root with 'tmpfs:recurse=0'表明根目录已配置为 tmpfs 上层的 overlayfs。Initializing machine ID from VM UUID表明 machine-id 由实例自身生成,各实例互不相同。Attestation document generated只表示证明文档生成成功;RESULT=SUCCESS KMS accepted attestation才表示 KMS 校验签名并比对 PCR4 和 PCR12 通过,这一行是远程证明通过的标志。data key decrypted locally (32 bytes)表示实例用本次生成的私钥解开了 KMS 返回的CiphertextForRecipient,受保护服务随后启动。- 输出中没有
FAILED和DEPEND行,开机过程中没有失败的服务。
执行如下命令确认实例没有交互入口:
aws ssm describe-instance-information --region ap-northeast-1 \
--filters Key=InstanceIds,Values=i-0998950bf053d2de3 --query 'InstanceInformationList' --output json
aws ec2 get-console-output --region ap-northeast-1 --instance-id i-0998950bf053d2de3 \
--latest --output text | grep -c 'login:'
返回结果如下:
[]
0
Systems Manager 中没有该实例的注册信息,System Log 中也没有登录提示。实例只能通过 System Log 观察,无法登录或下发命令。
还可以在 CloudTrail 中从 KMS 的视角核对 PCR 值。执行如下命令:
aws cloudtrail lookup-events --region ap-northeast-1 \
--lookup-attributes AttributeKey=Username,AttributeValue=i-0998950bf053d2de3 \
--max-results 20 --query 'Events[?EventName==`GenerateDataKey`]|[-1].CloudTrailEvent' --output text \
| python3 -c 'import json,sys; e=json.loads(sys.stdin.read()); r=e["additionalEventData"]["recipient"]; keep=["attestationDocumentModuleId","attestationDocumentNitroTpmPCR4","attestationDocumentNitroTpmPCR7","attestationDocumentNitroTpmPCR12"]; print(json.dumps({"eventName":e["eventName"],"errorCode":e.get("errorCode"),"requestParameters":e["requestParameters"],"recipient":{k:r[k] for k in keep}}, indent=2))'
lookup-events 按时间倒序返回事件,|[-1] 取该实例最早的一次 GenerateDataKey 记录,即首次开机的请求。返回结果如下:
{
"eventName": "GenerateDataKey",
"errorCode": null,
"requestParameters": {
"keySpec": "AES_256",
"keyId": "alias/ubuntu-2404-nitro-tpm-test"
},
"recipient": {
"attestationDocumentModuleId": "i-0998950bf053d2de3-tpm0000000000000000",
"attestationDocumentNitroTpmPCR4": "0f74fdc8fc227354b9f45b5986fbfcf6fcdbb016477c795a625362517b2f015a843e804b6c669642527d3ba394833ba8",
"attestationDocumentNitroTpmPCR7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35",
"attestationDocumentNitroTpmPCR12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
}
KMS 收到的 PCR4 与第五章第 6 节离线计算的结果完全一致,PCR7 也与离线结果一致,errorCode 为 null。
3、篡改根分区:验证 dm-verity 触发内核 panic
本节模拟能够访问系统盘的攻击者:把测试实例的系统卷挂到另一台实例上,直接修改根分区中的门控脚本,企图让远程证明失败时也返回成功,但不更新 UKI。镜像中没有登录入口,修改只能离线进行。
在本机执行如下命令停止测试实例,把系统卷卸下并挂载到构建机:
TEST=i-0998950bf053d2de3
BUILD=i-0b84ac9db7e50906e
VOL=vol-0d8126a3fb3293d70
aws ec2 stop-instances --region ap-northeast-1 --instance-ids $TEST \
--query 'StoppingInstances[0].CurrentState.Name' --output text
aws ec2 wait instance-stopped --region ap-northeast-1 --instance-ids $TEST
aws ec2 detach-volume --region ap-northeast-1 --volume-id $VOL --query State --output text
aws ec2 wait volume-available --region ap-northeast-1 --volume-ids $VOL
aws ec2 attach-volume --region ap-northeast-1 --volume-id $VOL --instance-id $BUILD \
--device /dev/sdg --query State --output text
返回结果如下:
stopping
detaching
attaching
vol-0d8126a3fb3293d70 是测试实例的系统卷,可以通过 describe-instances 的 BlockDeviceMappings 查询。实例运行正常时,systemd-logind 处理关机信号,本文实测从停止实例到卷挂载到构建机共约 25 秒。
在构建机上执行如下命令,在根分区块设备上找到门控脚本 fail() 函数所在的位置,把其中的 exit 1 改为 exit 0,只修改 1 个字节:
VOL_ID=vol-0d8126a3fb3293d70
DEV=$(readlink -f /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${VOL_ID/-/})
P2=${DEV}p2
lsblk -o NAME,SIZE,FSTYPE "$DEV"
PATTERN='log "RESULT=FAILURE $*"; exit 1; }'
OFF=$(grep -obaF "$PATTERN" "$P2" | head -1 | cut -d: -f1)
echo "pattern offset in ${P2}: $OFF"
dd if="$P2" bs=1 skip="$OFF" count=${#PATTERN} status=none; echo
printf '0' | dd of="$P2" bs=1 seek=$((OFF + ${#PATTERN} - 4)) conv=notrunc status=none
sync
echo 3 > /proc/sys/vm/drop_caches
dd if="$P2" bs=1 skip="$OFF" count=${#PATTERN} status=none; echo
echo "modified 4 KiB data block number: $((OFF / 4096))"
返回结果如下:
NAME SIZE FSTYPE
nvme1n1 4G
├─nvme1n1p1 200M vfat
└─nvme1n1p2 2G erofs
pattern offset in /dev/nvme1n1p2: 559108464
log "RESULT=FAILURE $*"; exit 1; }
log "RESULT=FAILURE $*"; exit 0; }
modified 4 KiB data block number: 136501
被修改的字节位于根分区的第 136501 个 4 KiB 数据块。ESP 中的 UKI 没有改动,内核命令行中的根哈希仍是批准镜像的值。
在本机执行如下命令把系统卷挂回测试实例并启动:
aws ec2 detach-volume --region ap-northeast-1 --volume-id $VOL --query State --output text
aws ec2 wait volume-available --region ap-northeast-1 --volume-ids $VOL
aws ec2 attach-volume --region ap-northeast-1 --volume-id $VOL --instance-id $TEST \
--device /dev/xvda --query State --output text
aws ec2 wait volume-in-use --region ap-northeast-1 --volume-ids $VOL
aws ec2 start-instances --region ap-northeast-1 --instance-ids $TEST \
--query 'StartingInstances[0].CurrentState.Name' --output text
返回结果如下:
detaching
attaching
pending
实例启动约 1 分钟后,执行如下命令查看 System Log:
aws ec2 get-console-output --region ap-northeast-1 --instance-id $TEST --latest --output text \
| grep -aE 'Welcome|Hostname set|machine ID|is corrupted|Kernel panic|ATTEST-TEST'
返回结果如下:
Welcome to Ubuntu 24.04 LTS!
[ 1.373108] systemd[1]: Hostname set to <nitro-tpm-ubuntu>.
[ 1.376276] systemd[1]: Initializing machine ID from VM UUID.
[ 1.468571] device-mapper: verity: 259:2: data block 136501 is corrupted
[ 1.468594] Kernel panic - not syncing: dm-verity device corrupted
[ 1.486520] ---[ end Kernel panic - not syncing: dm-verity device corrupted ]---
完整的 System Log 中,两行 Kernel panic 之间是内核调用栈,其中包含 verity_handle_err 和 verity_verify_io 等 dm-verity 函数,说明 panic 由 dm-verity 的校验失败处理流程触发。
判断该场景防护生效的依据如下:
data block 136501 is corrupted中的块号与修改的数据块完全一致,dm-verity 在读取该数据块时发现其哈希与哈希树不一致。- 由于内核命令行带有
verity.options=panic-on-corruption,内核随即 panic,系统停止运行,开机后约 1.5 秒即终止。 - 输出中没有
ATTEST-TEST,被篡改的门控脚本没有机会执行,实例也没有向 KMS 发出请求。
注意:内核 panic 后操作系统不再响应关机信号,
aws ec2 stop-instances即使加上--force参数,本文实测也需要约 4.5 分钟才能进入stopped状态。
4、篡改根分区并同步更新 UKI:验证 KMS 拒绝发放密钥
本节模拟更进一步的攻击者:修改根文件系统中的受保护业务程序,并按第五章的方法重新生成 erofs、dm-verity 根哈希和 UKI,使新的根分区能够通过 dm-verity 校验。在构建机上执行如下命令修改业务程序,并把输出目录改为 out-tampered:
F=$ROOTFS/usr/sbin/nitro-tpm-protected-workload
cp -a "$F" "$WORK/workload.orig"
echo 'echo "[PROTECTED-WORKLOAD] tampered: extra command injected"' >> "$F"
cat "$F"
OUT=$WORK/out-tampered
mkdir -p "$OUT"
返回结果如下:
#!/bin/bash
# 受保护业务的占位程序,正式环境替换为实际应用的启动命令
echo "[PROTECTED-WORKLOAD] STARTED after remote attestation"
echo "[PROTECTED-WORKLOAD] tampered: extra command injected"
然后在同一终端中重新执行第五章第 5 节的 mkfs.erofs 与 veritysetup format 命令、第 6 节的组装内核命令行与 ukify build 命令,以及第 7 节的全部命令,生成篡改后的 disk.raw。关键输出如下:
Root hash: 6624f391252bc62c9a6bd1b4c25ee35a0bfaf68a038f70ed37e760a941449440
veritysetup verify: OK
Wrote unsigned /home/ubuntu/attestable-image/out-tampered/BOOTX64.EFI
{
"Measurements": {
"HashAlgorithm": "SHA384",
"PCR4": "5a4bb3eb17ecd25d33078a2dc763d9f85aaf69f25450f22d4f61f2bd8154bc879caffae27a1b9a3455f46c123dda03d1",
"PCR7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35",
"PCR12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
}
disk p2 verity verify: OK
只在业务程序末尾增加一行,根哈希就从 bd39fe0b…2d97f695 变为 6624f391…41449440,UKI 的 PCR4 也从 0f74fdc8…94833ba8 变为 5a4bb3eb…3dda03d1。生成完成后执行如下命令恢复构建机上的业务程序:
cp -a "$WORK/workload.orig" "$ROOTFS/usr/sbin/nitro-tpm-protected-workload"
此时测试实例仍处于内核 panic 状态。在本机执行第 3 节的命令把系统卷挂到构建机,其中 stop-instances 需要加上 --force 参数。然后在构建机上执行如下命令,把篡改后的整个磁盘镜像写入系统卷:
DEV=$(readlink -f /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${VOL_ID/-/})
dd if="$WORK/out-tampered/disk.raw" of="$DEV" bs=4M conv=fsync status=progress 2>&1 | tail -1
echo 3 > /proc/sys/vm/drop_caches
cmp "$WORK/out-tampered/disk.raw" "$DEV" && echo "cmp whole disk: identical"
返回结果如下:
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 36.713 s, 117 MB/s
cmp whole disk: identical
在本机执行第 3 节的命令把系统卷挂回测试实例并启动。实例启动约 1 分钟后,执行如下命令查看 System Log:
aws ec2 get-console-output --region ap-northeast-1 --instance-id $TEST --latest --output text \
| grep -aE 'nitrotpm-verity: root hash|ATTEST-TEST|nitro-tpm-attest-gate\[|FAILED|DEPEND'
返回结果如下:
nitrotpm-verity: root hash: 6624f391252bc62c9a6bd1b4c25ee35a0bfaf68a038f70ed37e760a941449440
[ 4.322146] nitro-tpm-attest-gate[379]: [ATTEST-TEST] Attestation document generated, size 5205 bytes
[ 6.087086] nitro-tpm-attest-gate[379]: [ATTEST-TEST] RESULT=FAILURE
[ 6.088328] nitro-tpm-attest-gate[379]: aws: [ERROR]: An error occurred (AccessDeniedException) when calling the GenerateDataKey operation: User: arn:aws:sts::<account-id>:assumed-role/<runtime-instance-role>/i-0998950bf053d2de3 is not authorized to perform: kms:GenerateDataKey on resource: arn:aws:kms:ap-northeast-1:<account-id>:key/<key-id> because no identity-based policy allows the kms:GenerateDataKey action
[FAILED] Failed to start nitro-tpm-attest-g…M attestation gate against AWS KMS.
[DEPEND] Dependency failed for nitro-tpm-pr…load gated by NitroTPM attestation.
判断远程证明失败的依据如下:
- dm-verity 使用篡改后的根哈希
6624f391…41449440打开了根分区,校验通过,实例正常开机,这一层防护被同步更新的 UKI 绕过。 Attestation document generated, size 5205 bytes说明证明文档仍然生成成功,失败点不在nitro-tpm-attest。- 证明文档中的 PCR4 是篡改后 UKI 的值,与 KMS 策略中的参考值不一致,KMS 返回
AccessDeniedException,没有返回CiphertextForRecipient,实例拿不到数据密钥。 - 门控服务启动失败,受保护服务因
Dependency failed没有启动。
在 CloudTrail 中,这次请求的 errorCode 为 AccessDenied,additionalEventData 为 null,即 KMS 拒绝请求时不记录证明文档中的 PCR 值。由于镜像没有登录入口,排查时应在构建机上用 nitro-tpm-pcr-compute 计算可疑 UKI 的 PCR4,与 KMS 策略中的参考值对比。
5、恢复原始镜像:验证正常引导和可证明
在本机执行第 3 节的命令把系统卷挂到构建机,实例运行正常,stop-instances 不需要 --force 参数。在构建机上执行如下命令,写回批准的磁盘镜像:
dd if="$WORK/out/disk.raw" of="$DEV" bs=4M conv=fsync status=progress 2>&1 | tail -1
echo 3 > /proc/sys/vm/drop_caches
cmp "$WORK/out/disk.raw" "$DEV" && echo "cmp whole disk: identical"
返回结果如下:
4294967296 bytes (4.3 GB, 4.0 GiB) copied, 36.7054 s, 117 MB/s
cmp whole disk: identical
在本机执行第 3 节的命令把系统卷挂回测试实例并启动。实例启动约 1 分钟后,执行如下命令查看 System Log:
aws ec2 get-console-output --region ap-northeast-1 --instance-id $TEST --latest --output text \
| grep -aE 'ATTEST-TEST|PROTECTED-WORKLOAD|FAILED|DEPEND'
返回结果如下:
[ 4.502303] nitro-tpm-attest-gate[378]: [ATTEST-TEST] Attestation document generated, size 5210 bytes
[ 6.353044] nitro-tpm-attest-gate[378]: [ATTEST-TEST] RESULT=SUCCESS KMS accepted attestation, data key decrypted locally (32 bytes)
[ 6.367980] nitro-tpm-protected-workload[402]: [PROTECTED-WORKLOAD] STARTED after remote attestation
写回批准的磁盘镜像后,PCR4 回到参考值,远程证明重新通过,受保护服务正常启动。至此完成全部验证,下面进行小结。
七、小结
本文的可行性测试表明,Ubuntu 24.04 可以构建出与 Amazon Linux 2023 示例镜像防护能力相同的可证明的 AMI。工具方面,通过 rustup 安装 Rust 1.93.0,从 NitroTPM-Tools v1.1.2 源代码编译 nitro-tpm-attest 与 nitro-tpm-pcr-compute,并用 dpkg-deb 打包为 deb 软件包。镜像方面,用 debootstrap 从最小根文件系统开始构建,用 erofs 与 dm-verity 实现只读且可校验的根分区,用自写的 initramfs-tools 脚本打开 dm-verity 设备,用 Ubuntu 的 overlayroot 把根目录叠加到内存中,用 ukify 把内核、initrd 和包含根哈希的内核命令行打包为 UKI,并去除了全部交互入口。
使用本文方案时需要注意以下风险与费用:
- 本文是可行性测试,initrd 脚本、overlayroot 配置和软件包清单都只在本文环境中验证过。用于生产环境前,还需要补充软件包来源校验、构建过程的可重复性、镜像发布审批等环节。
- 门控服务只用于展示远程证明结果。真正的保护目标是篡改后的镜像拿不到 KMS 管理的密钥和数据,业务应把关键数据用 KMS 密钥加密,而不是依赖门控服务阻止业务启动。
- 镜像只检查 PCR4 和 PCR12,不启用 UEFI Secure Boot。篡改根分区并同步更新 UKI 的镜像仍能开机,只是拿不到密钥;如需阻止这类镜像开机,需要启用 Secure Boot 并保管好签名私钥。
- 运行期在 tmpfs 叠加层中的修改不会改变 PCR,远程证明无法发现这类修改;重启后修改全部丢失。
- 内核、软件包或配置的任何更新都需要重新构建镜像、计算新的 PCR4、更新 KMS 策略并替换实例。
veritysetup format每次生成随机的 Salt,即使内容不变,重新构建也会得到新的 PCR4。 - 构建机、测试实例停止后仍产生 EBS 存储费用,AMI 依赖的快照产生快照存储费用,写入用的 EBS 卷在注册 AMI 后可以删除,KMS 客户托管密钥产生月费和 API 请求费用。删除实例、AMI、快照或计划删除 KMS 密钥均不可撤销,请在确认不再需要后执行。
八、参考文档
Amazon EC2 实例证明概述:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nitrotpm-attestation.html
可证明的 AMI(Attestable AMIs):
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/attestable-ami.html
将数据与自身运维人员隔离:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/isolate-data-operators.html
NitroTPM 的实例与 AMI 要求:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html
为 Linux AMI 启用 NitroTPM:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-support-on-ami.html
获取 NitroTPM 证明文档:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/attestation-get-doc.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
与 AWS KMS 集成:
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/attestation-attest.html
NitroTPM 的 KMS 条件键:
https://docs.aws.amazon.com/kms/latest/developerguide/conditions-nitro-tpm.html
通过 Systems Manager 公共参数查找 Ubuntu AMI:
https://documentation.ubuntu.com/aws/aws-how-to/instances/find-ubuntu-images/
NitroTPM-Tools 源代码仓库(nitro-tpm-attest 与 nitro-tpm-pcr-compute):
https://github.com/aws/NitroTPM-Tools
rustup 安装说明:
dpkg-deb 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man1/dpkg-deb.1.html
dpkg-shlibdeps 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man1/dpkg-shlibdeps.1.html
debootstrap 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man8/debootstrap.8.html
initramfs-tools 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man7/initramfs-tools.7.html
veritysetup 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man8/veritysetup.8.html
mkfs.erofs 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man1/mkfs.erofs.1.html
ukify 手册页(Ubuntu 24.04):
https://manpages.ubuntu.com/manpages/noble/man1/ukify.1.html
Linux 内核 dm-verity 文档:
https://docs.kernel.org/admin-guide/device-mapper/verity.html
overlayroot 所在的 cloud-initramfs-tools 项目:
https://launchpad.net/cloud-initramfs-tools
安装 AWS CLI v2:
https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
最后修改于 2026-11-02