Wheelhouse docs

systemd units#

The package installs three services, one timer and one systemd generator, and enables them by symlink rather than from postinst, so the package installs inside a live-build chroot where systemctl cannot run. The agent's whole command line is in one unit file; systemctl cat wheelhouse-agent prints what a router is running.

UnitTypeRuns
wheelhouse-agent.servicesimpleAlways, from multi-user.target.
wheelhouse-firstboot.serviceoneshotOnce per machine, until the API key exists.
wheelhouse-console.serviceoneshotAt boot, before the login prompts, and from the timer.
wheelhouse-console.timertimerEvery 20 seconds.
wheelhouse-live-installergeneratorOn a live boot only: turns getty@tty1 into the installer.

wheelhouse-agent.service#

The daemon. Type=simple, User=root.

ini
After=network-online.target vyos-router.service wheelhouse-firstboot.service
Wants=network-online.target wheelhouse-firstboot.service
ExecStartPre=/usr/lib/wheelhouse/wait-for-vyos.sh
TimeoutStartSec=600
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=20

Its ExecStart passes nine flags and nothing else:

--api-url https://127.0.0.1
--api-key-file /config/wheelhouse/api-key
--admin-token-file /config/wheelhouse/admin-token
--license-key-file /config/wheelhouse/license-key
--addr 0.0.0.0:8443
--tls-self-signed
--ui-dir /usr/share/wheelhouse/ui
--data-dir /config/wheelhouse
--log-level info

Every secret is a file under /config/wheelhouse, never a flag value, because ExecStart is world-readable through /proc. The break-glass token and the licence file are optional: the unit names both paths unconditionally and the agent degrades to "no break-glass" and "unlicensed" when they are absent.

On SIGTERM the agent drains in-flight requests for up to 15 seconds, which is what TimeoutStopSec=20 allows for.

Waiting for the router#

vyos-router.service is Type=simple with RemainAfterExit, so systemd calls it active the moment it forks — while /config is still being mounted and the configuration is still being applied several seconds later. Anything merely After=vyos-router.service that touches /config writes into a directory about to be mounted over. So both the agent and first boot run wait-for-vyos.sh first, which polls the unit's SubState until it reads exited — the signal that the configuration is loaded.

It gives up after five minutes, and immediately if the unit reports failed.

Hardening#

ini
NoNewPrivileges=true      ProtectKernelTunables=true    RestrictRealtime=true
ProtectSystem=strict      ProtectKernelModules=true     LockPersonality=true
ProtectHome=true          ProtectControlGroups=true     SystemCallArchitectures=native
PrivateTmp=true           RestrictSUIDSGID=true
ReadWritePaths=/config
MemoryMax=200M
CPUQuota=300%

ProtectSystem=strict makes the whole filesystem read-only, so ReadWritePaths has to name every path the agent writes. It names the whole of /config rather than the two subdirectories under it: /config is a partition VyOS mounts during boot, and systemd builds the unit's mount namespace before that happens, so /config/wheelhouse does not exist yet. Naming it fails the unit; naming it with a - prefix makes systemd skip it, leaving the agent nothing writable at all.

wheelhouse-firstboot.service#

ini
After=vyos-router.service
Requires=vyos-router.service
Before=wheelhouse-agent.service
ConditionPathExists=!/config/wheelhouse/api-key
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=600

It runs until it has produced the API key and then never again — including after an image upgrade, because /config survives one and the condition stays false. The script refuses to write the key file unless the commit really landed, so a failed run leaves the condition true and the next boot tries again rather than starting an agent holding a key the router never accepted. See What first boot does.

wheelhouse-console.service and its timer#

ini
Before=getty-pre.target getty.target getty@tty1.service serial-getty@ttyS0.service
After=vyos-router.service
Type=oneshot
ExecStart=/usr/lib/wheelhouse/console-banner.sh
WantedBy=multi-user.target getty.target

agetty reads /etc/issue once, when it opens the terminal, so the unit is ordered before the getty units by name. Ordering only Before=getty.target is not enough: getty@.service and serial-getty@.service both carry Before=getty.target themselves, so the target is reached after them.

WantedBy names two targets. multi-user.target makes it run at all; getty.target puts it in the same transaction as the gettys, which is what the ordering needs. Wants=, not Requires=, so a banner that cannot be written does not keep the login prompt off the console.

The package pre-creates both of this unit's wants-symlinks — multi-user.target.wants and getty.target.wants — rather than leaving either to systemctl enable, which never runs in the image build.

It is not ordered on network-online.target or on the agent: the agent can take minutes to start, and every getty would wait with it. The banner draws with what it knows, and the timer fills in the addresses as they arrive.

ini
[Timer]
OnBootSec=0
OnUnitActiveSec=20s
AccuracySec=5s

OnBootSec=0 rather than a delay, so that on the boots where the service has not already run — a getty started early, an address that arrived late — the first refresh is immediate.

wheelhouse-live-installer#

A systemd generator at /lib/systemd/system-generators/wheelhouse-live-installer. It exits immediately unless /usr/lib/live/mount/medium/live/filesystem.squashfs exists — that is, unless this is a live boot. On a live boot it drops one file:

ini
# getty@tty1.service.d/wheelhouse-installer.conf
[Unit]
Description=Wheelhouse installer on tty1 (live image)
[Service]
ExecStart=
ExecStart=-/usr/bin/wheelhouse-install --auto
StandardOutput=tty
StandardError=tty
Environment=TERM=linux

A generator rather than a second unit competing for tty1, so getty@tty1 keeps its restart-forever semantics: when the installer hands the console back as a login prompt and that session ends, the installer returns. Installed systems keep the normal login prompt.

Both streams are the terminal because dialog draws on stdout and reports its answers on stderr.

What postinst does#

sh
if [ "$1" = configure ] && [ -d /run/systemd/system ]; then
    systemctl daemon-reload || true
    systemctl start wheelhouse-console.timer || true
    systemctl try-restart wheelhouse-agent.service || true
fi

Every call is allowed to fail, because inside the image build's chroot there is no systemd to talk to. On a running router the effect is that installing the package picks up new units and restarts the agent, so an upgrade takes effect without a reboot. prerm stops the agent on removal, under the same guard.

Documentation= on all three services points at file:/usr/share/doc/wheelhouse-agent/README, which says what this is and where the corresponding source comes from.

Changing the agent's flags#

Never edit /lib/systemd/system/wheelhouse-agent.service — the next package upgrade replaces it. Use a drop-in, and remember that adding to ExecStart means clearing it first:

bash
sudo mkdir -p /etc/systemd/system/wheelhouse-agent.service.d
sudo tee /etc/systemd/system/wheelhouse-agent.service.d/oidc.conf >/dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/wheelhouse-agent \
  --api-url https://127.0.0.1 \
  --api-key-file /config/wheelhouse/api-key \
  --admin-token-file /config/wheelhouse/admin-token \
  --license-key-file /config/wheelhouse/license-key \
  --addr 0.0.0.0:8443 \
  --tls-self-signed \
  --ui-dir /usr/share/wheelhouse/ui \
  --data-dir /config/wheelhouse \
  --log-level info \
  --oidc-issuer https://auth.example.com/application/o/wheelhouse/ \
  --oidc-client-id wheelhouse \
  --oidc-client-secret-file /config/wheelhouse/oidc-secret
EOF
sudo systemctl daemon-reload
sudo systemctl restart wheelhouse-agent
systemctl cat wheelhouse-agent          # confirm what is actually in force

The developer install's unit#

install/wheelhouse-agent.service is a separate, smaller unit for the developer path. It is not what ships, and the shipped unit carries everything it carries and more.

See also#

Updated 2026-09-03 systemd units boot