What is finished, and what is not#
The list of what is built, what is narrower than its name suggests, what has not been proven, and what is commercially missing. Read it before you move a network onto this.
Built and exercised#
- The agent. A Go binary fronting VyOS' HTTP API: staging, native commit-confirm,
revision rollback, an attributed audit log, parsed operational state (counters,
conntrack, routes, leases, certificates, logs) and a live telemetry WebSocket. It
authenticates real accounts (Argon2id, TOTP, sessions with CSRF, four roles, scoped
API tokens), speaks OIDC as a relying party, serves Prometheus metrics, terminates TLS
and serves the interface from
/usr/share/wheelhouse/ui. It publishes an OpenAPI 3.1 description generated from its own route table, and collects a redacted support bundle. Over a thousand Go test functions, race-clean. - The web UI. A React single-page application: 30 left-navigation entries across eight groups, 39 page components, a form for every setting with each value checked under its field, pending changes read as sentences, one Commit Bar, the commands and the configuration tree in Advanced mode, live state beside configured intent with drift flagged, ⌘K over pages and configuration objects, dark and light.
- The app store. 40 catalogue entries — 37 containers declared in the configuration tree, plus 3 built-in feature modules that gate native pages. Installed apps get a navigation entry, integration offers on the pages they affect, an update check and a restart.
- Migration and unattended installs. An importer that turns an OPNsense
config.xmlinto commands plus a report, with--check-againstto compare a running router with the configuration it should have; and an installer that finds a seed and installs with nobody at the keyboard. The unattended path is broken in every 0.6.x image published so far — 0.6.0, 0.6.1 and 0.6.2; see the note at the top of this page — and works in the tree, where a test holds the fix and an installed machine proves it. - Multi-WAN. Uplinks through every mechanism the platform has, health-checked failover, load balancing, policy routing and per-uplink NAT — proven on a dual-WAN VM built from the ISO.
- Inline IPS. Suricata in the packet path via NFQUEUE, entirely configuration-tree, so it diffs, commits and rolls back. Fail-open twice over, each layer verified separately.
- The image pipeline. CI builds the agent packages and the ISO on every tag, inspects the image before publishing it — volume descriptor, creation stamp against a 48-hour window, the checksum re-computed, and a hard failure if the corresponding-source record or the build-tooling patch is missing — then looks inside it: the shipped package list against the source record, the agent's version against the tag, an empty machine-id, and every document the image promises. What it publishes is the image with its checksum, the corresponding-source files, the shipped package list, the notes and an update-channel index — and no agent package. The packages are build inputs and stay artefacts of the run.
- The download gate. The release host answers a release directory only after the
licence server has seen a usable licence key — as a Bearer token, as the Basic-auth
password, or as the cookie the key form sets — and the channel index is the only public
file. Checked from outside on 2026-09-10: no key,
401; a browser, the key form; a lapsed or revoked licence, the reason; a paid-up key, the image.
Every release is exercised in a browser against a live router replica before it is tagged. That is a person looking at pages, not a recorded suite: it catches what is obviously wrong, and it did not catch an installer that could not install.
Not shipped at all#
Fourteen rows are struck through. They were on this list, they are not any more, and they are left here rather than deleted so that anyone who read this page before can see what moved. None of them is in a tagged release yet.
| Do not expect | What is actually true |
|---|---|
| ~~A capture you can walk away from~~ | Shipped, and proved on the demonstration router. Diagnostics → Capture has Stop it when nobody is watching, on by default: the agent ends the capture 20-25 seconds after the last time any signed-in page asked how it was doing, reports unwatched as the reason and writes the audited stop against whoever started it — any page watching the same capture keeps it alive. The page also asks the router to stop on its way out (fetch with keepalive on pagehide, for the capture it started and no other), which is a speed-up and not the guarantee: a page killed with its machine asks for nothing. Unticked, the old behaviour stands — only the limits end it, at most 300 seconds. The file itself is looked after as it always was: the page withdraws the download when the 15-minute retention deletes it, and an agent that starts up sweeps away the pcap an agent killed mid-retention left behind. On 2026-09-10 a capture started through the API with the option and never polled ended itself 20 s later with reason unwatched |
| ~~UPnP or NAT-PMP~~ | Shipped, and it is a configuration change like everything else. The base has no such service, so the agent speaks SSDP, UPnP IGD and NAT-PMP itself: each mapping a console asks for becomes a destination NAT rule and a matching forward-filter accept in rules 4000-4199, with the client's address and the lease expiry in the rule description. It is off until you turn it on and name the client subnets, because it lets any host on those subnets open a port through the firewall with no operator in the loop — and every mapping is a router commit, a revision and an audit entry. Expiry disables the rules rather than deleting them; only Remove deletes. IGDv2 is partial: no IPv6 pinholes, no DeviceProtection. It needs a licence like every other write and has to run on the router itself. Two things it cannot defend against, said plainly: NAT-PMP asks over UDP and a UDP source can be forged, so a forged request from an allow-listed address can open a port to that address — the allow-list is the whole of the defence; and discovery is a UDP socket that, since 0.8.1, is tied to the LAN interface at the kernel by default (a setting, Discovery on every address, relaxes it), with the source checks, a rate cap and the router's input firewall behind it either way; an SSH tunnel to the management UI is unaffected |
| ~~A one-to-one NAT editor~~ | Shipped, and since 0.9.1 it goes through the Commit Bar. Add 1:1 NAT builds a mapping as a destination NAT rule and the source NAT rule that mirrors it, planned by the agent so both sit ahead of the rules that would otherwise take their traffic, listed as one row and changed and removed as a unit. Wheelhouse does not write nat static: committing it through this build's HTTP API has been seen to stop the API process, while the CLI accepts the same lines, so a nat static rule made at the console is listed and left alone, and the agent refuses nat static at every door that reaches a commit — /api/stage, /api/fleet/{id}/stage, /api/configure, /api/reconcile with apply=true, /api/commit, /api/fleet/{id}/configure and /api/fleet/{id}/commit, so a desired-state file carrying it fails loudly instead of committing |
| ~~An OpenVPN instance editor~~ | Shipped. Protect → OpenVPN → Add instance builds a server or a client, certificates and all: named from the router's pki tree, minted by the agent, or pasted in and checked. Every field of an instance is edited afterwards, and a server's clients are added, revoked and given their .ovpn files from its settings |
| ~~A site-to-site OpenVPN instance from the UI~~ | Shipped, one end at a time. The editor builds one end of a site-to-site tunnel, shared-secret or TLS, and the input firewall rule for its port when asked. It configures one router, so the far end is a second visit |
| ~~An nftables ruleset inspector~~ | Shipped, per ruleset. Firewall → nftables reads one ruleset with the router's own detail view: each rule's Conditions column — the match part of what the router compiled — beside its action, protocol and counters, with the router's raw text underneath. One router call per ruleset, none per rule. What it is careful about: the router deletes the verdicts drop/reject/return/log, the counter and short comments out of that column before printing it, so the action is shown beside it and the page says why; it is the router's rendering of the rules the router generated, so a rule another process installed in a different table is not in it; and nft list ruleset has no read on this platform — for that, the console. It offers every ruleset the platform will render this way (input, forward and output filters, prerouting raw, and named rulesets); output raw is configurable and has no such read. Hit counters still come from firewall statistics |
| ~~Network namespace or VRF panels~~ | A VRF page is shipped, and since 0.9.1 it edits. A VRF is created, changed and deleted with its description, routing table and bound interfaces, planned by the agent so a new VRF gets a table nothing else uses and a deleted one releases its interfaces first, and its static routes are edited with the same form as the Static routes page. The page reads the vrf tree, each VRF's state and its IPv4 routing table in four router calls however many VRFs there are. A VRF's own BGP and OSPF, route leaking between VRFs and its VNI are not edited there, and an edit leaves them in place. Network namespace panels are not built |
| ~~Per-host traffic history~~, or graphs beyond a month | Per-host history is shipped; a month of it has not been watched. The agent folds /proc/net/nf_conntrack every 30 seconds into a second bounded archive on the router — 31 days of hours for up to 256 hosts, 24 hours of minutes for the day's 16 biggest talkers — and Sessions draws it, with no router API call and a poll whose memory does not grow with the table. Two limits it prints: every total is a floor (a flow that opens and closes between two polls is never seen, and a flow's first poll only takes its cursor), and with nf_conntrack_acct off — the platform default — there are no per-flow byte counters, so the archive keeps flow and peer counts and reports null, never zero, beside the button that turns counting on. An agent off the router says why instead of drawing an empty month: that file is the forwarding machine's own kernel state. Graphs beyond a month are still not kept; flow export exists so a collector can keep them |
| Git-backed configuration export | Shipped, and proven against a live remote. System → History → Git export: an SSH remote, the router's own deploy key with a pinned host key, one commit per export authored as whoever made the change. Secrets are redacted, so the repository is a history rather than a backup you can restore from unedited, and an export whose redacted configuration is unchanged makes no commit. On 2026-09-04 the demonstration router pushed its configuration over SSH to a bare repository on another machine — its own ed25519 key, the remote's host key pinned, the router's config.boot arriving redacted as one commit authored as the change's actor |
| ~~An OpenAPI document~~ | Shipped. GET /api/openapi.json serves OpenAPI 3.1, generated from the agent's own route table, and wheelhouse-agent openapi prints it. The endpoint reference is generated from the same table, and a build fails when the two disagree |
| ~~A support-bundle command~~ | Shipped. Support → Download support bundle, wheelhouse-agent support-bundle, or wheelhouse-support at the console. It is redacted against the agent's own secret rules, bounded at 16 MiB, and its manifest.json lists what was left out and why |
| ~~An in-product update notification~~ | Shipped. The router reads the release channel on a timer and the System page says when a newer version exists. It does not install anything |
| ~~A rescue boot entry~~ | Shipped. The install medium carries Rescue an installed Wheelhouse system in both bootloaders; it lands on wheelhouse-rescue, which resets a web UI password, clears a second factor, re-runs first boot, restores a configuration, and says why the agent will not start. Every action that changes something is written into the audit log's chain; the diagnosis is read-only and writes nothing |
| ~~Ethtool or offload controls~~ | The offload half is shipped: the nine interfaces ethernet <if> offload flags are staged from the interface detail panel, with what the kernel currently has on shown beside what the configuration asks for. Ring sizes, queue counts, link speed and duplex are not built |
| ~~Per-app dashboard widgets~~ | A narrow version is shipped. A catalogue entry may declare a stats URL and a path to a single number in the JSON it answers with; the app's card then shows that number and how old the reading is. Two of the 38 entries declare one. The boundary moved from "none" to "only the one number a catalogue entry declares" — there is still no credentialed call into an app's own API, and the health badge is still TCP plus an optional HTTP path |
| Starting or stopping a container out of band | Still no, and now recorded: the platform offers no operational verb for either, so both stay configuration — stop stages disable, start deletes it — and the agent will not drive a systemd unit the platform does not model (ADR 006). Restarting is the exception and is out of band: one restart container command on the router, audited, writing no revision. What is new is that the product now says when the configuration and podman disagree, in all four ways they can — including the container the configuration disables and podman has up anyway, which used to be badged "running" — with the commands that close it either way, the age of the read it was compared from, and "not compared" rather than a guess when the router will not answer for the configuration |
| Not shipped as claimed, in v0.7.0 | Two halves, both about that image's own promises. (a) Its licence and corresponding-source documents are absent — /usr/share/doc/wheelhouse is an empty directory in the shipped filesystem — so show license prints nothing and both banners point at that empty directory, while SOURCES-0.7.0-amd64.txt says the source travels with the image. (b) The published patch covers one file of the five that make up the change, and the build-source tarball is not in the release directory, so v0.7.0's published artefacts do not let anyone reconstruct the tree that built it. What is published beside it: SOURCES-0.7.0-amd64.txt — the repository, the exact commit, the container digest and every package version — and that one-file patch. The remedy is to publish wheelhouse-0.7.0-amd64-build-source.tar.gz into v0.7.0/, which is the owner's call because that directory's SHA256SUMS is signed. Fixed in the tree for the next tag: the documents move to /usr/share/wheelhouse/doc, the build reads the finished image back out of the ISO, and the release gate requires the tarball |
| A signed catalogue index that is actually in use | The agent fetches a signed catalogue index and verifies a detached ed25519 signature against a key compiled into it, refusing anything that does not verify — with the reason on the Apps page. No catalogue signing key exists, so every agent serves the catalogue compiled into it and a corrected entry still needs an agent release |
Shipped, but narrower than the name suggests#
- QoS. Each interface has an upload rate and a download rate, a VLAN included,
though not a VLAN inside another VLAN. Upload is CAKE; download is CAKE or policing (a
limiter with a rate and a burst). CAKE takes bandwidth, flow-isolation, RTT,
ack-filter,flow-isolation-natandno-split-gso. The agent plans each change (POST /api/qos/plan) and the Commit Bar applies it: upload is a policy on the interface's egress, a policed download a limiter on its ingress, and a CAKE download goes through an IFB the agent chooses and reuses. A policy the editor does not model, such as a shaper with classes, is left as it is and named. There is nodocsisoption — no such node exists on this base — and no directtcprogramming; the agent readstc qdisc showand nothing more, and the page says whether the kernel is running what the configuration asks for. Nothing is handed to the console: turning download shaping off leaves the IFB device idle for the next use rather than deleting it, because deleting it is a commit this base has been seen to fault on. - Companions. The page reports the configuration state of two service subtrees —
service dnsandservice dhcp-server— as one of three answers: configured, not configured, or not on this platform, beside what the router's process table shows. Its third panel is UPnP, which is Wheelhouse's own service rather than the router's: it is turned on and off on the UPnP page and has nothing to restart. The agent supervises no external process.
Not yet proven, so not claimed#
- Any second machine. The image has been installed to a disk and rebooted several times, in VMs and on one physical machine — four installer bugs were found that way — the third on real hardware, and the fourth, which stopped every unattended install in 0.6.x, in a VM. No other board, appliance or NIC has ever run it, and one machine is not a compatibility claim.
- Tested hardware. There is no compatibility list. The image's driver packages can be listed; that is not a compatibility claim either.
- In-place upgrade, on hardware. One has now been done in a VM, end to end: the
router downloaded a 595 MiB image over its WAN, validated the image's own checksums,
carried
/configand the SSH host keys into it, and made it the default boot image without touching the running one. A reboot brought it back in 40 seconds with a byte-identical configuration, and setting the previous image as default and rebooting again put it back. No upgrade has been done on a physical machine, and a VM's disk and firmware are the easy case. - Any performance figure. None measured, on hardware or in CI. The resident-memory and idle-CPU budget is a target.
- The interface is tested in jsdom, not in a browser. The agent has over a thousand test functions and CI runs them race-clean. The interface's suite renders nearly every page against answers mostly captured from real routers, and holds each to what it shows and what it stages; the configuration tree, reconcile, Support, About and the licence page have no test yet. jsdom is not a browser: nothing there sees layout, focus order or the telemetry socket, so those are checked by hand before each release. Nothing tested the installer end to end until 0.7.0, which is why three releases shipped unable to install; every release now has to install in a VM before it is tagged.
Security gaps to weigh before you deploy#
- The agent binds every interface by default (
0.0.0.0:8443), so out of the box the firewall is the only thing keeping the management interface off the WAN. Admin → Agent settings → Management address narrows it to one address, kept in/configso an image upgrade does not widen it back. That is narrower, not safe: it decides where the port answers, not who may reach it, and an address set wrong leaves the router reachable from the console rather than from a browser. It also fails open on a downgrade — an agent that does not know the field drops it on its next save and the listener widens back to every interface, deliberately, because an agent that refused to open the state file would be a router with no management plane at all. - A factory reset is not a secure erase. The System page's Factory reset, or
wheelhouse-rescue factory-resetat the console, wipes the agent's data directory and re-runs first boot, keeping the licence key by default. It unlinks files rather than overwriting them, and it revokes nothing another system already holds — an audit log on a syslog collector, a deploy key on a git remote. - Redaction hides what somebody named. It matches configuration paths now, in all
three shapes a configuration reaches a reader — the decoded document,
setcommand text, and the router's own brace-nested format — so a value that is sensitive because of where it sits, like a container's environment variable or an SNMP community string, is matched, and values that only looked like credentials are readable again. The gap that remains, and the reason this is still on the list: a pattern list hides what somebody named. A container'scommand,argumentsandlabelcan carry a token and are deliberately visible, and a leaf a future release adds under a name no rule mentions is covered only by the leaf-name half of the table. - The image is inventoried, not scanned. The dependency scan now gates the tag —
govulncheckandnpm auditrun as a job the image job needs — and every release publishes the exact package list of the shipped image, checked against the source record beside it, along with the corresponding-source tarball that earlier releases left behind. What no part of it does is consult a CVE database: the build host has no scanner and no offline feed, so the substitute is the list plus a diff against the previous release. If your process requires a scan, run one againstPACKAGES-<version>-<arch>.txt. - The in-place agent update is new in this cycle, and the package it installs is
checksummed, not signed. The agent
.debis published in every release directory again from the next tag, behind the same licence gate as the image — nothing on the release host changed to serve it, because a file in a gated directory is gated. What can be verified about it is what can be verified about every other artefact: its digest is a line of the directory'sSHA256SUMS, so a download that survived the network can be proved. Who built it cannot: no release signing key exists, the package carries no signature of its own, there is noaptrepository behind it anddpkg -ichecks nothing. Anyone whose process needs provenance should keep taking whole images and wait for the signing key. - The licence fingerprint derives from
/etc/machine-id, which an image upgrade replaces. Upgrading a router withadd system imagegives it a new fingerprint and costs it its activation until the key is re-entered — found on the development router on 2026-09-10. An install now mints its own id (the build asserts it, the installer enforces it, the install gate checks it), and a second fingerprint derived from a value kept in/configis written but switched off, because the licence server does not yet count the two as one router. Cloning a machine after installation is still one seat for both halves; nothing on a disk can see that.
Reporting a flaw is on the security page.
Commercially not ready#
- Prices set, not yet sellable. Three plans are priced — Pricing — and the licence agreement they would be sold under does not exist.
- No licence agreement and no terms of service. What ships is a stopgap that says in its own first paragraph that it has not been through legal review.
- Checkout is closed. Stripe is wired into the licence server and not configured, because there is nothing to sell under yet. Keys are issued by hand to beta seats.
- No published support address, issue tracker, chat channel or status page. The forge is private, so "file an issue" is not something a customer can do.
- The beta is closed and invite-only.
- It is not open source. The agent and the interface are proprietary, driving GPL components as separate programs. The base is GPL and its corresponding source is published with every release.
None of that is a reason not to look at it
Every page of the manual is public, and a licence key opens the release host. Read the gaps, then decide.
Read on#
- Roadmap — what is planned, in what order, with nothing dated.
- Beta — what a seat is for.
- The wiki keeps the same list, and the known gaps in more detail.