Wheelhouse

Security#

For the person who has to sign off on putting this at an edge: what listens on a fresh install, what the firewall checkbox writes, how accounts work, what is not defended against, and where a flaw goes.

What is finished, and what is not Accounts and access control

Nothing here says "secure". It describes mechanisms and lets you judge them.

What is listening, on a router that just finished installing#

PortServiceBound toReachable from
8443/tcpthe Wheelhouse agent: web interface and API0.0.0.0 — every interface, including the WANanything the firewall lets through
443/tcpthe platform's own HTTPS API127.0.0.1 onlythe agent, and root on the box
53/udp, tcpDNS forwarderthe LAN addressthe LAN subnet only
67/udpDHCP serverthe LAN interfacethe LAN
123/udpNTP client and server—loopback, link-local and private ranges on both families
22/tcpSSHnot configurednothing

Four of those matter.

The agent binds every interface. The firewall is the only thing keeping the management interface off the internet, and there is no listen-address setting short of editing the systemd unit — which an image upgrade replaces.

The platform's own API is pinned to loopback on first boot. The first-boot unit mints a random API key for the agent, enables the REST endpoints and binds them to 127.0.0.1. The router's API is not a network service on a Wheelhouse box; the agent is the management surface.

SSH is off. Neither the image's default configuration nor the installer writes service ssh, so a freshly installed router has no SSH at all and the console is the only shell. Turn it on deliberately, restrict it, and prefer keys.

NTP is a client and a server, answering only loopback, link-local and the private ranges of both address families — 127.0.0.0/8, 169.254.0.0/16, the three RFC 1918 blocks, ::1/128, fe80::/10 and fc00::/7. It does not answer the WAN.

Six routes answer without authentication#

GET /health, GET /api/auth/status, GET /api/oidc/status, the two OIDC redirect legs (/api/oidc/login and /api/oidc/callback), and POST /api/auth/login behind all three rate limiters. That is the whole of the API's unauthenticated surface: every other route requires a session or a token, and a role with it.

Two things outside the API also answer anonymously. The static handler serves the interface's own HTML, script and fonts to anyone who can reach the port, because a login screen has to be fetchable before anyone can log in. And /metrics, which needs the viewer role by default, is served to anyone when you pass --metrics-public — a flag that publishes far more than a version string, so treat it as opening a port rather than as a convenience.

GET /health answers {"status":"ok","version":"<agent version>"}. It is the only version answer available before a login, and nothing else about the box is in it — no host name, no licence state, no router reachability.

GET /api/oidc/status tells the login screen whether single sign-on is configured and whether the provider is answering. It does not return the issuer URL, and a discovery error has the issuer's host name scrubbed out of it before it is shown, because for a self-hosted identity provider that host name is reconnaissance. The full issuer is in GET /api/admin/oidc, which answers only an admin.

What the installer's firewall checkbox writes#

Whenever the box is left ticked and there is a WAN. A LAN is not required:

The shape of it
state policy   established accept, related accept, invalid drop
ipv4 input     default drop; accept from the LAN, from lo, ICMP,
               the DHCP client's return traffic on the WAN,
               and a management source if you named one
ipv4 forward   default drop; accept inbound from the LAN
ipv6 input     default drop; the same, plus ICMPv6 and DHCPv6
ipv6 forward   default drop; accept inbound from the LAN, ICMPv6 through

Both address families are filtered. A box filtered on one and open on the other is not filtered, and a provider can bring IPv6 up without being asked. If the image has no firewall ipv6 node the installer says so at the end rather than silently committing a v4-only ruleset.

ICMP is accepted on every interface, WAN included. On IPv4 that is a choice you can narrow. On IPv6 it is not a choice: neighbour discovery and path-MTU discovery are ICMPv6.

Nothing in it opens 8443 from the WAN.

Turning it off takes a deliberate answer. Unticking the box with a WAN present raises a screen saying the router will accept everything arriving from the internet, and its default answer is Turn it on.

Check what you actually have on any box you did not install yourself:

show configuration commands | match firewall

An empty answer on a router with a WAN is an emergency, not a preference.

Accounts#

Argon2id at 64 MiB and three passes, TOTP verified with a live code before it is switched on, four roles enforced server-side on every route, API tokens carrying a role and an expiry and stored only as a hash, opaque HttpOnly SameSite=Strict session cookies with a CSRF token echoed on every mutation, three separate login rate limiters, and OIDC single sign-on linked by the provider's subject claim rather than by email.

Two-factor can be made mandatory for everyone, and in the current tree the setting is safe to use: an account that has not enrolled meets an enrolment screen standing in front of every route rather than a locked door, and the agent refuses to turn the policy on at all when no admin account can still sign in to complete it. On 0.5.1 that screen does not exist yet and the setting will strand you — the detail is on the access-control page.

The whole of it, including what it does not do — no recovery codes, no administrator reset of a second factor, no per-object permissions — is on the access-control page.

What is not defended against#

  • Root on the router. Root reads every secret, edits the state file and rewrites the audit log with a text editor. There is no tamper-evidence and no off-box copy.
  • Physical access. No disk encryption. Secrets are files on an unencrypted partition, and the console is a login prompt in front of a bootloader you can edit.
  • A compromised browser or administrator workstation. A live session is a live session.
  • Supply chain. Checksums are published; no release is signed, because no signing key exists, and there is nowhere yet to fetch a public key that would make a signature mean anything.
  • Denial of service at line rate. The agent caps request bodies and rate limits logins. It is a management plane, not a scrubbing centre.

Assumed trustworthy: your identity provider if you configure single sign-on, your container registry when you install an app, and the licence server for the one thing it does.

The licence is not a security control#

An unlicensed agent refuses to change configuration. It still authenticates, and it still serves every read but two — the two fleet reads named on the pricing page. It is a commercial gate on a self-hosted binary — do not reason about it as if it protected anything.

Getting fixes#

Where the problem livesHow the fix reaches you
The agent or the interfaceA new agent package, or the next image
The kernel, the base, FRR, Kea, nftables, strongSwan, OpenVPN, PodmanA new image built from a newer vyos-build. There is no in-place package update path
A container you installedThe app's own update action; the image comes from its registry, not from us

Reporting a vulnerability#

The policy: a three-business-day acknowledgement, a severity judgement within ten, a fix or a dated plan within ninety, credit in the changelog if you want it, and safe harbour for good-faith research against your own installation.

There is no bug bounty. There is credit, a reply from a person who read the report, and a fix.

Read on#