Wheelhouse docs

Seed cloud-config keys#

A seed is either an answer file or a cloud-config document with a wheelhouse: mapping — the shape people already write when they seed machines with cloud-init. Either way, seed-to-answers.py turns it into KEY=VALUE lines before the installer sees it, so the mapping below is the whole of what a cloud-config seed can say.

user-data
#cloud-config
wheelhouse:
  disk: auto
  hostname: edge
  timezone: America/Los_Angeles
  wan:
    interface: eth1
    addressing: dhcp
  lan:
    interface: eth0
    address: 10.0.0.1/16
    dhcp_server: true
    nat: true
  firewall: true
  management_source: 203.0.113.0/24
  passwords:
    admin: "the web UI password"
    console: "the console password"
  config:
    commands: config.commands
  ssh_authorized_keys:
    - ssh-ed25519 AAAA… you@laptop
  reboot: true
  on_error: halt

The mapping, key by key#

Cloud-config keyBecomesDefault
diskDISKauto — the one disk that is not the boot medium. Refuses when there is none or more than one.
hostnameHOSTNAMEwheelhouse
timezoneTIMEZONEUTC
wan.interfaceWAN_IFACEempty, which skips the WAN
wan.addressingWAN_ADDRESSINGdhcp
wan.addressWAN_ADDRESSempty
wan.gatewayWAN_GATEWAYempty
wan.dnsWAN_DNSempty. A list is joined with commas.
lan.interfaceLAN_IFACEempty, which skips the LAN
lan.addressLAN_ADDRESSempty
lan.dhcp_serverLAN_DHCPyes if a LAN interface is named, otherwise no
firewall (or lan.firewall)FIREWALLno when the seed carries a configuration, otherwise yes
lan.natNATyes if a LAN interface is named, otherwise no
management_sourceMGMT_SOURCEempty
passwords.adminADMIN_PASSWORDrequired
passwords.consoleCONSOLE_PASSWORDrequired
rebootREBOOTyes
poweroffPOWEROFFno
keep_previousKEEP_PREVIOUSno
overwriteOVERWRITEno
on_errorON_ERRORhalt
config.commandsCONFIG_COMMANDS
config.boot, or config.fileCONFIG
ssh_authorized_keysSSH_KEYS

Any key whose value comes out empty is left out of the answer file entirely, so the installer's own default applies.

Booleans accept yes, true, on and 1 — anything else is false.

Two defaults that are not what they look like#

firewall follows the WAN, not the LAN. A seed with a WAN and no LAN describes a router facing the internet with nothing behind it, which is the one that most needs a closed front door. lan: {firewall: …} still works, because seeds already written say it that way, and the top-level firewall: wins when both are present.

A seed that carries a configuration gets no firewall by default. When config.commands, config.boot or config.file is set, that configuration decides what the firewall is; the installer writing its own default-deny ruleset first would leave two writers numbering the same rules differently. Set firewall: true explicitly if you want both.

Payloads#

config.commands, config.boot and ssh_authorized_keys may each be given three ways:

Written asWhat happens
A file nameResolved next to the seed and copied into /run/wheelhouse-seed/, so the installer can still read it after the medium is unmounted.
Inline text (any value containing a newline)Written into the work directory as a file.
A URLPassed through unchanged; the installer fetches it. http:// and ftp:// are refused unless the machine was booted with wheelhouse.insecure=1.

A named file that is not beside the seed produces a warning on stderr — seed names X, which is not next to the seed — and the key is dropped rather than guessed at.

ssh_authorized_keys accepts a list or a single string, and is written out as an authorized_keys file installed on both console accounts.

What config.commands and config.boot are#

KeyContent
config.commandsA file of set lines — the same lines show configuration commands prints, and what the OPNsense importer writes. Refused if it holds no set or delete line.
config.boot (or config.file)A whole configuration file, which the router loads and validates itself.

Both are applied after the passwords are set, so a restored firewall can be as closed as you like without locking the installer out of the agent.

The YAML the installer can parse#

The seed is parsed with PyYAML when the image has it, and with a small subset parser when it does not — so a seed still works on an image without PyYAML rather than failing at the worst possible moment. The subset is exactly what a Wheelhouse seed uses:

  • nested mappings by indentation;
  • inline mappings, {a: b, c: d};
  • inline and block lists of scalars;
  • block scalars introduced with |;
  • true/yes and false/no as booleans;
  • quoted scalars, where a # is just a character;
  • unquoted scalars, which end at the first whitespace-then-#, the way YAML reads them.

Anchors, aliases, multi-document streams and flow-style nesting beyond one level are not in the subset. If your seed needs those, make sure the image has PyYAML — or write the answer-file form, which has no parser at all.

Plain answers instead of YAML#

A seed that does not begin #cloud-config and contains at least one ^[A-Z_]+= line is passed straight through as an answer file. Both forms work on the same medium and in the same places.

Checking a seed before you boot with it#

bash
# What the seed converts to:
python3 packaging/seed-to-answers.py user-data /tmp/work

# What those answers would do to the router:
sudo wheelhouse-install --answers /tmp/work/answers.conf --commands

See also#

Checked against#

packaging/seed-to-answers.py (main, as_bool, load_yaml, copy_payload), packaging/wheelhouse-install (fetch_payload, apply_config, apply_ssh_keys), packaging/wheelhouse-autoinstall, tools/wheelhouse-seed.py, docs/unattended-install.md.

Updated 2026-09-02 seed cloud-config unattended installer