Puffmatic - OpenBSD Autoinstall Generator
Abstract
This article covers automatic OpenBSD installation over the network and from USB thumb drive.
First part briefly discusses the problem at hand and motivations.
Second part discusses the procedure to manually prepare images and file sets, diving in the methodology.
Third part introduces puffmatic
- a script that automates above steps.
If you are familiar with unattended installation, you can skip intial
discussion and jump stright to puffmatic
.
If you are curious how it works, I recommend going through whole article, so you can troubleshoot if anything goes sideways.
Part 1 - Why?
Motivation
OpenBSD has a six-month release cycle, accompanied by a one-year support cycle, which promotes regular updates. I run OpenBSD on my personal laptop and several servers, meaning I confront the possibility of a system upgrade every six months.
I have a reluctance toward upgrades; I simply don’t trust them. While I haven't had any negative experiences with the OpenBSD upgrade process, that’s mostly because I have never relied on it.
Instead of upgrading, I prefer to perform fresh installations.
Configuring a new system can be a laborious task, but since I use Ansible to manage all my systems for both automation and disaster recovery, that concern is minimized.
The only truly "tedious" aspect of this process is the installation
itself, followed by a system bootstrap to prepare it for configuration
(install python3
from ports and few other quirks).
Fortunately, OpenBSD offers autoinstall(8), and install.site(5) allowing me to automate this part to streamline the process.
Objectives
I want to support two scenarios:
- Automatic installation from a USB stick to provision my physical hardware, such as the APU4 server or my laptop;
- Semi-automatic network installation over HTTP
The latter case requires some clarification. Although OpenBSD supports
direct automatic installation from bsd.rd
booted via TFTP, I run
several VPS instances, and my provider does not offer TFTP capability
nor local http
server. I am willing to provide the URL to
auto_install.conf
when prompted by the installer. Let's call it
semi-automatic.
Overall, system provisioning is divided into two phases:
- installation of base system + some initial configuration
- further configuration using
ansible
, conducted overssh
Phase 1 - autoinstall(8)
During this phase, I want to provision a bootable system with root
SSH
access and a minimum set of utilities required by ansible
.
At a minimum, I need to provision sshd
host keys (so I don't have to
clear my known_hosts
after each upgrade) and configure the network
interfaces. This can be achieved using site set archives.
Once the system has network access, I can leverage rc.firsttime
to install
python3
, which is required by ansible
during phase 2.
I am intentionally limiting this phase to the bare minimum, as system configuration using shell scripts can quickly balloon in complexity and reliability issues.
Phase 2 - configuration
In this phase, I'm working with a fresh install accessible via ssh
. I
can execute ansible
scripts and securely run arbitrarily complex
provisioning procedures.
Part 2 - How?
In this section, I delve into the intricate details of the entire procedure without utilizing any automation. This comprehensive exploration enables readers to grasp the technical challenges that must be addressed.
Autoinstall from USB
This variant is realtively uncomplicated:
- grab
install77.img
installation image from one of the mirrors - add site file sets to
install77.img
- add
auto_install.conf
,disklabel
tobsd.rd
- patch
boot.conf
to run from patchedbsd.rd
- run it
Below is the breakdown of the whole procedure.
Adding site77.tgz and site77-${hostname}.tgz
To mount the image, simply use vnd(4), copy the necessary files, and you're done. Preparation for the site setup is detailed in OpenBSD FAQ, so we won’t cover it here.
My installation process divides the file sets into "common" (shared by all machines) and "host," which is specific to each individual host. Therefore, I need to add two tarballs.
vnconfig vnd0 install77.img
mount /dev/vnd0a /mnt
cp site77*.tgz /mnt/7.7/amd64/
umount /mnt
vnconfig -u vnd0
Patching bsd.rd
This one is a bit more involving.
Ramdisk kernel filesystem is emedded in compressed bsd.rd
and cannot
be mounted directly. Additionally, install77.img
comes with 2 bsd.rd
files: one in /bsd.rd
and another in /7.7/amd64/bsd.rd
. The installer
boots the latter by default and I don't want to modify file sets, so
we need to patch boot.conf
.
We need to:
gunzip
bsd.rd
- extract filesystem from decompressed
bsd.rd
- mount the filesystem image using
vnd(4)
- add
auto_install.conf
anddisklabel
to the ramdisk root - put patched filesystem back into decompressed
bsd.rd
- compress
bsd.rd
- copy it to
/bsd.rd
(root ofinstall77.img
, not host system)
Patching boot.conf
By default, the installer image loads using /7.7/amd64/bsd.rd
. We
don't want to modify that file. Instead, we will patch and boot the
/bsd.rd
found in the installation image root.
To avoid the need for manual selection of the file at the bootloader
prompt, we can update /etc/boot.conf
.
Since my system is headless and lacks a video console, I also want to
enable the com0
console. My final boot.conf
is as follows:
stty com0 115200 set tty com0 set image /bsd.rd
Provision USB stick
Insert the USB stick. Run dmesg
to identify the device you will be
writing to. Wiping your hard drive by accident can teach you to do
backups, but that's not our goal here today. For this example, I will
assume sd1
is the target device.
dd if=install77.img of=/dev/sd1c bs=1M
Pull it out, plug it in, reboot and after a while, you should end up with a running system.
Autoinstall over (wild) network
This section covers installation via the Internet. Yes, the Internet. I want to retrieve response files from a machine within the network, as I do not have the luxury of provisioning installation artifacts in a walled garden.
Security considerations
Autoinstall over open internet may be regarded as reckless, so let's dive into some security considerations first to gauge the risk.
There are 4 types of installation resources to consider:
- response file
- disklabel file
- upstream file sets (their big tarballs)
- site sets (our own small tarballs)
Let's see if serving those files can be problematic.
Response file
The response file contains two types of secrets:
root
password- Disk encryption passphrase (optional)
By default, the system is configured to prevent remote root
login
using password authentication, making the provisioned secret unusable
without direct access to the system console. We can provision a
temporary password and change it later during the phase 2
configuration.
User authentication for ansible
access is based on PKI and we
provision public ssh
key, which by definition is not secret.
The disk encryption passphrase (optional) is another sensitive piece
of data. However, it is not usable unless an attacker has either a
disk dump or, again, direct access to the system console. The
passphrase can also be changed later using bioctl(8)
, so the window of
opportunity is time-limited.
If an attacker gains access to the system console during installation and compromises your empty machine, it indicates that your provider account has been breached and you are in much bigger troubles anyway.
The response file will be served over HTTPS (encrypted) and we can utilize basic authentication to prevent public access.
Additionally, we can further limit access to files served by httpd
using firewall IP filtering.
I find that threat level acceptable to me. You make your own judgment.
Disklabel & upstream file sets
No secrets here, period. Let's move on.
Site sets
The situation here is a bit more complicated. The OpenBSD installer does not support basic authentication for installation sets, which limits our options for protecting these files from unauthorized access during installation window.
We do have TLS encryption, allowing us to secure these files using a "secret path" technique, similar to AWS S3. Without knowledge of the path, it is impossible to reach the served files in the first place.
The installer retrieves the secret location from the response file, which is delivered relatively securely, protected by a combination of TLS + basic authentication + IP ACL.
If this is still insufficient, we can always choose not to include sensitive data in the site sets and provision that through SSH during phase 2.
Overall, I find the threat level acceptable as well.
Setup
To serve installation files, we need:
httpd(8)
- included in the base system, although any other web server will suffice.- Public IP address. I'm serving my installation files from a BT/EE residential DSL address (my personal laptop). Although the IP is dynamic, it tends to remain stable for weeks, which is "good enough" for my use.
- Proper TLS certificate - The OpenBSD installer will not work with self-signed certificates. I obtained mine from Let's Encrypt using the DNS-01 challenge method. It's valid for three months.
- DNS - I temporarily added an
install.*
domain to my zone, pointing to my residential IP address during installation.
Overall, I have a valid installation server accessible via a valid hostname with proper TLS and basic authentication.
My servers have fixed IP addresses that never change, making defining IP ACL straightforward.
This is fine. I'm ok with the events that are unfolding currently.
Boot & install
I'm using Hetzner, which have OpenBSD stock images available in offer. I'm booting my VPS
from cd77.iso
and point it to my installation server sitting on my desk at home:
Welcome to the OpenBSD/amd64 7.7 installation program. (I)nstall, (U)pgrade, (A)utoinstall or (S)hell? a Could not determine auto mode. Response file location? [https://100.64.1.2/install.conf] https://user:pass@install.example.com/install-alfa.conf (I)nstall or (U)pgrade? i Fetching https://user:pass@install.example.com/install-alfa.conf Performing non-interactive install... ...
That's all folks!
Part 3 - Automate it with puffmatic
Here we are. Over time, we have collected a variety of
auto_install.conf
response files, a few siteXY.tgz
tarballs, and some
peculiar configuration quirks for various edge cases. Maintaining
these configurations by hand has become quite tedious.
Introducing puffmatic
- a Python script that:
- takes "domain configuration" as input
- downloads file sets from the OpenBSD mirror
- patches installer images with
auto_install.conf
- generates response files and site sets for hosting over HTTP
You maintain and version a set of YAML configuration files. The script processes these config templates, fills in the details, and packages your site files for you.
Want to jump stright into example? Check example.com README.md.
Prerequisites
This script - apart from python3 packages - uses only utilities available in base install.
However, it requires priviledged access to perform certain operations:
- mount - we need to mount filesystem images
- umount - we need to unmount them when done
- vnconfig - required to attach files as drives
- install - required to copy files into mounted installer root
To avoid running entire script as root
, it leverages doas
to execute
only those selected commands. Here is required doas.conf
:
permit nopass user cmd mount
permit nopass user cmd umount
permit nopass user cmd vnconfig
permit nopass user cmd install
You could argue that running install
as root
effectively opens the
system for abuse and that's true. If concerned, create a dedicated
account to build installation sets. Most of the script doesn't require
root
access, so I tried to limit it as much as possible using doas
technique.
It works for me. Don't shout at me, ok?
Domain configuration
Configuration is divided into domains. Literally domain names, like
example.com
, which we describe here. Configuration direcotory
("domain"), will contain few elements:
-
config.yaml
- Main configuration file. The file itself is well documented, so we spare detailed fields description. Here we configure mirror URL, where to put output files and how to host them.
-
site
- this directory contains files that will be placed in
siteXY.tgz
file set. This directory is accompanied bysite.mtree
, which describes file attributes. Consult mtree to learn more about this tool. -
templates
- template
install.conf.j2
with some configuration overrides. This response template file will be used by default, unless there is a host specific one. -
hosts
- here we store host-specific configuration, like
alfa
foralfa.example.com
andbravo
forbravo.example.com
. Host specific configuration can provide ownsite
directory (that one will be packaged assite77-${hostname}.tgz
),config.yaml
overrides or complementary values andboot.conf
. -
install.conf.j2
- autoinstall response file. This file can be provided
either as a Jinja2 template (it will be populated using values from
merged
config.yaml
files) or stright asinstall.conf
, used verbatim.
Running the generator
There are two modes available: network installation or USB stick. Generator must be inside the "domain configuration" directory.
USB
To generate auto-installable USB image, run puffmatic-usb
:
puffmatic-usb $HOSTNAME
Here, $HOSTNAME
must match one of the host configuration directories
in the hosts/
folder. The resulting file will be named
install77-$HOSTNAME.img
and placed in the output/img
directory. This
image must be written to the USB thumb drive using dd
. The
install77.img
file will be downloaded if it doesn't already
exist. This process may take some time, so the initial run could be
longer. Subsequent runs will utilize the cached installer image file.
Network
To generate HTTP site sets, use the following command:
puffmatic-net
Note that there are no arguments required. This command will process
all hosts in the ${PWD}/hosts/
directory and output relevant response,
disk labels, and site sets in the output/sets
and output/resp
directories. Those output directories are suitable for serving with
httpd(8)
.
Network installation requires a complete set of files, so
puffmatic-net
will mirror the entire OpenBSD release directory for the
configured architecture. This process involves downloading
approximately 2.5GB of data, so the initial mirroring (using rsync
)
will take some time. Subsequent runs will utilize cached files.
Messed up? Just delete output/
and start again.