Privacy#
A Wheelhouse router makes two outbound requests that you did not ask for, once a day each, to two hosts:
| What | To | Carrying |
|---|---|---|
| A licence refresh | The licence server | Four fields — your key, an installation fingerprint, the host name and the agent's version |
| An update check | The download host | Nothing. 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:
{
"key": "WHL1.<kid>.<payload>.<signature>",
"fingerprint": "9f3c1d2a4b5e6f70",
"hostname": "edge",
"version": "0.6.2"
}| Field | What it is | Why it is there |
|---|---|---|
key | Your current licence token, the WHL1.… string | It is the thing being refreshed |
fingerprint | 16 hexadecimal characters derived from this installation's machine id — the formula is below | It 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 |
hostname | The router's host name as hostname reports it — whatever you typed at install. If the system cannot answer, the literal wheelhouse is sent | So the licence page can say which of your routers an activation is, rather than showing you a list of hashes |
version | The agent's version string, for example 0.6.2 | So 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 hexIf /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#
| When | What |
|---|---|
| About 20 seconds after the agent starts | The first attempt |
| Every 24 hours after a success | The routine refresh |
| Every hour after a failure | Retry until one succeeds |
| Immediately after a key is stored | So 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.
| Answer | Effect on the router |
|---|---|
200 | The new token is adopted; the status and message are shown on the Licence page |
403 | The licence is recorded as revoked and the write plane closes at once |
400, 402, 404, 409 | The refusal and its reason are recorded and displayed; the existing token keeps working until its own expiry |
| Anything else, or no answer at all | Nothing 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#
| Host | https://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 check | 45 seconds after the agent starts |
| Then | Every 24 hours, or every hour after a failure |
| Requests per check | One — 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 |
| Recorded | Every 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.
sudo systemctl edit wheelhouse-agent[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 infoA 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:
curl -sk -X PUT -H "Authorization: Bearer wh_…" \
-H 'Content-Type: application/json' \
-d '{"enabled": false}' \
https://<router>:8443/api/admin/updatesThe 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.
| What | When | To whom |
|---|---|---|
| Your identity provider | Every SSO sign-in, and discovery at start-up | The issuer URL you configured |
| A container registry | When you install or update an app | Docker Hub or GHCR, depending on the app; the router pulls the image directly |
| DNS and NTP | Continuously | Whatever the router's configuration names. The image's default NTP servers are pool.ntp.org |
| A URL you paste | When you install a system image | Whatever host that URL names |
| The internet at large | Whenever you route a packet | It 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#
- The Wheelhouse licence — the agreement side, as opposed to the key.
- Reporting a vulnerability — if you find the claim on this page is not true.
- What is finished, and what is not — including what the audit log cannot do.
- What the licence gates — why the refresh exists at all.
- The audit log — what is recorded, and what is not.
- Watch for releases — the update check, from the operator's side rather than the privacy one.
- Files on the router — every file the agent owns, and its mode.