Wheelhouse docs

Harden a router that faces the internet#

You will end up having made ten deliberate decisions about a box that has the internet on one side of it. Not "secured" — that word means nothing on its own. Each item below names a mechanism, says what it costs, and gives you the commands, so you can judge which ones your network needs.

Do them in this order. It is roughly the order of value.

Before you start#

  • Admin role on the web UI, and console access for the two items that need a shell.
  • A backup, taken before you begin.
  • An hour. Several of these can end your session, and doing them in a hurry is how that becomes a drive to the site.

1. Confirm you have a firewall, and that it covers IPv6#

bash
show configuration commands | match firewall

An empty answer on a box with a WAN is an emergency. A box installed with the firewall unchecked and plugged into a modem has its management UI on the internet protected by one password.

Check both families, not one. A box filtered on IPv4 and open on IPv6 is not filtered, and a provider can bring IPv6 up without asking. A default-drop ruleset that admits what you meant has the ruleset the installer writes and how to extend it.

2. Take the management UI off the WAN#

This is the one that matters most, and it needs explaining.

The agent binds every interface. --addr 0.0.0.0:8443 is in the shipped systemd unit. Nothing but the firewall keeps the web UI off the internet, and there is no listen-address setting in the product.

The clean answer is a firewall rule: the input chain defaults to drop and admits the LAN, so with a normal installer-written ruleset the UI is already LAN-only. Confirm that from outside:

bash
nc -vz 203.0.113.2 8443     # should not connect

For belt and braces, bind the agent to one address with a systemd drop-in rather than by editing the packaged unit:

/etc/systemd/system/wheelhouse-agent.service.d/addr.conf
[Service]
ExecStart=
ExecStart=/usr/bin/wheelhouse-agent … --addr 192.0.2.1:8443

3. Replace the self-signed certificate#

So that a certificate warning means something again. A browser that has been trained to click through one warning clicks through the next one.

Issue a certificate and bind it to the web UI — and read the caveat there about which HTTPS service the checkbox binds to.

4. Give every person their own account, with the lowest role that works#

Nobody should share admin. The audit log records who did what, and it can only do that if "who" is a person.

Administration → Users → + Add user. Three roles, enforced server-side on every route:

RoleMay
viewerRead operational and configuration state. Configuration reads are redacted — private keys, pre-shared secrets and password hashes come back as [redacted].
operatorEverything a viewer can, plus stage, commit, commit-confirm, roll back, reconcile, install apps
adminEverything an operator can, plus accounts, API tokens, agent settings, the licence, power and boot images, and the unredacted configuration download

Add an operator.

5. Turn on two-factor — after reading this#

TOTP, enrolled per account and verified with a live code before it is switched on, so a mis-scanned QR cannot lock anyone out. require_totp in agent settings makes it mandatory for everyone.

Enrol a second factor.

6. Use API tokens for automation#

One per consumer, with an expiry, revoked when the consumer goes away. Tokens carry a role, only their SHA-256 is stored, and the plaintext is shown exactly once. They begin wh_, which makes a leaked one findable by a secret scanner and greppable in a log.

Use a token rather than a cookie for anything scripted: token requests skip the CSRF header requirement that browser sessions have.

Issue a token for automation.

7. Leave SSH off, or bind it and use keys#

A freshly installed router has no SSH at all — neither the image's configuration nor the installer writes service ssh. That is the right default.

If you need it: bind it to the LAN address, put keys on the console accounts, and turn off password authentication.

set service ssh
set service ssh listen-address 192.0.2.1
set service ssh disable-password-authentication

Turn SSH on, and restrict it.

8. Shorten the session lifetime#

Twelve hours by default, adjustable in Administration → Agent settings. Shorten it if the machine is somewhere people walk past. Changing a password signs out that account's other sessions, and you can see and revoke your own live sessions on the Account page.

9. Take backups off the box and encrypt them#

A backup is a credential: it holds password hashes, TOTP secrets, the router's API key, every pre-shared key and private key in the configuration, and your licence.

Take a backup that is actually complete.

10. Watch the audit log#

System → Audit records who, from where, with which role, and the exact operations. It also records refusals, as denied <method> <path> with the actor — which is how you find out that somebody's token is being used for something it should not be.

Two more, if the router matters#

Create a break-glass token, deliberately. Nothing creates one, so a default install has none, and its absence is what turns a forgotten password into a console trip.

bash
sudo sh -c 'umask 077; openssl rand -hex 24 > /config/wheelhouse/admin-token'
sudo systemctl restart wheelhouse-agent

It is a full admin credential in a file. Treat it as one, or leave it absent on purpose.

Turn on the IDS, if you have the CPU for it. Turn on Suricata IDS.

The gaps you cannot close today#

These are real, they are the product's own list, and none of them has a workaround inside the product.

GapWhat it means for you
The agent binds every interfaceThe firewall is the only thing keeping 8443 off the WAN. Item 2.
No admin reset for two-factor, no recovery codesItem 5.
The audit log is not tamper-evident and cannot leave the boxItem 10.
Fleet writes bypass staging and reviewPOST /api/fleet/{id}/configure is a direct configure with no diff and no Commit Bar. It is audited on this agent. Point one agent at several routers.
GET /api/oidc/status discloses the issuer URL unauthenticatedThe login screen's own probe deliberately withholds it; this route undoes that. If your identity provider is internal, treat its hostname as public on any router whose management port an attacker can reach.
Secrets in some configuration shapes are not redacted below adminRedaction matches secret leaf names. A value that is a secret because of where it sits rather than what it is called — a container's environment variable, an SNMP community string — is not matched. Assume a viewer can read those.
No factory reset, no rescue boot entryRecovery is the console and the previous image. Locked out of the UI.
Releases are not signedA checksum detects a corrupt download, not a hostile one. Learn that a release exists.
No IPv6 in the operational viewsA dual-stack router can be configured for IPv6 but not usefully inspected for it.
Container images are floating tagsThe one part of an app install that is not reviewable is the part that runs code on your router. Install an app.

Check it worked#

Work down your own list from outside the network:

bash
# The management port
nc -vz 203.0.113.2 8443
# SSH
nc -vz 203.0.113.2 22
# Anything you did not mean to publish
nmap -Pn -p- 203.0.113.2

Then from the LAN, confirm you have not broken your own access. And sign in and read System → Audit: everything you just did should be in it, attributed to you.

Undoing it#

Each item is independent. The riskiest to reverse is item 2 — if a drop-in binds the agent to an address the machine no longer has, the agent will not start, and the fix is at the console.

See also#


Checked against docs/security.md · agent/main.go · agent/security.go · agent/store.go · packaging/wheelhouse-agent.service

Updated 2026-09-02 security hardening checklist