FreeBSD bsdinstall Scripted Provisioning: Reproducible Installs With Guardrails
Build and test unattended FreeBSD installs with bsdinstall scripts, controlled distribution sets, explicit partitioning, and post-install verification.
FreeBSD’s bsdinstall is more than an interactive menu program. Its manual exposes targets that can be called by a script, including a script target that reads a preamble and then runs a shell setup script inside the newly installed system. This makes unattended or minimally attended installation possible, but it does not make disk selection harmless. A provisioning workflow still needs inventory, image control, console access, and a test plan for every supported hardware profile.
The operational goal is repeatability with a clear failure boundary. A script should choose a known layout, install a known set of distributions from trusted media, configure only required services, and leave a record of what happened. It should not silently treat an unknown disk as disposable or assume that a successful installer exit means the new machine is ready for production.
Understand the script boundary
The bsdinstall(8) manual distinguishes the installer target from the scripting target. A script has a preamble containing variables that guide partitioning and installation, followed by a shell script introduced by the normal interpreter line. The setup portion executes under chroot in the newly installed system before bsdinstall exits. That is not the same execution environment as a cloud-init or first-boot script.
The manual also documents differences between scripted and interactive installs. For example, the documented scripted flow normally expects distributions to be available on the installation media rather than fetched remotely during installation. Build media and script versions together, and do not assume that a network dependency can be added at the last minute.
A simple illustrative setup fragment is:
#!/bin/sh
set -eu
sysrc hostname="node01.example.test"
sysrc sshd_enable="YES"
sysrc ntpd_enable="YES"
This fragment is the post-install shell section, not a complete unattended installation by itself. The preamble must precede it and must select distributions and a partition layout appropriate to the target release. Replace example values with host-management data and test service variable names against the installed release’s rc.conf(5).
Make the destructive boundary explicit
Automatic partition targets can erase data. Keep disk inventory and target selection outside any assumption that the first disk is the correct disk. In a lab, use a disposable virtual disk and capture the installer log. On physical systems, inventory model, serial number, capacity, boot mode, and intended role before starting the installation. Confirm the storage topology and backup state with a separate, human-reviewed provisioning manifest.
FreeBSD documentation describes the script target and the preamble options; it does not promise a generic safety prompt for every unattended layout. Treat a scripted installer as an authorized destructive operation. A preflight check should compare expected device inventory with the machine’s observed devices, then stop if the host does not match the declared profile. Do not add a “pick the first disk” loop to a general image without a hardware-specific identity contract.
Keep separate scripts or explicit profiles for UFS, root-on-ZFS, and virtual-machine image construction. Partitioning variables for a ZFS pool have different semantics from a default UFS layout. If a root pool spans disks, the script must capture its intended vdev arrangement and must not accidentally create a stripe where redundancy is required.
Build versioned installation media
The bsdinstall manual describes placing an automatic install script at /etc/installerconfig in extracted release media. The exact release image contents and script behavior can change; build a media artifact for the release you intend to deploy and keep its checksum, source image, and script revision together. If you rebuild the ISO, retain the build command, source tree revision, and output hash in the provisioning record.
Before using the media, check the target release’s errata and architecture-specific requirements. A boot-only image, full installation image, and prebuilt virtual-machine image are different artifacts with different assumptions. Verify that all requested distribution sets exist on the medium, that the installer can find them without an unexpected network dependency, and that the selected kernel and userland match.
The script should be treated as code. Review it, lint shell syntax, test each supported path in a virtual machine, and review the resulting disk layout from the installed system. Avoid embedding long-lived credentials or private keys in an ISO that may be copied or retained beyond its approved life. A post-install script can call pkg, but package availability, repository configuration, and network reachability need their own explicit assumptions.
Separate installed configuration from first boot
The setup portion runs in the chroot for the newly installed system. The FreeBSD 15.1 bsdinstall(8) manual notes that newly configured target services, including networking, have not been started at that point; only installation-host services are available. A sysrc call can write the target’s network configuration, but it does not bring that target network up for later setup-script commands. A command succeeding also does not prove that its service will start after reboot or that dependencies are reachable. Keep first-boot application provisioning separate from the installer when it depends on network identity, secret delivery, external APIs, or a configuration-management agent.
Use a minimal base configuration at install time: hostname, network setup needed for the first boot, time zone, required services, and a noninteractive operator account where appropriate. Defer application secrets and environment-specific values to a trusted provisioning channel. Keep one source of truth for each setting to prevent a first-boot agent from undoing the install script’s configuration.
Make the setup shell fail deliberately on unexpected errors. set -e is not a complete error-handling strategy for every shell construct, so commands with important effects should be checked explicitly and their output captured. Print phase markers that make the installer log useful: base configuration, account setup, package installation, and validation. Avoid suppressing errors with broad redirection to /dev/null.
Verify the installed system before declaring success
After reboot, verify that the machine booted the intended release and kernel, that all expected filesystems are mounted, and that enabled services are healthy. Use the installer log as a record, then validate the running system independently:
freebsd-version -kru
mount -p
sysrc -a | sort
service -e
service sshd status
Replace service names with the actual profile. A value in rc.conf is configuration intent, not a health check. Verify network addresses, routes, DNS, time synchronization, package inventory, and a representative application transaction from the intended management network.
For UFS, compare the partition map with the profile’s approved layout. For ZFS, capture zpool status, dataset properties, boot-environment state, and any encryption or mirror configuration expected for the image. Do not infer the final layout from the installer preamble alone; inspect what was actually created.
If the system fails to boot, use the installation media’s shell or live environment to inspect partitions and logs. Preserve the installer log before retrying. Re-running an unattended script against the wrong disk can overwrite evidence and data. Have a recovery path that records serial console output and can boot a known-good installation medium.
Common failure cases
An installer that stops for input is not automatically defective. Unsupported hardware, a missing distribution set, an invalid variable, or a changed prompt can leave a supposedly unattended workflow waiting. Use the target release’s exact manual and test the exact image; don’t assume behavior from another major release.
A host that boots but lacks packages may have been installed from media without those packages or may have no configured repository. This is a separate provisioning phase. A host that fails network reachability may have an incorrect interface name, DHCP dependency, or static profile. Verify link, address, route, and resolver independently rather than re-running the destructive install.
A script that succeeds while leaving an incomplete account or service configuration should be treated as a failed build. Store a machine-readable acceptance record: image hash, script revision, release, architecture, disk-layout result, enabled services, package manifest, and validation outcomes. That record makes later drift distinguishable from installation defects.
Acceptance criteria
Promote an unattended install only after a clean disposable-machine run from the exact media, confirmation that no prompt was missed, inspection of the resulting partition table and root filesystem, verification of the installed release and kernel, and successful post-boot service and network checks. Then repeat on each supported hardware or VM profile.
The installer automates a sequence of provisioning operations. It does not guarantee correct disk selection, application readiness, secure secret handling, or successful recovery. Make those responsibilities explicit in the runbook and acceptance checks.
Related:
- FreeBSD GPT Data Disk Layouts with gpart and Stable Labels
- bsdinstall Gains Guided Root-on-ZFS Support, Years After ZFS Itself Landed
Sources: