What the licence gates#
A Wheelhouse licence unlocks configuration changes on a router, and nothing else. Without a usable licence the agent still signs you in and still serves every page, every counter, every log and the whole audit log. It refuses to add or apply changes, restore a revision, install an app or run the reconcile loop, saying so in one line with a link to the Licence page.
The gate is on the write plane, with one exception: the two fleet reads below, which read a different router.
Where the gate sits#
Every route in the write plane is gated twice — the operator role first, then the licence.
| Needs a licence | Does not |
|---|---|
POST /api/stage, /api/stage/remove, /api/commit, /api/commit/confirm, /api/discard | Every GET about this router, including /api/config, /api/audit, /api/history and the telemetry stream |
POST /api/configure, /api/rollback, /api/config/save, /api/config/load | /api/auth/* — sign-in, sign-out, password change, TOTP enrolment, session listing |
POST /api/reconcile, /api/capture | GET /api/license itself, and GET /api/auth/status, which carries a licensed flag so the sign-in screen can say so |
Every /api/apps/* write | GET /metrics |
GET /api/fleet/{id}/config, GET /api/fleet/{id}/version — the two reads that are about a different router, and which also need the fleet feature | |
POST /api/diagnostics/traceroute and POST /api/ids/ips/plan — operator, but commands and plans rather than configuration changes | |
wheelhouse-agent plan, which reads and prints and changes nothing |
The refusal is a 402 Payment Required whose body names the fix rather than just refusing:
{"error": "a licence is required to change configuration; enter one under Admin → Licence",
"license": "unlicensed"}When the licence exists but has a problem, the message is that problem — the licence expired on 2026-08-14 — followed by configuration changes are disabled.
The key, and why it works offline#
A key is WHL1.<kid>.<payload>.<signature>: an Ed25519 signature over a small JSON payload
naming the licence id, the customer, the plan, the features, the router limit, the issue
time, the expiry and the grace window. The agent verifies it against a public key compiled
into its own binary, so a router with no route to the internet still knows what it holds.
kid identifies which public key signed it, so a rotation ships the new public key in
a release and keeps the old one for existing tokens. A build carrying no key can
verify nothing and refuses every licence — a build without keys is a build that cannot
be licensed, not one licensed for free.
The states#
GET /api/license reports one of these, and the Licence page shows it:
| State | Writes | Meaning |
|---|---|---|
unlicensed | closed | No key. |
valid | open | Inside the token's expiry. |
grace | open | Past expiry, inside the grace window. The page says when changes stop. |
expired | closed | Past expiry and past grace. |
revoked | closed | The server said so on a refresh. |
invalid | closed | The stored key does not parse or does not verify — a bad signature, or a key id this build does not know. |
clock-unverified | open | The router's clock reads earlier than the licence was issued. |
The last state matters on a box with a bad clock. A clock reading before the issue time
cannot be trusted to judge expiry either, and a forward jump from a broken time source
would otherwise close the write plane — with set service ntp behind that same gate, so
the fix would be locked behind the fault. The agent stays open and says why: this router's
clock reads before the licence was issued; set the time (System → Settings → Time synchronisation) so expiry can be
judged.
The grace window is a field in the token, in seconds, not a constant in the agent. The licence server issues seven days.
The daily refresh#
While the licence server is reachable, the agent refreshes: about 20 seconds after start, then every 24 hours, retrying hourly after a failure, and immediately after a new key is entered.
The request is a POST to /v1/refresh at the licence server —
https://license.rhymelikedi.me by default, changeable with --license-server, and
disabled entirely by --license-server ''. It carries four fields and nothing else:
the key, a fingerprint of this installation, the router's host name and the agent version.
The fingerprint is 16 hexadecimal characters — the first eight bytes of SHA-256 over
/etc/machine-id plus a fixed string — so the server can count how many routers are using
one licence without learning the machine id. Privacy sets out every
field and what the connection itself reveals.
What the answers do:
| Answer | Effect |
|---|---|
| A fresh token | Stored and used from then on; its expiry follows what has been paid for. |
| Network failure | Nothing changes. The current token stays good until its expiry plus grace. |
403 | Revoked. The write plane closes at once, and the page says why. |
402, 404, 409, 400 | Recorded, with the server's reason, so the page can show it. Whatever token is held keeps whatever validity it had. |
Features within a plan#
Some capabilities are named in the token's features list rather than gated by
expiry. Today that is fleet: /api/fleet/{id}/* is wrapped in
requireFeature("fleet", …), which is requireLicense plus a check on the list, and
answers 402 with the fleet feature is not part of this licence's plan. Fleet
configuration loads regardless of the licence; the routes are what is gated.
Entering and removing a key#
- In the UI: Admin → Licence, which is
PUT /api/admin/licenseand needs the admin role. The key is verified before it is stored, so a typo is refused with the reason. - On the command line:
--license-key-file /config/wheelhouse/license-key, a 0600 file. The agent refuses to read a secret file that is group- or world-readable. A key given by flag is also saved, so the next start without the flag still holds it.
Either way the key is stored in the agent's state file with the settings, and the settings endpoints never return it. Setting and removing a key are both audited.
What this gate is not#
The licence also does not touch the GPL boundary. It covers Wheelhouse's own agent and UI; the router underneath is GPL and its corresponding source is published — see Wheelhouse and VyOS.
See also#
- Read plane, write plane, admin plane — the other gate on the same routes.
- Licence and plans — the plans, and what an unlicensed router still does.
- Privacy — the daily refresh, field by field.
- Entering a licence — the task.