# How to create a custom Debian virtual machine installation using preseed and virt-install

## Introduction

Debian [preseed](https://wiki.debian.org/DebianInstaller/Preseed) is a method for automating the installation of Debian, Ubuntu, and other Debian derivatives. By using a preseed file (`preseed.cfg`), we can predefine answers to the questions that the installer asks, allowing for an unattended installation process. This is particularly useful for deploying multiple systems with a consistent configuration, saving time and reducing the potential for human error.

We'll start with a simple preseed file to get the basic installation up and running. From there, we'll gradually build upon it, incorporating features like full disk encryption for enhanced security, adding extra packages to tailor the system to our needs, and running a few commands for post-install configuration to ensure everything is set up correctly.

## Simple preseed

Let's start with the following preseed:

```plaintext
# Localization and keymap (US)
d-i debian-installer/locale string en_US
d-i keyboard-configuration/xkb-keymap select us

# Automatic network configuration
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string debian
d-i netcfg/get_domain string local

# Debian mirror settings
d-i mirror/country string manual
d-i mirror/http/hostname string ftp.debian.org
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string 

# Account setup (root/toor and user/resu)
d-i passwd/root-password-crypted password $6$I8U7yNFep4gy/oTc$9jqHSPtccHF7jsE4k1astCfy2Ua/UX5fFHXU1mKWaXsU6shipTBmdV5pxa2vcw2xpFkm7BdzZT1E9BBbLG0jY/
d-i passwd/root-password-again-crypted password $6$I8U7yNFep4gy/oTc$9jqHSPtccHF7jsE4k1astCfy2Ua/UX5fFHXU1mKWaXsU6shipTBmdV5pxa2vcw2xpFkm7BdzZT1E9BBbLG0jY/
d-i passwd/user-fullname string User
d-i passwd/username string user
d-i passwd/user-password-crypted password $6$HidIccPq4vpzdXSs$KBviV/Z7ZavtDpoCnOxAqfZfSm6VS8G9n14NL1K39vYOZ.uRuEq996U/C1qO25l.L3wgaXJtigpYnBRlH2/8T/
d-i passwd/user-password-again-crypted password $6$HidIccPq4vpzdXSs$KBviV/Z7ZavtDpoCnOxAqfZfSm6VS8G9n14NL1K39vYOZ.uRuEq996U/C1qO25l.L3wgaXJtigpYnBRlH2/8T/

# Clock and time zone setup: UTC
d-i clock-setup/utc boolean true
d-i time/zone string UTC

# Partitioning: guided partitioning, everything in the same partition
d-i partman-auto/disk string /dev/vda
d-i partman-auto/method string regular
d-i partman-auto/choose_recipe select atomic
d-i partman/choose_partition select finish
d-i partman/confirm write_new_label boolean true
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/confirm_nochanges boolean true

# Package selection: just SSH server
tasksel tasksel/first multiselect standard, ssh-server

# Don't participate in the package usage survey
popularity-contest popularity-contest/participate boolean false

# Install the bootloader on /dev/vda
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean false
d-i grub-installer/bootdev string /dev/vda

# Finishing up
d-i finish-install/reboot_in_progress note
```

The Debian Installer (`d-i`) is a set of tools and scripts used to install Debian and its derivatives. In the preseed file, it automates the installation process by providing predefined answers to installation questions.

In order, this preseed file does the following:

* Sets`en_US` locale and US keyboard
    
* Automatic network configuration and no proxy for [`apt`](https://en.wikipedia.org/wiki/APT_(software))
    
* Sets`root` password: `toor`
    
* Create new user called `user`, with a password of `resu`
    
* Sets the timezone to UTC
    
* Automatic partitioning
    
* Installs SSH server
    
* Doesn't participate in package survery
    
* Adds bootloader
    

**Notes**:

* Hashes for passwords can be generated with `openssl passwd -6` (Install `openssl` package if necessary).
    
* When the system is installed, the output from the kernel won't be displayed at boot time, so it will go from "Loading initrd" to the login/password prompt in the console. In most cases that's not an issue, but when something is taking a long time at boot, it will look like it's stuck at "***Loading initrd***". See below at the end of the *Full Disk Encryption* section to change that.
    

### Installation

Assuming the preseed file is in `/tmp/preseed.cfg`, run the following for an unattended installation:

```bash
sudo virt-install --install debian12 \
    --graphics none \
    --console pty,target_type=serial \
    --noautoconsole \
    --initrd-inject /tmp/preseed.cfg \
    --extra-args="console=ttyS0,115200n8 preseed/file=/preseed.cfg"
```

Let's skip the first few parameters (as they are explained in [a previous blog post](https://bluespruce.io/using-virt-install-to-create-unattended-virtual-machine-install)) and go over the changes:

* `--initrd-inject /tmp/preseed.cfg`: We are injecting a preseed file (into `/`)
    
* `preseed/file=/preseed.cfg`: in the kernel arguments, we are specifying the preseed is a file and its path.
    

## Full disk encryption

For a [FDE](https://en.wikipedia.org/wiki/Full_disk_encryption), we need to replace the `d-i` section with `partman` in the above preseed with the following (we're using a passphrase of `password1`):

```plaintext
# Partitioning
d-i partman-auto/disk string /dev/vda
d-i partman-auto/method string crypto
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true

# You can choose one of the three predefined partitioning recipes:
# - atomic: all files in one partition
# - home: separate /home partition
# - multi: separate /home, /usr, /var, and /tmp partitions
d-i partman-auto/choose_recipe select atomic

# This makes partman automatically partition without confirmation.
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true

# Encrypted LVM
d-i partman-crypto/passphrase password password1
d-i partman-crypto/passphrase-again password password1
d-i partman-crypto/dmraid/erase_disks boolean false
d-i partman-auto-crypto/erase_disks boolean false

# LVM
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic
d-i partman-auto-lvm/new_vg_name string debian-vg
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true

# Filesystems
d-i partman-auto-lvm/new_vg_name string debian-vg
d-i partman-lvm/choose_partition string finish
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
```

In this automated disk partitioning setup for a BIOS system using `/dev/vda`, we configure full-disk encryption (FDE) with [LVM](https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)) and [LUKS](https://en.wikipedia.org/wiki/LUKS). The configuration creates a small `/boot` partition with [ext2](https://en.wikipedia.org/wiki/Ext2), a root partition that occupies the majority of the disk, and a 2GB swap partition at the end. The entire disk is encrypted with LUKS, and the logical volumes for root and swap are managed within an LVM volume group named `crypt`.

Additionally, we need to run the following two commands after the installation:

```plaintext
sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="console=ttyS0,115200n8"/' /etc/default/grub
update-grub
```

While text gets displayed on the console, the output requesting to enter the passphrase (as well as the output from the kernel when booting) doesn't get shown unless we force the console in the kernel parameters, and it looks like the boot process is hanging at loading the `initrd`.

When integrating it into the `preseed` file, we need to modify `preseed/late_command`, which now looks like:

```plaintext
d-i preseed/late_command string \
    in-target sed -i 's/GRUB_CMDLINE_LINUX=""/GRUB_CMDLINE_LINUX="console=ttyS0,115200n8"/' /etc/default/grub; \
    in-target update-grub;
```

## Adding packages

If we want to add more packages, we need to add them on a line starting with `d-i pkgsel/include`. If we add [Docker](https://en.wikipedia.org/wiki/Docker_(software)), it will look like:

```plaintext
d-i pkgsel/include string docker.io
```

## What else can we do?

Using the `preseed/late_command` section, we can script it however we want. For multiline or multiple commands, we need to use `\`, and end each command with `;`. Bear in mind that the installation is done in a `chroot` (in `/target`). Any command that needs to be run inside the `chroot` environment must be preceded by `in-target`.

```plaintext
d-i preseed/late_command string \
    echo "Welcome to your preseeded VM" >> /target/etc/motd; \
    in-target usermod -aG sudo user; \
```

When adding `in-target` before the command, it's a command that is run inside the installation.

In this example, the first command appends a message to the [MOTD](https://en.wikipedia.org/wiki/Message_of_the_day). The installed environment is in `/target`, and the MOTD is in `/etc/motd`. If we wanted to use `in-target`, it would have been `in-target echo "Welcome to your preseeded VM" >> /etc/motd`. The second command runs inside the `chroot` environment and adds the user `user` to the `sudo` group, to allow it to use `sudo`.

It can sometimes be tricky to do certain things due to the limitations of the installation process, and it takes a bit of trial and error to figure out how to do it properly or find alternate ways to achieve what we want. The first thing the Debian installer does is validate our preseed file, and if it isn't valid, it will say so and wait. Thus, it's important to have the console (either via `virt-manager` or via `virsh console`) as early as possible during these times; otherwise, if we arrive later, nothing will happen on the screen, and it will look like it's doing nothing or hanging.
