Przeglądaj źródła

Use first-boot script to resume BitLocker

Unfortunately the "-RebootCount" parameter of "Suspend-BitLocker"
isn't honored, perhaps due to the fact that the disks are attached
to a different VM.

For this reason, we'll inject a first-boot script to resume
BitLocker explicitly.
Lucian Petrut 2 tygodni temu
rodzic
commit
6c39ca711d

+ 41 - 2
coriolis/osmorphing/osmount/windows.py

@@ -16,6 +16,13 @@ LOG = logging.getLogger(__name__)
 
 
 class WindowsMountTools(base.BaseOSMountTools):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # A list of BitLocker encrypted volumes that were unlocked
+        # by us. We'll use a first-boot script to resume BitLocker.
+        self._unlocked_volumes: list[str] = []
+
     def _connect(self):
         connection_info = self._connection_info
 
@@ -241,8 +248,11 @@ class WindowsMountTools(base.BaseOSMountTools):
         It doesn't decrypt the device, it just adds a publicly accessible
         BitLocker protector that automatically unlocks the volume.
 
-        When the replica instance boots, BitLocker will be automatically
-        enabled again and the TPM protector reconfigured.
+        When the replica instance boots, the TPM protector will be reconfigured
+        automatically. Unfortunately the '-RebootCount' parameter isn't
+        honored, perhaps due to the fact that the disks are attached to a
+        separate VM. For this reason, we'll use a first-boot script to resume
+        BitLocker explicitly.
         """
         self._conn.exec_ps_command(f'Suspend-BitLocker "{volume_id}"')
 
@@ -281,12 +291,41 @@ class WindowsMountTools(base.BaseOSMountTools):
             # We'll intentionally propagate the failure if we managed to
             # unlock the volume but failed to suspend BitLocker.
             self._suspend_bitlocker(encrypted_volume_id)
+            self._unlocked_volumes.append(encrypted_volume_id)
 
         if not unlocked:
             raise exception.CoriolisException(
                 "Could not unlock any volume using the specified "
                 "BitLocker recovery password.")
 
+    def install_encryption_firstboot_setup(
+        self,
+        os_root_dir,
+        os_morphing_tools,
+    ):
+        if not self._unlocked_volumes:
+            LOG.info(
+                "No unlocked BitLocker volumes, skipping first-boot setup.")
+            return
+
+        # We'll inject a first-boot script to resume BitLocker explicitly.
+        # Unfortunately the "-RebootCount" parameter of "Suspend-BitLocker"
+        # isn't honored, perhaps due to the fact that the disks are attached
+        # to a different VM.
+        script_content = ""
+        for encrypted_volume_id in self._unlocked_volumes:
+            LOG.info(
+                "Resuming BitLocker after first boot, volume: %s",
+                encrypted_volume_id)
+            script_content += f'Resume-BitLocker "{encrypted_volume_id}"\r\n'
+
+        # Resume BitLocker after bringing the disks online, which has a script
+        # priority of 10.
+        os_morphing_tools.register_firstboot_script(
+            script_content,
+            user_provided=False,
+            script_filename="11-bitlocker-firstboot.ps1")
+
     def mount_os(self):
         self._set_basic_disks_rw_mode()
         self._bring_disks_online()

+ 25 - 0
coriolis/tests/osmorphing/osmount/test_windows.py

@@ -451,6 +451,9 @@ class WindowsMountToolsTestCase(test_base.CoriolisBaseTestCase):
         mock_suspend_bitlocker.assert_called_once_with(
             mock.sentinel.volume1)
 
+        self.assertEqual(
+            self.tools._unlocked_volumes, [mock.sentinel.volume1])
+
     @mock.patch.object(windows.WindowsMountTools, "_get_encrypted_volume_ids")
     @mock.patch.object(windows.WindowsMountTools, "_unlock_encrypted_volume")
     @mock.patch.object(windows.WindowsMountTools, "_suspend_bitlocker")
@@ -481,3 +484,25 @@ class WindowsMountToolsTestCase(test_base.CoriolisBaseTestCase):
             [mock.call(mock.sentinel.volume0, fake_pass),
              mock.call(mock.sentinel.volume1, fake_pass)]
         )
+
+    def test_install_encryption_firstboot_setup_noop(self):
+        # No unlocked volumes, nothing to do.
+        mock_morphing_tools = mock.Mock()
+        self.tools.install_encryption_firstboot_setup(
+            mock.sentinel.os_root_dir,
+            mock_morphing_tools)
+        mock_morphing_tools.register_firstboot_script.assert_not_called()
+
+    def test_install_encryption_firstboot_setup(self):
+        self.tools._unlocked_volumes = ["vol1", "vol2"]
+        mock_morphing_tools = mock.Mock()
+        self.tools.install_encryption_firstboot_setup(
+            mock.sentinel.os_root_dir,
+            mock_morphing_tools)
+
+        expected_script = (
+            'Resume-BitLocker "vol1"\r\nResume-BitLocker "vol2"\r\n')
+        mock_morphing_tools.register_firstboot_script.assert_called_once_with(
+            expected_script,
+            user_provided=False,
+            script_filename="11-bitlocker-firstboot.ps1")