From 338d0d24f997cddb0623a19e58b6cb27e8c439e9 Mon Sep 17 00:00:00 2001 From: Kai Lueke Date: Wed, 10 Apr 2024 17:27:46 +0900 Subject: [PATCH 1/2] kola: Add tests for encrypted root disks with TPM PCR binding As documented in https://github.com/flatcar/flatcar-website/pull/317 we can use PCR binding in Flatcar with some limitations and workarounds. I think we should be able to get rid of the first boot PCR difference by handling the first-boot flag detection in userspace and if we can guarantee that the outcome of setting the first boot or not is the same, i.e., we would have to always measure the effective Ignition config. For the rebinding on update the best bet we have is to create a Flatcar variant with sd-boot for signed PCR policies. Anyway, these are future topics and it's already good that we can make some encryption setups work. --- kola/tests/misc/tpm.go | 178 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 4 deletions(-) diff --git a/kola/tests/misc/tpm.go b/kola/tests/misc/tpm.go index c36e1e12f..def84b3bc 100644 --- a/kola/tests/misc/tpm.go +++ b/kola/tests/misc/tpm.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/coreos/go-semver/semver" "github.com/coreos/pkg/capnslog" @@ -17,6 +18,12 @@ import ( "github.com/flatcar/mantle/util" ) +const ( + VariantDefault string = "" + VariantNoUpdate string = "noupdate" + VariantWithUpdate string = "withupdate" +) + var ( // For now Ignition has no systemd-cryptenroll support and a helper service is used IgnitionConfigRootCryptenroll = conf.Butane(`--- @@ -47,6 +54,127 @@ systemd: ExecStart=rm /etc/luks/rootencrypted [Install] WantedBy=multi-user.target +`) + // Note: Keep the two below configs in sync with those + // documented in the Flatcar TPM docs. + // Ideally the reboot wouldn't be needed (or done in the initrd?) + IgnitionConfigRootCryptenrollPcrNoUpdate = conf.Butane(`--- +variant: flatcar +version: 1.0.0 +storage: + files: + - path: /etc/flatcar/update.conf + overwrite: true + contents: + inline: | + SERVER=disabled + luks: + - name: rootencrypted + wipe_volume: true + device: "/dev/disk/by-partlabel/ROOT" + filesystems: + - device: /dev/mapper/rootencrypted + format: ext4 + label: ROOT +systemd: + units: + - name: cryptenroll-helper-first.service + enabled: true + contents: | + [Unit] + ConditionFirstBoot=true + OnFailure=emergency.target + OnFailureJobMode=isolate + After=first-boot-complete.target multi-user.target + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=systemd-cryptenroll --tpm2-device=auto --unlock-key-file=/etc/luks/rootencrypted --tpm2-pcrs= /dev/disk/by-partlabel/ROOT + ExecStart=mv /etc/luks/rootencrypted /etc/luks/rootencrypted-bind + ExecStart=sleep 10 + ExecStart=systemctl reboot + [Install] + WantedBy=multi-user.target + - name: cryptenroll-helper-bind.service + enabled: true + contents: | + [Unit] + ConditionFirstBoot=false + ConditionPathExists=/etc/luks/rootencrypted-bind + OnFailure=emergency.target + OnFailureJobMode=isolate + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=systemd-cryptenroll --tpm2-device=auto --unlock-key-file=/etc/luks/rootencrypted-bind --tpm2-pcrs=4+7+8+9+11+12+13 --wipe-slot=tpm2 /dev/disk/by-partlabel/ROOT + ExecStart=mv /etc/luks/rootencrypted-bind /etc/luks/rootencrypted + [Install] + WantedBy=multi-user.target +`) + // The rebinding for the update is due to how GRUB measures things and + // we can only make this work without rebinding if we switch to sd-boot + IgnitionConfigRootCryptenrollPcrWithUpdate = conf.Butane(`--- +variant: flatcar +version: 1.0.0 +storage: + files: + - path: /oem/bin/oem-postinst + overwrite: true + mode: 0755 + contents: + inline: | + #!/bin/bash + set -euo pipefail + # When the update fails to correctly apply, this runs again + if [ -e /etc/luks/rootencrypted-bound ]; then + mv /etc/luks/rootencrypted-bound /etc/luks/rootencrypted-bind + fi + # But since a reboot inbetween could have bound it again, + # remove the PCR binding for every run + systemd-cryptenroll --tpm2-device=auto --unlock-key-file=/etc/luks/rootencrypted-bind --wipe-slot=tpm2 --tpm2-pcrs= /dev/disk/by-partlabel/ROOT + luks: + - name: rootencrypted + wipe_volume: true + device: "/dev/disk/by-partlabel/ROOT" + filesystems: + - device: /dev/mapper/rootencrypted + format: ext4 + label: ROOT +systemd: + units: + - name: cryptenroll-helper-first.service + enabled: true + contents: | + [Unit] + ConditionFirstBoot=true + OnFailure=emergency.target + OnFailureJobMode=isolate + After=first-boot-complete.target multi-user.target + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=systemd-cryptenroll --tpm2-device=auto --unlock-key-file=/etc/luks/rootencrypted --tpm2-pcrs= /dev/disk/by-partlabel/ROOT + ExecStart=mv /etc/luks/rootencrypted /etc/luks/rootencrypted-bind + ExecStart=sleep 10 + ExecStart=systemctl reboot + [Install] + WantedBy=multi-user.target + - name: cryptenroll-helper-bind.service + enabled: true + contents: | + [Unit] + ConditionFirstBoot=false + ConditionPathExists=/etc/luks/rootencrypted-bind + OnFailure=emergency.target + OnFailureJobMode=isolate + Before=update-engine.service + [Service] + Type=oneshot + RemainAfterExit=yes + ExecStart=systemd-cryptenroll --tpm2-device=auto --unlock-key-file=/etc/luks/rootencrypted-bind --tpm2-pcrs=4+7+8+9+11+12+13 --wipe-slot=tpm2 /dev/disk/by-partlabel/ROOT + ExecStart=mv /etc/luks/rootencrypted-bind /etc/luks/rootencrypted-bound + [Install] + WantedBy=multi-user.target `) IgnitionConfigRootTPM = `{ "ignition": { @@ -124,7 +252,7 @@ systemd: func init() { runRootTPMCryptenroll := func(c cluster.TestCluster) { - tpmTest(c, IgnitionConfigRootCryptenroll, "/") + tpmTest(c, IgnitionConfigRootCryptenroll, "/", VariantDefault) } register.Register(®ister.Test{ Run: runRootTPMCryptenroll, @@ -134,9 +262,31 @@ func init() { Distros: []string{"cl"}, MinVersion: semver.Version{Major: 3913, Minor: 0, Patch: 1}, }) + runRootTPMCryptenrollPcrNoUpdate := func(c cluster.TestCluster) { + tpmTest(c, IgnitionConfigRootCryptenrollPcrNoUpdate, "/", VariantNoUpdate) + } + register.Register(®ister.Test{ + Run: runRootTPMCryptenrollPcrNoUpdate, + ClusterSize: 0, + Platforms: []string{"qemu"}, + Name: "cl.tpm.root-cryptenroll-pcr-noupdate", + Distros: []string{"cl"}, + MinVersion: semver.Version{Major: 3913, Minor: 0, Patch: 1}, + }) + runRootTPMCryptenrollPcrWithUpdate := func(c cluster.TestCluster) { + tpmTest(c, IgnitionConfigRootCryptenrollPcrWithUpdate, "/", VariantWithUpdate) + } + register.Register(®ister.Test{ + Run: runRootTPMCryptenrollPcrWithUpdate, + ClusterSize: 0, + Platforms: []string{"qemu"}, + Name: "cl.tpm.root-cryptenroll-pcr-withupdate", + Distros: []string{"cl"}, + MinVersion: semver.Version{Major: 3913, Minor: 0, Patch: 1}, + }) runRootTPM := func(c cluster.TestCluster) { - tpmTest(c, conf.Ignition(IgnitionConfigRootTPM), "/") + tpmTest(c, conf.Ignition(IgnitionConfigRootTPM), "/", VariantDefault) } register.Register(®ister.Test{ Run: runRootTPM, @@ -148,7 +298,7 @@ func init() { }) runNonRootTPM := func(c cluster.TestCluster) { - tpmTest(c, conf.Ignition(IgnitionConfigNonRootTPM), "/mnt/data") + tpmTest(c, conf.Ignition(IgnitionConfigNonRootTPM), "/mnt/data", VariantDefault) } register.Register(®ister.Test{ Run: runNonRootTPM, @@ -160,7 +310,7 @@ func init() { }) } -func tpmTest(c cluster.TestCluster, userData *conf.UserData, mountpoint string) { +func tpmTest(c cluster.TestCluster, userData *conf.UserData, mountpoint string, variant string) { swtpm, err := startSwtpm() if err != nil { c.Fatalf("could not start software TPM emulation: %v", err) @@ -188,6 +338,13 @@ func tpmTest(c cluster.TestCluster, userData *conf.UserData, mountpoint string) c.Fatal(err) } + if variant == VariantNoUpdate || variant == VariantWithUpdate { + // Wait for the first reboot + time.Sleep(1 * time.Minute) + // Verify that the machine rebooted + _ = c.MustSSH(m, "grep -v flatcar.first_boot /proc/cmdline") + } + checkIfMountpointIsEncrypted(c, m, mountpoint) // Make sure the change is reboot-safe. This is especially important for the case of an encrypted root disk because the @@ -199,6 +356,19 @@ func tpmTest(c cluster.TestCluster, userData *conf.UserData, mountpoint string) } checkIfMountpointIsEncrypted(c, m, mountpoint) + + if variant == VariantWithUpdate { + // Simulate update effect by + // affecting the GRUB PCR values for the next boot + _ = c.MustSSH(m, "echo 'set linux_append=\"flatcar.autologin console=ttyS0,115200 quiet\"' | sudo tee -a /oem/grub.cfg") + // and calling the OEM hook we set up + _ = c.MustSSH(m, "sudo /oem/bin/oem-postinst") + err := m.Reboot() + if err != nil { + c.Fatalf("could not reboot machine: %v", err) + } + checkIfMountpointIsEncrypted(c, m, "/") + } } type softwareTPM struct { From 3c4d6329e0a7f2fd6eb26649662281ddcda657f8 Mon Sep 17 00:00:00 2001 From: Kai Lueke Date: Thu, 11 Apr 2024 14:30:52 +0900 Subject: [PATCH 2/2] kola: Align Butane YAML indentation for TPM tests --- kola/tests/misc/tpm.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/kola/tests/misc/tpm.go b/kola/tests/misc/tpm.go index def84b3bc..c32b1c92b 100644 --- a/kola/tests/misc/tpm.go +++ b/kola/tests/misc/tpm.go @@ -31,9 +31,9 @@ variant: flatcar version: 1.0.0 storage: luks: - - name: rootencrypted - wipe_volume: true - device: "/dev/disk/by-partlabel/ROOT" + - name: rootencrypted + wipe_volume: true + device: "/dev/disk/by-partlabel/ROOT" filesystems: - device: /dev/mapper/rootencrypted format: ext4 @@ -69,9 +69,9 @@ storage: inline: | SERVER=disabled luks: - - name: rootencrypted - wipe_volume: true - device: "/dev/disk/by-partlabel/ROOT" + - name: rootencrypted + wipe_volume: true + device: "/dev/disk/by-partlabel/ROOT" filesystems: - device: /dev/mapper/rootencrypted format: ext4 @@ -133,9 +133,9 @@ storage: # remove the PCR binding for every run systemd-cryptenroll --tpm2-device=auto --unlock-key-file=/etc/luks/rootencrypted-bind --wipe-slot=tpm2 --tpm2-pcrs= /dev/disk/by-partlabel/ROOT luks: - - name: rootencrypted - wipe_volume: true - device: "/dev/disk/by-partlabel/ROOT" + - name: rootencrypted + wipe_volume: true + device: "/dev/disk/by-partlabel/ROOT" filesystems: - device: /dev/mapper/rootencrypted format: ext4