Wheelhouse docs

What first boot does#

wheelhouse-firstboot.service runs /usr/lib/wheelhouse/firstboot.sh once per machine, as root, before the agent starts. It does three things: mints the agent its own router API key, pins the router's HTTP API to loopback, and — only if nobody else has — writes the product's login banners.

The unit's condition is the key file itself:

ini
ConditionPathExists=!/config/wheelhouse/api-key

So it runs until it has produced that file and then never again, including after an image upgrade, because /config survives one.

In order#

  1. Wait for the router. wait-for-vyos.sh polls vyos-router.service until its SubState reads exited, which is the only reliable signal that the boot configuration has been loaded. Exits 1 if the router service failed, or after five minutes.
  2. Stop if the key already exists. A non-empty /config/wheelhouse/api-key means there is nothing to do.
  3. Generate a key. 30 bytes from /dev/urandom, base64, with /, + and = stripped, then truncated to at most 40 characters. Because the stripping happens first, the key is 32 to 40 characters rather than always 40; if fewer than 32 survive, the script fails rather than writing a short key.
  4. Read the base version and the documentation directory, for the banner text below.
  5. Commit three nodes in one configuration session:

    set service https api keys id wheelhouse key <the generated key>
    set service https api rest
    set service https listen-address 127.0.0.1
  6. Save, naming the file explicitly.
  7. Verify by reading the saved configuration back, not by trusting the exit status.
  8. Write the key to disk at /config/wheelhouse/api-key, mode 0600.
  9. Log it: logger -t wheelhouse "first boot: API key created, HTTP API bound to loopback".
  10. Optionally write the login banners, in a second commit.

What each of the three nodes does#

NodeEffect
service https api keys id wheelhouse key …The agent's own credential. Root-equivalent on this router; the file it lands in is 0600, and the same commit closes the API it opens.
service https api restThe REST endpoints are off until asked for. With only a key configured, the router's API answers 404 to everything.
service https listen-address 127.0.0.1The router's API stops being a network service. The agent, on the same box, is the management surface.

See Ports and listeners.

Believe the file, not the exit status#

commit can answer 0 while refusing the change — it does exactly that during boot, with "the configuration was not applied". So the script greps the saved configuration for the key it just set, and refuses to write the key file if it is not there:

wheelhouse first boot: the API key did not reach the configuration; not writing it

A failed run leaves /config/wheelhouse/api-key absent, which leaves the unit's condition true, so the next boot tries again rather than starting an agent that holds a key the router never accepted.

The login banners#

Written only when nobody else has written any, checked before the configuration session opens:

bash
grep -qE '^[[:space:]]*(pre-login|post-login) ' /config/config.boot

A Wheelhouse OS image already carries banners from its image flavor, and an operator may have written their own; neither is overwritten. This covers the package installed on a plain VyOS box.

set system login banner pre-login "Wheelhouse\n"
set system login banner post-login "Wheelhouse\n\n  Web UI     https://[this router]:8443\n  Licences   <docs>\n\nBuilt on VyOS <base> (GPL). Corresponding source: <docs>\n"

<docs> is /usr/share/doc/wheelhouse on an image and /usr/share/doc/wheelhouse-agent when only the package is installed, picked at runtime so two banners on one box never name different directories. <base> is VYOS_VERSION from /etc/os-release, falling back to VERSION_ID when /etc/os-release is VyOS' own — never /opt/vyatta/etc/version, which on a product image holds the product's version. An empty value prints the credit without a version.

Without these nodes, VyOS renders its own template into /etc/motd: "Welcome to VyOS", the VyOS logo and the upstream support portal, on the console and — through pam_motd — over SSH. Setting the node replaces that template wholesale. The upstream credit stays, on its own line.

This is a second commit, after the key is on disk. If it fails, the script says so and carries on.

The pre-login node is also what VyOS writes into /etc/issue.net, the file sshd serves before authentication — which is why the console banner script does not write that file itself. The console banner carries the first-boot password and every interface address; neither belongs in front of an unauthenticated SSH connection.

Two traps when scripting the router's CLI#

  • set -e cannot be used. VyOS' script template sources the CLI's completion setup, whose shopt -p extglob returns non-zero, and the script dies there.
  • exit is an alias. After source /opt/vyatta/etc/functions/script-template, exit expands to eval $(vyatta_exit_configure) — it tears down the configuration session and returns; it does not end the script. exit 1 does not stop the script: it passes a stray 1 to that eval, bash reports 1: command not found, and execution carries straight on to the next line. Everywhere the script really means "stop" it says builtin exit.

Checking it worked#

bash
systemctl status wheelhouse-firstboot
journalctl -t wheelhouse
sudo ls -l /config/wheelhouse/api-key            # -rw------- root root
show configuration commands | match 'service https'

If the key file is missing and the unit has failed, the journal line names which of the two verifications did not pass.

See also#

Updated 2026-09-02 boot first-boot api-key banners