How to install Fedora/RHEL/CentOS via kickstart on an existing LUKS device

Kickstart installations let us easily script and replicate unattended or semi-unattended installations of Fedora, Red Hat Enterprise Linux or CentOS. The instructions needed to install the operating system are specified, with a dedicated syntax, inside a Kickstart file which is passed to the Anaconda installer. In this tutorial we will see how to reuse an already existing LUKS (Linux Unified Keys Setup) container when performing a Kickstart installation: this is something that cannot be achieved just with Kickstart instructions and requires some extra steps.

In this tutorial you will learn:

  • How to use an existing LUKS container when performing a Kickstart installation of Fedora, RHEL or CentOS
  • How to create and use an updates.img file to be used with the Anaconda installer.

How to install Fedora/RHEL/CentOS via kickstart on an existing LUKS device

How to install Fedora/RHEL/CentOS via kickstart on an existing LUKS device

Software Requirements and Conventions Used

Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Fedora/Rhel/CentOS
Software No specific software is needed to follow this tutorial.
Other
  • Knowledge of the Kickstart syntax
  • Knowledge of LUKS (Linux Unified Key Setup) and the cryptsetup command.
Conventions # – requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux commands to be executed as a regular non-privileged user

Introduction

Kickstart let us easily replicate and customize operating system installations in ways that are simply impossible to achieve from the Anaconda graphical installer. We can, for example, declare what packages or package groups should be installed on the system and what should be excluded instead.

We also have the chance to execute custom commands before or after the installation is performed, specifying them inside the dedicated %pre and %post sections of the Kickstart file respectively. We will take advantage of this last mentioned feature to use an already existing LUKS device during the installation process.

Encryption with native Kickstart syntax

Creating LUKS containers is quite easy, and can be done by just using native kickstart instructions. Here is an example:



part pv.01 --ondisk=sda --encrypted --luks-type=luks1 --cipher=aes-xts-plain64 --pbkdf-time=5000 --passphrase=secretpassphrase

In the above example, by using the part instruction, we create an encrypted lvm physical volume on the /dev/sda disk. We specify the LUKS version to use (luks1 in this case – at least in recent versions of Fedora luks2 has become the default), the cipher, and the time, expressed in milliseconds, to spend for PBKDF ( Password-Based Key Derivation Function) passphrase processing (it is the equivalent of using the --iter-time option of cryptsetup).

Even if  it is not a safe habit, we used also the --passphrase to provide the encryption passphrase: without this option, the installation process would be interrupted, and we would be prompted to provide one interactively.

We can clearly see how, using Kickstart, we get a lot more flexibility compared to a traditional installation; why would we need to perform extra steps, then? There are still some tasks we cannot achieve using just the standard Kickstart syntax. Among other things, we cannot create LUKS containers on raw devices (only on partitions) or specify the hashing algorithm to use for the LUKS key setup, which by default is set to sha256 (nothing wrong with it).

For these reasons we may want to create our partition setup before performing the installation, either manually or by using tools like parted inside the %pre section of the kickstart file itself. We may also just have an existing LUKS setup we don’t want to destroy. In all these cases we must performs the extra steps we will see in a moment.

The kickstart %pre section

The %pre section of a kickstart file is the first one to be parsed when the file is retrieved. It is used to perform custom commands before the installation starts and must be closed explicitly with the %end instruction.

In %pre, the bash shell interpreter is used by default, but others can be specified via the --interpreter option (to use python we would write %pre --interpreter /usr/bin/python). We can use this section to run the commands required to open the existent LUKS container. Here is what we can write:

%pre
iotty="$(tty)"
exec > "${iotty}" 2> "${iotty}"

while true; do
  cryptsetup luksOpen /dev/sda1 cryptroot - && break
done
%end

Let’s take a look at the code above. First of all, we store the result of the tty command, which prints the file name of the terminal connected to standard input, into the iotty variable.

With the exec > "${iotty}" 2> "${iotty}" command we redirected standard output and standard error to the same terminal:
this way we will be able to enter the container password when the crytpsetup luksOpen command will be executed and the prompt will be displayed on screen. The command is launched in an infinite loop which is interrupted only if the LUKS container is successfully opened.

If we want need to run a completely unattended installation, we must pass the passphrase directly to cryptsetup (again, this is not recommended). We would write:

%pre
echo -n "ourverysecretpassphrase" | cryptsetup luksOpen /dev/sda1 cryptroot -
%end

In the example above we passed the passphrase to the standard input of the cryptsetup command via a pipe |: we used the echo command with the -n option to avoid a newline character to be appended at the end of the passphrase.

Patching Fedora 31 anaconda installer

If we try to use an unlocked LUKS container when installing Fedora 31 via Kickstart, we will receive the following
message, and the process will be aborted:

The existing unlocked LUKS device cannot be used for the installation without an encryption key specified for this
device. Please, rescan the storage.

This happens due to this commit introduced in the Fedora 31 version of the Anaconda installer. The code basically checks that an existing LUKS device has a registered key, if it doesn’t the installation is aborted. The problem is that blivet, the python library used by Anaconda to manage partition acquires the key only if the container is opened by it: this can be done from the graphical installer but there is not, at the moment of writing, a Kickstart instruction to unlock an existing  LUKS container. I personally commented the commit explaining the situation, and a bug has been opened on red hat bugzilla.

Creating an updates.img file

At the moment the only workaround (that I know of) is to patch the Anaconda source code, commenting the line which executes the control introduced with the commit we mentioned above. The good news is that it is a very simple to operation.

As a first thing, we need to clone the Anaconda git repository, specifically the f31-release branch:

$ git clone https://github.com/rhinstaller/anaconda -b f31-release


Once the repo is cloned, we enter the anaconda directory and modify the pyanaconda/storage/checker.py file: all we have to do is to comment line 619:

def set_default_checks(self):
        """Set the default checks."""
        self.checks = list()
        self.add_check(verify_root)
        self.add_check(verify_s390_constraints)
        self.add_check(verify_partition_formatting)
        self.add_check(verify_partition_sizes)
        self.add_check(verify_partition_format_sizes)
        self.add_check(verify_bootloader)
        self.add_check(verify_gpt_biosboot)
        self.add_check(verify_swap)
        self.add_check(verify_swap_uuid)
        self.add_check(verify_mountpoints_on_linuxfs)
        self.add_check(verify_mountpoints_on_root)
       #self.add_check(verify_unlocked_devices_have_key)
        self.add_check(verify_luks_devices_have_key)
        self.add_check(verify_luks2_memory_requirements)
        self.add_check(verify_mounted_partitions)

We save the modification and, from the root of the repository, we launch the makeupdates script which is found in the scripts directory. For the script to be executed we must have python2 installed:

$ ./scripts/makeupdates

The script will generate the updates.img file which will contain our modifications. To check its content we can use the lsinitrd command:

$ lsinitrd updates.img
Image: updates.img: 8.0K
========================================================================
Version:

Arguments:
dracut modules:
========================================================================
drwxr-xr-x   3 egdoc    egdoc           0 Jan 30 09:29 .
drwxr-xr-x   3 egdoc    egdoc           0 Jan 30 09:29 run
drwxr-xr-x   3 egdoc    egdoc           0 Jan 30 09:29 run/install
drwxr-xr-x   3 egdoc    egdoc           0 Jan 30 09:29 run/install/updates
drwxr-xr-x   3 egdoc    egdoc           0 Jan 30 09:29 run/install/updates/pyanaconda
drwxr-xr-x   2 egdoc    egdoc           0 Jan 30 09:29 run/install/updates/pyanaconda/storage
-rw-r--r--   1 egdoc    egdoc       25443 Jan 30 09:29 run/install/updates/pyanaconda/storage/checker.py
========================================================================

We will use this file to “patch” the installer of Fedora 31.

Applying the patch

To apply the modifications contained in the file we just generated, we need to place it somewhere where we can easily access it, perhaps via ftp or http, or even on a local block device, and use the inst.updates parameter to reference it from the Fedora installer image. From the grub menu we highlight the “Install Fedora” menu entry:


fedora31-installer-menu

Fedora 31 installer menu


Once the menu line is selected, we press the Tab key: the kernel command line associated with the entry is displayed on the bottom of the screen:


fedora31-installer-cmdline

The kernel command line used by the “Install Fedora” entry All we have to do now is to append the inst.updates instruction and provide the path to the updates.img file we created. Supposing both the Kickstart and the updates.img file are accessible via http on a local server with ip 192.168.0.37, we would write:
vmlinuz initrd=initrd.img inst.stage2=hd:LABEL=Fedora-S-dvd-x86_31-31 quiet
inst.updates=http://192.168.0.37/updates.img inst.ks=http://192.168.0.37/ks.cfg

At this point we can press enter to boot. With the above modification the installer will not complain anymore about
the unlocked LUKS device, and the installation will proceed without problems.

Conclusions

In this article we saw how to tune a kickstart installation in order to reuse an already existing LUKS device, unlocking it in the %pre section of the kickstart file, and how to apply a small workaround to the Fedora 31 Anaconda installer which would otherwise fail when such type of installation is attempted. If you are curious about the Kickstart syntax please take a look at the online documentation.