Build a seed that installs the replacement#
You will end up with a small ISO image — a seed — that you attach to the new machine as a second disk or write to a USB stick. Boot the Wheelhouse image with the seed present, and the installer finds it, installs to the machine's other disk, applies the configuration the seed carries and reboots into a configured router with nobody at the keyboard.
This is the fastest way to stand up a replacement for an OPNsense box, and the only practical way to install more than two or three machines the same way.
Before you start#
- A Wheelhouse ISO for the machine, and the machine itself, with UEFI boot and one disk that is not the boot medium.
- Either an OPNsense
config.xmlto import, or a cloud-config you wrote yourself. xorrisofs,genisoimageormkisofsif you want an ISO. On Debian:apt install xorriso. If you have none of them,--out-dirwrites the files instead and you copy them onto a stick you formatted and labelled yourself.- Two passwords: one for the Wheelhouse
adminaccount, and one for the router's console accounts. The installer creates two —wheelhouse, the one you use, andvyos, kept for recovery — and gives both the same password, so there is one thing to remember and no way to be locked out of the recovery account.
Step 1 — Put the passwords in files#
umask 077
printf '%s' 'the web UI password' > admin.pw
printf '%s' 'the console password' > console.pwprintf rather than echo so no trailing newline creeps into the password. The tool
reads the file and strips whitespace, but the habit is worth keeping.
If you leave both out and are on a terminal, the tool prompts twice for each and refuses if the two entries differ. With no terminal and no files it stops rather than guessing.
Step 2 — Build the seed from the OPNsense configuration#
tools/wheelhouse-seed.py --opnsense config.xml --map lan=eth0,wan=eth1 \
--admin-password-file admin.pw --console-password-file console.pw \
--ssh-key ~/.ssh/id_ed25519.pub \
--include-source \
--out seed.isowrote ./out/config.commands (147 commands)
wrote ./out/report.md
translated 31, differently 3, not at all 9
wrote seed.iso (372 KiB, label CIDATA, built with xorrisofs)
attach it as a disk (or write it to a USB stick with dd) and boot the Wheelhouse image;
the installer will find it.
it holds the passwords in the clear: wipe it once the machine is up.The tool runs the importer for you and puts three things on the
seed: config.commands, the report as import-report.md, and a user-data
cloud-config that ties them together. It also reads the host name and time zone out of
the imported commands, so the new box keeps the old router's name unless you pass
--hostname.
--include-source puts the original config.xml on the seed as opnsense-config.xml,
which is what makes verifying the migration a two-command job
months later.
--ssh-key puts a public key on both console accounts, so the machine is reachable
the moment it boots without a password living on a stick any longer than it has to. Both,
because a key that opens only the recovery account would be a trap and one that opens only
the product's account would leave the recovery account key-less. SSH still has to be
enabled in the configuration — see Turn SSH on, and restrict it.
The firewall answer is decided for you, deliberately#
The tool reads the commands it just generated and writes one of two lines into the seed:
firewall: false # the imported configuration brings its own firewall: true # nothing came across (see import-report.md),
# so the installer writes its default-deny oneTwo writers would number the same rules differently and the router would refuse the result. No writer at all would put a new box on the internet with nothing in front of it. So the seed states the answer rather than leaving it to a default.
Step 3 — Or write the cloud-config yourself#
For a machine that is not a migration, write seed.yaml and build from that:
tools/wheelhouse-seed.py --user-data seed.yaml --out seed.isoAnything named config.commands, config.boot or authorized_keys sitting beside
seed.yaml is copied onto the seed too.
#cloud-config
wheelhouse:
disk: auto # auto = the one disk that is not the boot medium
hostname: edge
timezone: Europe/Berlin
wan:
interface: eth1
addressing: dhcp # or static
address: 203.0.113.2/24 # static only
gateway: 203.0.113.1 # static only
dns: [9.9.9.9, 1.1.1.1] # static only
lan:
interface: eth0
address: 192.0.2.1/24
dhcp_server: true # hand out addresses and answer DNS
nat: true # share the WAN address
firewall: true # default-deny on the WAN, both address families
management_source: 203.0.113.0/24 # who may reach the web UI on a box with no LAN
passwords:
admin: "the web UI password"
console: "the console password"
config:
commands: config.commands # a file on the seed, a URL, or inline text
boot: config.boot # or a whole VyOS configuration to load
ssh_authorized_keys:
- ssh-ed25519 AAAA... you@laptop
reboot: true # false stops after installing
poweroff: false # true powers off instead of rebooting
keep_previous: false
overwrite: false # reinstall over a disk that already has a system
on_error: halt # halt | poweroff | menu | shell | exitEverything is optional except the two passwords and a disk. A seed that only sets
config.commands gets its addresses from the restored configuration, so the wan: and
lan: sections can be left out entirely — which is exactly what the OPNsense path does.
Two keys are commonly put in the wrong place.
firewall: is top level, not under lan:. The firewall follows the WAN, not the LAN:
a machine with an uplink and no LAN was the one case that used to get no ruleset at all
while the web UI listened on every address. lan: { firewall: … } is still read, for seeds
written before the move, and the top-level key wins. A seed that carries config.commands
or config.boot defaults firewall to false, because that configuration is the thing
deciding what the firewall is; every other seed defaults it to true.
management_source: is the prefix allowed to reach the web UI on a box that has no LAN
to reach it from. Without it, a WAN-only machine with the default-deny ruleset is one you
cannot sign in to.
A seed can also be plain KEY=VALUE lines instead of YAML, which is the format
wheelhouse-install --answers has always taken and what the importer's --answers flag
writes:
DISK=auto
HOSTNAME=edge
TIMEZONE=Europe/Berlin
WAN_IFACE=eth1
WAN_ADDRESSING=dhcp
LAN_IFACE=eth0
LAN_ADDRESS=192.0.2.1/24
LAN_DHCP=no
NAT=no
FIREWALL=no
MGMT_SOURCE=203.0.113.0/24
CONFIG_COMMANDS=config.commands
ADMIN_PASSWORD=
CONSOLE_PASSWORD=
REBOOT=yes
ON_ERROR=haltWithout ON_ERROR, a seeded install halts the machine and a hand-run --answers install
exits, which is what each one wants. VYOS_PASSWORD is the old name of CONSOLE_PASSWORD
— from before the console account was named after the product — and a seed written against
it still works; CONSOLE_PASSWORD wins if a file carries both.
Serving a seed over the network#
A seed URL on the kernel command line is fetched over https:// only. Plain http:// and
ftp:// are refused, with the reason on screen: whoever is on the network path would
otherwise be the one setting this router's passwords. Two escape hatches, both explicit and
both on the kernel command line, where only the person at the keyboard can put them:
| Kernel parameter | Effect |
|---|---|
wheelhouse.insecure=1 | Says the network is trusted and means it. The install proceeds over plain HTTP and logs a warning saying why it did. |
wheelhouse.autoinstall.sha256=<hex> | Pins the seed's content. The installer hashes what it fetched and stops if it does not match — which is what makes an unauthenticated fetch safe rather than merely permitted. |
Neither is an answers key, deliberately: a seed that arrived over the network must not be able to say that the network it arrived over is trustworthy.
Step 4 — Get the seed onto the machine#
A VM: attach seed.iso as a second CD or disk.
Hardware: write it to a USB stick.
sudo dd if=seed.iso of=/dev/sdX bs=4M status=progress conv=fsyncCheck /dev/sdX twice. lsblk before and after plugging the stick in is the way to be
sure.
No ISO tool: build the files instead and copy them onto a filesystem you labelled
CIDATA or WHEELHOUSE yourself.
tools/wheelhouse-seed.py --opnsense config.xml --map lan=eth0,wan=eth1 \
--admin-password-file admin.pw --console-password-file console.pw \
--out-dir /media/stickStep 5 — Boot the Wheelhouse image with the seed present#
The installer looks for a seed in this order and stops at the first one it finds:
- The kernel command line:
wheelhouse.autoinstall=<value>, where the value is a URL, a device (/dev/sdb1),label:NAME, a path on the live image,autofor the normal search, oroffto insist on a person at the keyboard. - A labelled filesystem: any partition labelled
WHEELHOUSEorCIDATA— cloud-init's NoCloud name, which is why the tool uses it by default — holdingwheelhouse.conf,wheelhouse.yamloruser-data. Case does not matter. - cloud-init's own seed, when cloud-init has already found one:
/var/lib/cloud/seed/nocloud,nocloud-net, or the current instance. /usr/share/wheelhouse/autoinstall.conf, for images built to install a fleet the same way every time.
Two rules keep a seed from turning into a machine that erases itself repeatedly.
At most one unattended install per boot. If it cannot finish, the machine does what
on_error says and the next start of the installer shows the menu instead of erasing a
disk again.
A seed will not reinstall a machine that is already installed. If the target disk
holds a system and the seed does not say overwrite: true, the installer says so and
leaves the disk alone — which is what you want the first time somebody reboots a new
router with the USB stick still in it.
Check it worked#
Watch the console. Each step is printed, and everything lands in
/var/log/wheelhouse-install.log on the live system. On failure the installer names that
log in its own error message.
To try a seed on a live system without rebooting, or to ask which seed would be used:
sudo wheelhouse-autoinstall # just say which seed would be used
sudo wheelhouse-install --seed # find a seed and use it
sudo wheelhouse-install --answers /path/to/answers.confTo see the set commands a set of answers produces without touching a disk:
sudo wheelhouse-install --answers /tmp/answers --commandsThat is worth doing before you hand a seed to a fleet.
After the reboot, the console banner prints the web UI address. Sign in as admin with
the password from the seed, and then:
- Wipe the seed.
shred -u seed.isoon the laptop, and overwrite the stick. - Check the configuration arrived — Check a running router against the configuration it should have.
- Work through the report's third section — Read the migration report.
- Take a backup, which is a different thing from the seed — Take a backup that is actually complete.
Undoing it#
An install is not undoable: the disk is written. If the result is wrong, fix the seed and
install again with overwrite: true. Nothing on the old router has been touched, which
is the reason to keep it powered off but intact until the new one has carried traffic for
a week.
See also#
- Turn a
config.xmlinto VyOS commands - Read the migration report
- Rebuild a dead router onto a new box — the same problem when the input is a backup rather than an OPNsense file
- Installing without a keyboard — the seed model in overview
- Working from the console
Checked against tools/wheelhouse-seed.py ·
tools/opnsense-import.py ·
docs/unattended-install.md ·
packaging/wheelhouse-autoinstall