Wheelhouse docs

Privacy#

A Wheelhouse router makes two outbound requests that you did not ask for, once a day each, to two hosts:

WhatToCarrying
A licence refreshThe licence serverFour fields — your key, an installation fingerprint, the host name and the agent's version
An update checkThe download hostNothing. It is a plain GET of a static file: no key, no fingerprint, no host name, not even a version in the User-Agent

That is the whole of it. There is no telemetry, no analytics, no crash reporting and no usage metrics. Nothing counts your rules, your interfaces, your clients or your traffic and sends the number anywhere. This page states both requests field by field and shows how to turn either one off.

The licence refresh#

What is sent#

Four keys, and no others. The values below are examples; three of the four differ per router:

json
{
  "key": "WHL1.<kid>.<payload>.<signature>",
  "fingerprint": "9f3c1d2a4b5e6f70",
  "hostname": "edge",
  "version": "0.6.2"
}
FieldWhat it isWhy it is there
keyYour current licence token, the WHL1.… stringIt is the thing being refreshed
fingerprint16 hexadecimal characters derived from this installation's machine id — the formula is belowIt lets the server count how many routers are using one licence, which is what a plan's router limit means. It is a one-way hash; the server never learns the machine id
hostnameThe router's host name as hostname reports it — whatever you typed at install. If the system cannot answer, the literal wheelhouse is sentSo the licence page can say which of your routers an activation is, rather than showing you a list of hashes
versionThe agent's version string, for example 0.6.2So a support conversation starts from what you are actually running

That is the complete body of the licence refresh. Nothing else is added, and there is no separate telemetry request.

The fingerprint is exactly this:

first 8 bytes of SHA-256( trim(contents of /etc/machine-id) + "|wheelhouse" ), in hex

If /etc/machine-id is empty or unreadable, the router's host name is hashed in its place. The machine id itself never leaves the router.

The connection itself reveals what any HTTPS request reveals: your router's public IP address, and the time of the request. The licence server sees those the way any web server sees them.

Where it goes#

A POST to /v1/refresh on the licence server, over HTTPS, with Content-Type: application/json and a 20-second timeout. The server is a build-time default — https://license.rhymelikedi.me — settable with --license-server. The value in use is shown in the web UI on the Licence page under Licence server, and in GET /api/license along with the fingerprint the router will send.

How often#

WhenWhat
About 20 seconds after the agent startsThe first attempt
Every 24 hours after a successThe routine refresh
Every hour after a failureRetry until one succeeds
Immediately after a key is storedSo the Licence page can show the server's answer straight away

What comes back, and what the agent does with it#

The response is JSON: a fresh token, a status, an expires_at, a message, an error and a features list. The agent reads at most 64 KiB of it. If a new token is present and verifies, it replaces the stored one — in state.json, so the next start holds it. The token's expiry is the sooner of your paid-through date and 30 days ahead, which is what lets an offline router keep working.

AnswerEffect on the router
200The new token is adopted; the status and message are shown on the Licence page
403The licence is recorded as revoked and the write plane closes at once
400, 402, 404, 409The refusal and its reason are recorded and displayed; the existing token keeps working until its own expiry
Anything else, or no answer at allNothing changes. The failure is recorded as cannot reach the licence server: … and the current token stays good until its expiry plus its grace period

A network that eats the request cannot cost you your router.

What happens if it never runs at all#

Nothing breaks. A licence key is a signed statement the agent verifies offline, against an Ed25519 public key compiled into the binary. A router that has never reached the licence server and never will still knows what it holds, and keeps working until the token's expiry plus the grace period carried in the token — currently a 7-day policy on the server side.

Refresh exists so that a renewal extends your token without you touching the router, and so that a revoked licence stops working within a day. Neither is required for the router to run.

The update check#

The download host publishes a channel index beside every release — stable.json or beta.json — naming the newest version on that channel, its notes and its checksum file. The agent reads it so the System page can say that a newer version exists.

What is sent#

For the channel index, nothing. It is a plain GET of a static file over HTTPS. No licence key, no fingerprint, no host name, and the User-Agent is the bare string wheelhouse-agent with no version in it — the download host has no need to know which release is asking, and a static file could not use it. As with any HTTPS request, the connection itself reveals your router's public IP address and the time.

When the index names a version newer than yours, the two requests that follow — that release's checksum file and its notes — carry the licence key, as the password of the URL, because the download host serves a release directory only to a licensed router. Nothing else about the router goes with them.

Where it goes, and how often#

Hosthttps://releases.rhymelikedi.me, a build-time default. PUT /api/admin/updates can point it at your own mirror; the System page changes only the channel and the on/off switch
First check45 seconds after the agent starts
ThenEvery 24 hours, or every hour after a failure
Requests per checkOne — the channel index. When the index names a version newer than yours, two more follow: the checksum file published beside it, and that release's notes
RecordedEvery change to the policy is an audit entry, update-policy, naming who made it

What it does with the answer#

It says so in the interface, and nothing else. It installs nothing: installing an image is add system image through the System page, admin-only, audited and typed to confirm. The release notes are checked against their entry in the published SHA256SUMS before a word of them is shown — both files come from the same host, so that proves agreement rather than authenticity, and the page says so in those words rather than showing a reassuring tick.

Turning them off#

The licence refresh#

Pass an empty licence server. With one, refresh() returns immediately and no request is ever made.

bash
sudo systemctl edit wheelhouse-agent
ini
[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 \
  --license-server '' \
  --addr 0.0.0.0:8443 \
  --tls-self-signed \
  --ui-dir /usr/share/wheelhouse/ui \
  --data-dir /config/wheelhouse \
  --log-level info

A systemd override has to restate the whole ExecStart, so copy the rest of the line from /lib/systemd/system/wheelhouse-agent.service first. An image upgrade replaces the unit file and keeps your override.

The Licence page shows refresh disabled under Licence server when it is off.

The update check#

A switch, not a unit file. Clear Check for a newer release once a day on the System page, or, with an administrator token:

bash
curl -sk -X PUT -H "Authorization: Bearer wh_…" \
  -H 'Content-Type: application/json' \
  -d '{"enabled": false}' \
  https://<router>:8443/api/admin/updates

The setting is stored in updates.json in the agent's data directory, so it survives a restart and an image upgrade, and turning it off is itself an audit entry. With it off no request is ever made, and the page says checks are off rather than telling you the router is current. That is the right arrangement for an air-gapped or egress-filtered site; the cost is that nothing on the box will tell you a release exists — Watch for releases.

What is stored on the router about you#

Three files under /config/wheelhouse/, all mode 0600.

state.json — what the router itself cannot remember for the agent:

  • Accounts. User names, roles, Argon2id password hashes and salts, TOTP secrets, when each account was created and when it last signed in.
  • Email addresses and display names, but only for accounts created through single sign-on, and only what your own identity provider asserted.
  • Live sessions. The session id, the account, the source address, the browser's user-agent string, and — for SSO sessions — the ID token, kept so that signing out can also sign you out at the provider.
  • API tokens. Label, role, a SHA-256 of the token, who created it, when it was last used. Never the token itself.
  • Settings, including your licence key and the syslog collector the audit log is copied to, if you have named one.

recovery.json — the two-factor recovery codes, hashed with Argon2id exactly as passwords are, keyed by account name, with the time the set was issued and which of them have been spent. Never the codes themselves: they are shown once, at enrolment, and the router cannot show them to you again. It is a separate file so that a spent code does not rewrite the credential file, and so that an agent rolled back to a build without this feature leaves the codes alone instead of dropping them on its next write.

audit.jsonl — the audit log, one JSON object per line, appended and synced as each entry is written. Every entry carries the actor, their role, their source address, the operation, the configuration path, the exact commands and whether it succeeded, plus a sequence number, the hash of the entry before it and its own hash. The file rotates at 8 MiB and one previous generation is kept as audit.jsonl.1, so the log costs at most twice that on disk. The audit_retention setting — 2000 entries by default — bounds the window the agent holds in memory and serves from GET /api/audit; older entries stay in the file.

The audit log can be copied off the box, and only if you say so. Naming a collector in audit_syslog sends every entry to it as it is written, framed as RFC 5424, so an operator can prove the router's own log has not been rewritten since. Nothing is configured by default: a router as shipped names no collector, makes no such connection, and keeps the only copy of its log. Entries are also downloadable by an administrator through GET /api/audit/export, which is a request you made — The audit log.

Other things the router talks to, because you asked it to#

None of these is Wheelhouse phoning home. Each is something you configured.

WhatWhenTo whom
Your identity providerEvery SSO sign-in, and discovery at start-upThe issuer URL you configured
A container registryWhen you install or update an appDocker Hub or GHCR, depending on the app; the router pulls the image directly
DNS and NTPContinuouslyWhatever the router's configuration names. The image's default NTP servers are pool.ntp.org
A URL you pasteWhen you install a system imageWhatever host that URL names
The internet at largeWhenever you route a packetIt is a router

What is held about you elsewhere#

Whatever a licence purchase creates — an account, an email address, a subscription — lives on the licence server and with the payment processor, not on the router. Billing is Stripe's; the licence server never sees a card.

Neither the retention period for licence-refresh records nor the formal privacy notice a data protection officer will ask for has been written. This page describes what the router sends, which is a question about the code and is answerable today; what the licence server keeps and for how long is a question about a policy that does not exist yet.

What this website collects#

These pages are static files. There is no analytics, no tracking pixel, no embedded third party and no cookie. Nothing on a page loads from another host: the one typeface, the stylesheet, the search index and about 11 KB of JavaScript all come from the same origin, which is why the site can be served under a content security policy of default-src 'none' with 'self' and nothing else.

  • Search runs in your browser. The index is built at build time and fetched from this host; no query is ever sent anywhere.
  • The light/dark toggle writes one key, wh-theme, to your browser's local storage. It never leaves the browser.
  • The web server keeps request logs, as almost every web server does. What is kept, and for how long, has not been decided; there is no published retention policy for this site.

Where to check this yourself#

Watch it from the network. There are two unprompted requests and no others: one POST a day to the host the Licence page names, and one GET a day to the host the System page names. Switch both off as above and a packet capture on the router's uplink should show neither, ever.

See also#

Updated 2026-09-18 privacy licence telemetry