From 43cd86dfaf41075bf941eec8355eb1ddc78c276a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?= Date: Tue, 12 Nov 2024 09:52:44 +0100 Subject: [PATCH] Add random key support for LUKS encryption Allow to pass luks="random". In random mode use the generated keyfile as the only key to decrypt. This is only secure if the generated initrd also gets protected e.g. through encryption like it is done with the secure linux execution on zSystems --- doc/source/image_description/elements.rst | 31 ++++++++++++---- kiwi/builder/disk.py | 2 +- kiwi/storage/luks_device.py | 29 +++++++++++---- test/unit/storage/luks_device_test.py | 43 +++++++++++++++++++++++ 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/doc/source/image_description/elements.rst b/doc/source/image_description/elements.rst index 43249a8e61e..d8dcb5a87ef 100644 --- a/doc/source/image_description/elements.rst +++ b/doc/source/image_description/elements.rst @@ -566,15 +566,34 @@ root_clone="number" boot_clone="number" Same as `root_clone` but applied to the boot partition if present -luks="passphrase|file:///path/to/keyfile": +luks="passphrase|file:///path/to/keyfile|random": Supplying a value will trigger the encryption of the partition serving the root filesystem using the LUKS extension. The supplied value represents either the passphrase string or the location of - a key file if specified as `file://...` resource. When using - a key file it is in the responsibility of the user how - this key file is actually being used. By default any - distribution will just open an interactive dialog asking - for the credentials at boot time ! + a key file if specified as `file://...` resource or the reserved + name `random`. When using a passphrase the system will interactively + ask for that passphrase on first boot unless it is set empty. + In case of an empty passphrase the system cannot be considered secure. + When using a key file the information from the file is read and + used as a passphrase. The given key file is **not automatically** + placed into the system or added to the `etc/crypttab` which means + the passphrase in the key file is by default requested from an + interactive dialog at boot time. When using the reserved word + `random`, kiwi will create a key file with a random passphrase + and place this information into `etc/crypttab`. This allows + the system to boot without user interaction but also requires + the initrd to be protected in some way because it will contain + the keyfile. The use of `random` is therefore only secure if + the image adds additional security that encrypts the initrd + like it is e.g. done in the IBM secure execution process. + If the encryption of the system is combined with the attribute + `bootpartition="false"` it's important to understand that this + will place `/boot` into the encrypted area of the system and + leaves reading boot data from it as a responsibility to the + bootloader. Not every bootloader can cope with that and those + that can e.g. grub will then open an interactive dialog at + the bootloader level asking for the credentials to decrypt the + root filesystem. luks_version="luks|luks1|luks2": Specify which `LUKS` version should be used. If not set and by diff --git a/kiwi/builder/disk.py b/kiwi/builder/disk.py index 52f07be3742..15856dcb9ed 100644 --- a/kiwi/builder/disk.py +++ b/kiwi/builder/disk.py @@ -667,7 +667,7 @@ def _map_luks( # LUKS pool without asking # luks_need_keyfile = \ - True if self.boot_is_crypto or self.luks == '' else False + True if self.boot_is_crypto or self.luks == '' or self.luks == 'random' else False luks_root.create_crypto_luks( passphrase=self.luks or '', osname=self.luks_os, diff --git a/kiwi/storage/luks_device.py b/kiwi/storage/luks_device.py index ba90ad9ce28..ab72b7d9a87 100644 --- a/kiwi/storage/luks_device.py +++ b/kiwi/storage/luks_device.py @@ -108,6 +108,13 @@ def create_crypto_luks( if not passphrase: log.warning('Using an empty passphrase for the key setup') + if keyfile: + self.luks_keyfile = keyfile + keyfile_path = os.path.normpath( + os.sep.join([root_dir, self.luks_keyfile]) + ) + LuksDevice.create_random_keyfile(keyfile_path) + if randomize: log.info('--> Randomizing...') storage_size_mbytes = self.storage_provider.get_byte_size( @@ -123,12 +130,26 @@ def create_crypto_luks( log.info('--> Creating LUKS map') - if passphrase: + if passphrase and passphrase == 'random': + # In random mode use the generated keyfile as the only + # key to decrypt. This is only secure if the generated + # initrd also gets protected, e.g through encryption + # like it is done with the secure linux execution on + # zSystems + passphrase_file = keyfile_path + # Do not add an additional keyfile + keyfile = '' + elif passphrase: + # Setup a passphrase file for which the system will + # ask for in an interactive dialog passphrase_file_tmp = Temporary().new_file() with open(passphrase_file_tmp.name, 'w') as credentials: credentials.write(passphrase) passphrase_file = passphrase_file_tmp.name else: + # Setup an empty passphrase, insecure and only useful + # for initial deployment which then applies a process + # to secure the image e.g reencrypt passphrase_file_zero = '/dev/zero' extra_options = [ '--keyfile-size', '32' @@ -142,12 +163,8 @@ def create_crypto_luks( 'luksFormat', storage_device ] ) + if keyfile: - self.luks_keyfile = keyfile - keyfile_path = os.path.normpath( - os.sep.join([root_dir, self.luks_keyfile]) - ) - LuksDevice.create_random_keyfile(keyfile_path) Command.run( [ 'cryptsetup', '--key-file', passphrase_file diff --git a/test/unit/storage/luks_device_test.py b/test/unit/storage/luks_device_test.py index b0248f3b753..69c41c6f423 100644 --- a/test/unit/storage/luks_device_test.py +++ b/test/unit/storage/luks_device_test.py @@ -95,6 +95,49 @@ def test_create_crypto_luks_empty_passphrase( ] self.luks.luks_device = None + @patch('kiwi.storage.luks_device.LuksDevice') + @patch('kiwi.storage.luks_device.Command.run') + @patch('kiwi.storage.luks_device.Temporary.new_file') + @patch('os.chmod') + def test_create_crypto_luks_random_passphrase( + self, mock_os_chmod, mock_tmpfile, mock_command, mock_LuksDevice + ): + tmpfile = Mock() + tmpfile.name = 'tmpfile' + mock_tmpfile.return_value = tmpfile + with patch('builtins.open', create=True): + self.luks.create_crypto_luks( + passphrase='random', osname='sle12', + keyfile='some-keyfile', root_dir='root' + ) + assert mock_command.call_args_list == [ + call( + [ + 'dd', 'if=/dev/urandom', 'bs=1M', 'count=1', + 'of=/dev/some-device' + ] + ), + call( + [ + 'cryptsetup', '-q', '--key-file', 'root/some-keyfile', + '--cipher', 'aes-xts-plain64', + '--key-size', '256', '--hash', 'sha1', + 'luksFormat', '/dev/some-device' + ] + ), + call( + [ + 'cryptsetup', '--key-file', 'root/some-keyfile', 'luksOpen', + '/dev/some-device', 'luksRoot' + ] + ) + ] + mock_LuksDevice.create_random_keyfile.assert_called_once_with( + 'root/some-keyfile' + ) + assert self.luks.luks_keyfile == 'some-keyfile' + self.luks.luks_device = '' + @patch('kiwi.storage.luks_device.LuksDevice') @patch('kiwi.storage.luks_device.Command.run') @patch('kiwi.storage.luks_device.Temporary.new_file')