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#
show configuration commands | match firewallAn 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:
nc -vz 203.0.113.2 8443 # should not connectFor belt and braces, bind the agent to one address with a systemd drop-in rather than by editing the packaged unit:
[Service]
ExecStart=
ExecStart=/usr/bin/wheelhouse-agent … --addr 192.0.2.1:84433. 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:
| Role | May |
|---|---|
viewer | Read operational and configuration state. Configuration reads are redacted — private keys, pre-shared secrets and password hashes come back as [redacted]. |
operator | Everything a viewer can, plus stage, commit, commit-confirm, roll back, reconcile, install apps |
admin | Everything an operator can, plus accounts, API tokens, agent settings, the licence, power and boot images, and the unredacted configuration download |
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.
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.
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-authentication8. 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.
sudo sh -c 'umask 077; openssl rand -hex 24 > /config/wheelhouse/admin-token'
sudo systemctl restart wheelhouse-agentIt 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.
| Gap | What it means for you |
|---|---|
| The agent binds every interface | The firewall is the only thing keeping 8443 off the WAN. Item 2. |
| No admin reset for two-factor, no recovery codes | Item 5. |
| The audit log is not tamper-evident and cannot leave the box | Item 10. |
| Fleet writes bypass staging and review | POST /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 unauthenticated | The 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 admin | Redaction 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 entry | Recovery is the console and the previous image. Locked out of the UI. |
| Releases are not signed | A checksum detects a corrupt download, not a hostile one. Learn that a release exists. |
| No IPv6 in the operational views | A dual-stack router can be configured for IPv6 but not usefully inspected for it. |
| Container images are floating tags | The 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:
# 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.2Then 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#
- A default-drop ruleset that admits what you meant
- Publish a service — the correct way to open one thing
- Locked out of the UI
- What is finished, and what is not
- What listens, and where
Checked against docs/security.md ·
agent/main.go ·
agent/security.go ·
agent/store.go ·
packaging/wheelhouse-agent.service