]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
ceph-volume: make TPM2 PCR policy configurable (default to PCR 7) 68670/head
authorGuillaume Abrioux <gabrioux@ibm.com>
Wed, 29 Apr 2026 09:17:23 +0000 (11:17 +0200)
committerGuillaume Abrioux <gabrioux@ibm.com>
Wed, 29 Apr 2026 09:17:23 +0000 (11:17 +0200)
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 <gabrioux@ibm.com>
src/ceph-volume/ceph_volume/devices/lvm/batch.py
src/ceph-volume/ceph_volume/devices/lvm/common.py
src/ceph-volume/ceph_volume/devices/raw/common.py
src/ceph-volume/ceph_volume/objectstore/baseobjectstore.py
src/ceph-volume/ceph_volume/tests/objectstore/test_baseobjectstore.py

index af29801f46ed00a66a2b839d7cdb7c319e929728..8887621bef5f073bf9995377c25697d72d7e8db6 100644 (file)
@@ -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',
index d2a7310e61534f418fd5687508a98718341b2db1..f9dc2ce60aef047e48141902175ef89610a589ac 100644 (file)
@@ -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',
index 8f8c258a84cd624d8ee196f94b7959632958e8a4..940c4d8127f10a2eb79fdc6eea9ad1b3b47f081e 100644 (file)
@@ -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',
index 345790bb7352e3b979e884011976581531c0562c..7520f3a05c577e20a1de01de86365e26e4e7ef2c 100644 (file)
@@ -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,
index a059fa0f54692d0489f39412ba88c20aa4138b61..290c53a640feae7053d38434d26fdc05d0f19660 100644 (file)
@@ -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([])