Wheelhouse docs

Files and directories#

Two install layouts exist and they share almost nothing. The appliance — the ISO or the .deb — puts the binary in /usr/bin, its data in /config/wheelhouse and its units in /lib/systemd/system. The developer install, install/install.sh, puts the binary in /opt/wheelhouse and its secrets in /etc/wheelhouse, because the host it runs on has no /config. Nothing reads both locations: a secret written under /etc/wheelhouse on an appliance is a file the agent will never open.

/config is the partition VyOS keeps across an image upgrade. That is the only reason the agent's data lives there and not under /var/lib, which does not survive one.

Installed by the package#

Everything here comes from packaging/build-deb.py, and the modes are the ones it sets.

PathModeWhat
/usr/bin/wheelhouse-agent0755The agent.
/usr/bin/wheelhouse-install0755The disk installer.
/usr/bin/wheelhouse-autoinstall0755The seed finder. Prints a path and exits 0, or exits 1; writes nothing.
/usr/bin/wheelhouse-opnsense-import0755tools/opnsense-import.py, so a migration can be done on the router itself.
/usr/share/wheelhouse/ui/0755 dir, 0644 filesThe built web UI the agent serves with --ui-dir.
/usr/share/wheelhouse/dialogrc0644The installer's dialog colour scheme.
/usr/lib/wheelhouse/firstboot.sh0755What first boot does.
/usr/lib/wheelhouse/wait-for-vyos.sh0755Blocks until the router has finished loading its configuration.
/usr/lib/wheelhouse/console-banner.sh0755The console banner.
/usr/lib/wheelhouse/install-driver.py0755Drives the base installer through a pseudo-terminal.
/usr/lib/wheelhouse/seed-to-answers.py0755Turns a cloud-config seed into answer-file lines.
/lib/systemd/system/wheelhouse-agent.service0644
/lib/systemd/system/wheelhouse-firstboot.service0644
/lib/systemd/system/wheelhouse-console.service0644
/lib/systemd/system/wheelhouse-console.timer0644
/lib/systemd/system-generators/wheelhouse-live-installer0755On a live boot, turns getty@tty1 into the installer.
/etc/systemd/system/multi-user.target.wants/wheelhouse-{agent,firstboot,console}.servicesymlinkEnabled by symlink rather than by postinst, so the package works inside a live-build chroot where systemctl cannot run.
/etc/systemd/system/timers.target.wants/wheelhouse-console.timersymlinkSame reason.
/usr/share/doc/wheelhouse-agent/copyright0644The Debian copyright file.
/usr/share/doc/wheelhouse-agent/README0644What this is, what it is built on, and where the corresponding source lives. The units' Documentation= points here, because the forge is private and a URL is no substitute for a file on the box.

Added by the image#

Only on a Wheelhouse OS image, staged by packaging/iso/build-iso.sh and asserted by packaging/iso/branding/50-wheelhouse-brand.chroot, which fails the build if any of them is missing.

PathWhat
/usr/share/doc/wheelhouse/TERMS.txtThe product's terms.
/usr/share/doc/wheelhouse/THIRD-PARTY.txtThird-party notices.
/usr/share/doc/wheelhouse/SOURCE-OFFER.txtThe written offer for the corresponding source.
/usr/share/doc/wheelhouse/SOURCES.txtThe vyos-build commit, the build container, and the version of every installed package.
/usr/share/vyos/EULAA symlink to ../doc/wheelhouse/TERMS.txt, so show license on the router pages the Wheelhouse terms.
/etc/os-releaseRewritten by the image flavor. See Identifying the system.
/usr/share/wheelhouse/autoinstall.confOptional, and never shipped by default: a seed baked into a custom image. See Where the installer looks for a seed.

The data directory#

--data-dir, /config/wheelhouse on the appliance. Created at 0700 if it does not exist. Everything the agent must remember across a restart is here, and nothing else is.

PathModeWritten byWhat
api-key0600first bootThe router's own API key, 40 characters from /dev/urandom. Root-equivalent on that router.
admin-token0600youThe optional break-glass token. The shipped unit names it unconditionally; a missing file simply means there is none.
license-key0600youAn optional pinned licence key. The web UI stores one in state.json instead.
initial-password0600the agentThe generated first-run admin password. The console banner shows it until that account's password is changed, then the agent removes it.
state.json0600the agentAccounts, password hashes, TOTP secrets, sessions, API token hashes, settings, enabled feature modules, and the licence key the UI stored.
state.json.bak0600the agentA hard link to the previous state.json, refreshed before each write, so at every instant at least one complete state file exists.
state.json.tmp0600the agentTransient. Written, fsynced, then renamed over state.json.
state.json.corrupt-<timestamp>0600the agentOnly when both copies failed to parse. The agent starts empty rather than refusing to boot, and keeps the unreadable files for inspection.
audit.jsonl0600the agentOne JSON object per line, appended and fsynced per entry. Rotates at 8 MB.
audit.jsonl.10600the agentExactly one previous generation. The audit log therefore costs at most 16 MB on disk.
agent.crt0644--tls-self-signedThe generated certificate.
agent.key0600--tls-self-signedIts private key.
agent.yamlyoursyouThe conventional name for the desired-state file. It is a convention, not a format promise — JSON parses too.
install.commands0600the installerThe exact set lines the install applied, copied onto the new disk as a record of how the box was built.
oidc-secret0600youThe identity provider's client secret. Nothing reads this by default — the shipped unit sets no --oidc-* flags. The path is a convention this documentation keeps so the secret lives beside the others.

The permission rule#

The agent refuses to read any secret file that is group- or world-readable, and says which one:

api-key-file (/config/wheelhouse/api-key) is mode 644; it must not be readable by
group or other (chmod 600)

That applies to --api-key-file, --admin-token-file, --license-key-file, --initial-admin-password-file, --oidc-client-secret-file and --fleet-config. A missing file is fatal only for --api-key-file; the others warn and continue.

Paths outside the data directory the agent touches#

PathAccessWhy
/config/archive/config.boot.<N>.gzread, through the routerPOST /api/rollback loads one of these. The router's HTTP API has no rollback operation; the CLI's command works by loading the archived file, so the agent does the same.
/config/config.bootthrough the routerWhere POST /api/config/save writes by default.
/config/apps/…created by POST /api/apps/prepareVolume source directories for installed apps.
/etc/machine-idreadHashed with a constant into the licence fingerprint. Falls back to the host name when absent.

The systemd unit runs with ProtectSystem=strict and ReadWritePaths=/config, so /config is the only writable tree the agent has.

Runtime files#

None of these survive a reboot; /run is a tmpfs.

PathWritten byWhat
/etc/issueconsole-banner.shThe console banner. Mode 0600 while it carries the first-boot password, 0644 afterwards. /etc/issue.net is deliberately not written here — sshd serves that file before authentication.
/run/wheelhouse/banner-factsconsole-banner.shCached version strings and the live/installed answer, keyed on the mtime of /var/lib/dpkg/status.
/var/log/wheelhouse-install.logthe installer, the seed finderEvery installer step, and the base installer's transcript. The first place to look when an install fails; the installer names it in its own error message.
/run/wheelhouse-install.commandsthe installerThe set lines the install will run, written before the summary so they can be read, and run from that file afterwards.
/run/wheelhouse-install.lockthe installerPresent while the installer runs. The banner refuses to restart tty1's getty while it exists.
/run/wheelhouse-install.stagethe installerWhich stage the install is in, for the progress screen.
/run/wheelhouse-install.disk-touchedthe install driverSet once the target disk has been partitioned. It is what makes the failure message say the disk is gone rather than that nothing was written.
/run/wheelhouse-install.no-ipv6-firewallthe installerSet when the image has no firewall ipv6 node, so the absence of a v6 ruleset is reported rather than discovered later.
/run/wheelhouse-autoinstall.attemptedthe installerAn unattended install runs at most once per boot. This file is why a failure cannot become a loop that keeps erasing the disk.
/run/wheelhouse-seed/answers.confthe seed finderThe converted seed.
/run/wheelhouse-seed/{seed,config.commands,config.boot,authorized_keys}the seed finderPayloads copied off the seed medium, so they are still readable once it is unmounted.

The developer install#

install/install.sh is the lab path, not the product.

PathModeWhat
/opt/wheelhouse/wheelhouse-agent0755The running binary.
/opt/wheelhouse/wheelhouse-agent.old0755The previous binary, kept for the automatic rollback.
/opt/wheelhouse/wheelhouse-agent.new0755Transient staging path, so a failed build never clobbers a working binary.
/opt/wheelhouse/ui/0755The built UI.
/opt/wheelhouse/agent.logOnly when there is no systemd unit and the script backgrounds the agent itself.
/opt/wheelhouse/agent.pidSame. It is what stop_service kills, rather than pattern-matching a command line.
/etc/wheelhouse/0700
/etc/wheelhouse/api-key0600
/etc/wheelhouse/admin-token0600Removed when --admin-token is not given, rather than left stale.
/config/wheelhouse0700The data directory, same as the appliance — accounts and the audit log belong on the persistent partition even in a lab.

Finding it all on a running box#

bash
dpkg -L wheelhouse-agent                 # everything the package owns
ls -la /config/wheelhouse                # the data directory, with modes
systemctl cat wheelhouse-agent           # the flags actually in use
find /config/wheelhouse -perm /0077      # any secret file with loose permissions

See also#

Checked against#

packaging/build-deb.py, packaging/wheelhouse-agent.service, packaging/firstboot.sh, packaging/console-banner.sh, packaging/wheelhouse-install, packaging/wheelhouse-autoinstall, packaging/iso/build-iso.sh, packaging/iso/branding/50-wheelhouse-brand.chroot, agent/store.go, agent/tls.go, agent/main.go (loadSecretFiles, initialPasswordPath, archiveDir), agent/license.go (licenseFingerprint), install/install.sh, docs/deploy.md "Secrets on disk", packaging/README.md.

Updated 2026-09-02 files paths secrets permissions