From ab7015c693a5e156e677e908378631d1bbdcb4f6 Mon Sep 17 00:00:00 2001 From: Guillaume Abrioux Date: Wed, 29 Apr 2026 11:17:23 +0200 Subject: [PATCH] ceph-volume: make TPM2 PCR policy configurable (default to PCR 7) tpm enrollment for dmcrypt OSDs is hardcoded to systemd-cryptenroll --tpm2-pcrs 9+12 which ties the LUKS key to initrd and kernel command line measurements, which is brittle on RHEL image mode systems: after a bootc switch, the kernel, initrd, or cmdline often change, the PCRs move, and the volume won't unlock until you re-enroll or fall back to another key. typical error: ``` Apr 27 14:17:25 ceph-jx5fq20u bash[4289]: Running command: nsenter --mount=/rootfs/proc/1/ns/mnt --ipc=/rootfs/proc/1/ns/ipc --net=/rootfs/proc/1/ns/net --uts=/rootfs/proc/1/ns/uts /usr/lib/systemd/systemd-cryptsetup attach M3zE7r-qsGZ-xs0T-610d-SJNZ-U89x-J0cJq8 /dev/ceph-cac05fb6-51d3-4a60-9fc1-4958c568b433/osd-block-b1a495a0-e1a4-4888-baf9-7990f45f1e56 - tpm2-device=auto,discard,headless=true,nofail Apr 27 14:17:26 ceph-jx5fq20u ceph-e5520e2c-420d-11f1-a7b9-5254001191fb-osd-0-activate[4300]: stderr: Failed to unseal secret using TPM2: Operation not permitted Apr 27 14:17:26 ceph-jx5fq20u bash[4289]: stderr: Failed to unseal secret using TPM2: Operation not permitted ``` The patch makes the PCR set configurable and defaults to 7 so bootc style deployments behave correctly. Fixes: https://tracker.ceph.com/issues/76318 Signed-off-by: Guillaume Abrioux --- .../ceph_volume/devices/lvm/batch.py | 9 +++++ .../ceph_volume/devices/lvm/common.py | 7 ++++ .../ceph_volume/devices/raw/common.py | 8 +++++ .../objectstore/baseobjectstore.py | 4 ++- .../tests/objectstore/test_baseobjectstore.py | 33 +++++++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/ceph-volume/ceph_volume/devices/lvm/batch.py b/src/ceph-volume/ceph_volume/devices/lvm/batch.py index af29801f46ed..8887621bef5f 100644 --- a/src/ceph-volume/ceph_volume/devices/lvm/batch.py +++ b/src/ceph-volume/ceph_volume/devices/lvm/batch.py @@ -215,6 +215,14 @@ class Batch(object): help='Whether encrypted OSDs should be enrolled with TPM.', action='store_true' ) + parser.add_argument( + '--tpm2-pcrs', + dest='tpm2_pcrs', + help=('PCRs for systemd-cryptenroll --tpm2-pcrs when using --with-tpm ' + '(default binds to Secure Boot policy, see systemd-cryptenroll(1)).'), + default='7', + type=str, + ) parser.add_argument( '--crush-device-class', dest='crush_device_class', @@ -398,6 +406,7 @@ class Batch(object): 'bluestore', 'dmcrypt', 'with_tpm', + 'tpm2_pcrs', 'crush_device_class', 'no_systemd', 'dmcrypt_format_opts', diff --git a/src/ceph-volume/ceph_volume/devices/lvm/common.py b/src/ceph-volume/ceph_volume/devices/lvm/common.py index d2a7310e6153..f9dc2ce60aef 100644 --- a/src/ceph-volume/ceph_volume/devices/lvm/common.py +++ b/src/ceph-volume/ceph_volume/devices/lvm/common.py @@ -97,6 +97,13 @@ common_args: Dict[str, Any] = { 'help': 'Whether encrypted OSDs should be enrolled with TPM.', 'action': 'store_true' }, + '--tpm2-pcrs': { + 'dest': 'tpm2_pcrs', + 'help': ('PCRs for systemd-cryptenroll --tpm2-pcrs when using --with-tpm ' + '(default binds to Secure Boot policy, see systemd-cryptenroll(1)).'), + 'default': '7', + 'type': str, + }, '--no-systemd': { 'dest': 'no_systemd', 'action': 'store_true', diff --git a/src/ceph-volume/ceph_volume/devices/raw/common.py b/src/ceph-volume/ceph_volume/devices/raw/common.py index 8f8c258a84cd..940c4d8127f1 100644 --- a/src/ceph-volume/ceph_volume/devices/raw/common.py +++ b/src/ceph-volume/ceph_volume/devices/raw/common.py @@ -64,6 +64,14 @@ def create_parser(prog: str, description: str) -> argparse.ArgumentParser: help='Whether encrypted OSDs should be enrolled with TPM.', action='store_true' ), + parser.add_argument( + '--tpm2-pcrs', + dest='tpm2_pcrs', + help=('PCRs for systemd-cryptenroll --tpm2-pcrs when using --with-tpm ' + '(default binds to Secure Boot policy, see systemd-cryptenroll(1)).'), + default='7', + type=str, + ), parser.add_argument( '--osd-id', help='Reuse an existing OSD id', diff --git a/src/ceph-volume/ceph_volume/objectstore/baseobjectstore.py b/src/ceph-volume/ceph_volume/objectstore/baseobjectstore.py index 345790bb7352..7520f3a05c57 100644 --- a/src/ceph-volume/ceph_volume/objectstore/baseobjectstore.py +++ b/src/ceph-volume/ceph_volume/objectstore/baseobjectstore.py @@ -38,6 +38,7 @@ class BaseObjectStore: self.block_device_path: str = '' self.dmcrypt_key: str = encryption_utils.create_dmcrypt_key() self.with_tpm: int = int(getattr(self.args, 'with_tpm', False)) + self.tpm2_pcrs: str = getattr(self.args, 'tpm2_pcrs', '7') self.method: str = '' self.osd_path: str = '' self.key: Optional[str] = None @@ -232,6 +233,7 @@ class BaseObjectStore: """ Enrolls a device with TPM2 (Trusted Platform Module 2.0) using systemd-cryptenroll. This method creates a temporary file to store the dmcrypt key and uses it to enroll the device. + PCR selection follows `--tpm2-pcrs` on the ceph-volume CLI (`self.tpm2_pcrs`, default is "7"). Args: device (str): The device path to be enrolled with TPM2. @@ -245,7 +247,7 @@ class BaseObjectStore: temp_file_name: str = temp_file.name.replace('/rootfs', '', 1) cmd: List[str] = ['systemd-cryptenroll', '--tpm2-device=auto', device, '--unlock-key-file', temp_file_name, - '--tpm2-pcrs', '9+12', '--wipe-slot', 'tpm2'] + '--tpm2-pcrs', self.tpm2_pcrs, '--wipe-slot', 'tpm2'] process.call(cmd, run_on_host=True, show_command=True) def add_label(self, key: str, diff --git a/src/ceph-volume/ceph_volume/tests/objectstore/test_baseobjectstore.py b/src/ceph-volume/ceph_volume/tests/objectstore/test_baseobjectstore.py index a059fa0f5469..290c53a640fe 100644 --- a/src/ceph-volume/ceph_volume/tests/objectstore/test_baseobjectstore.py +++ b/src/ceph-volume/ceph_volume/tests/objectstore/test_baseobjectstore.py @@ -276,6 +276,39 @@ class TestBaseObjectStore: with pytest.raises(NotImplementedError): BaseObjectStore([]).activate() + def test_enroll_tpm2_default_pcrs(self, monkeypatch, factory): + captured: dict = {} + + def fake_call(cmd, **kwargs): + captured['cmd'] = cmd + return ([], '', 0) + + monkeypatch.setattr( + 'ceph_volume.objectstore.baseobjectstore.process.call', fake_call) + args = factory(with_tpm=True) + bo = BaseObjectStore(args) + bo.dmcrypt_key = 'sekrit' + bo.enroll_tpm2('/dev/sdz') + assert '--tpm2-pcrs' in captured['cmd'] + i = captured['cmd'].index('--tpm2-pcrs') + assert captured['cmd'][i + 1] == '7' + + def test_enroll_tpm2_custom_pcrs(self, monkeypatch, factory): + captured: dict = {} + + def fake_call(cmd, **kwargs): + captured['cmd'] = cmd + return ([], '', 0) + + monkeypatch.setattr( + 'ceph_volume.objectstore.baseobjectstore.process.call', fake_call) + args = factory(with_tpm=True, tpm2_pcrs='9+12') + bo = BaseObjectStore(args) + bo.dmcrypt_key = 'sekrit' + bo.enroll_tpm2('/dev/sdz') + i = captured['cmd'].index('--tpm2-pcrs') + assert captured['cmd'][i + 1] == '9+12' + @patch('ceph_volume.objectstore.baseobjectstore.prepare_utils.create_key', Mock(return_value=['AQCee6ZkzhOrJRAAZWSvNC3KdXOpC2w8ly4AZQ=='])) def setup_method(self, m_create_key): self.b = BaseObjectStore([]) -- 2.47.3