Privacy#
A Wheelhouse router makes exactly one outbound request that you did not ask for: a licence refresh, once a day, four fields, to one host. There is no telemetry, no analytics, no crash reporting, no usage metrics and no update check. Nothing counts your rules, your interfaces, your clients or your traffic and sends the number anywhere. This page states the request field by field, names the code that builds it, and shows how to turn it off — because a claim like that is worth nothing unless you can check it.
The licence refresh#
What is sent#
agent/license.go builds the body in refresh() and posts it.
Four keys, and no others — with example values, since three of the four differ per router:
{
"key": "WHL1.<kid>.<payload>.<signature>",
"fingerprint": "9f3c1d2a4b5e6f70",
"hostname": "edge",
"version": "0.5.1"
}| 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.5.1 | So a support conversation starts from what you are actually running |
That is the complete body. Nothing else is added, and there is no separate telemetry
request. The functions are refresh(), licenseFingerprint() and hostnameOrDefault();
the version is the version constant in agent/main.go.
The fingerprint is licenseFingerprint(), and it 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, and the value in use
is shown in the web UI on the Licence page under Licence server, so the router tells you
where it is talking to rather than you having to believe a document. It is also 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 (licenseRefreshInitial) |
| Every 24 hours after a success | The routine refresh (licenseRefreshEvery) |
| Every hour after a failure | Retry until one succeeds (licenseRefreshRetry) |
| Immediately after a key is stored | So the Licence page can show the server's answer straight away (handleLicenseSet) |
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 (ADR-003).
| 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. That is deliberate: losing the ability to see a router because a refresh failed would be a worse product than losing the ability to change it.
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
(PRICING.md).
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.
Turning it off#
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 —
packaging/wheelhouse-agent.service. An image
upgrade replaces the unit file and keeps your override.
The Licence page shows refresh disabled under Licence server when it is off, so the
state is visible rather than assumed.
What is stored on the router about you#
Two files under /config/wheelhouse/, both mode 0600 —
agent/store.go.
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.
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. 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.
None of that leaves the router. There is no mechanism in the agent to export the audit log anywhere, which is a gap for anyone who wants it in a SIEM and a privacy property in the meantime — What is finished, and what is not.
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 (PRICING.md).
How long licence-refresh records are retained, and the formal privacy notice a data
protection officer will ask for, are not written yet: <TODO: owner>.
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 —
site/deploy/Caddyfile.snippet.
- 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 in Caddy's JSON format, as almost every web server
does. What is kept and for how long is
<TODO: owner>.
Where to check this yourself#
Everything on this page above the website section is one file. The payload is built in
refresh(), the fingerprint in licenseFingerprint(), the schedule in
runLicenseRefresher() — all in agent/license.go. If you have
the source, read it. If you do not, watch it from the router: the request is one POST a day
and it is the only unprompted one there is.
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.
- Files on the router — every file the agent owns, and its mode.
Checked against#
agent/license.go ·
agent/main.go ·
agent/store.go ·
docs/privacy.md ·
docs/security.md ·
docs/adr/003-licensing.md ·
PRICING.md ·
packaging/wheelhouse-agent.service ·
site/deploy/Caddyfile.snippet