NodeRoost

Open source · BSD-3-Clause · self-hosted

A control panel for headscale

Your own Tailscale control server, with a real admin panel.
Add a machine, grant access, send traffic through a chosen node — no editing policy.hujson, no SSH into nodes.

headscale 0.29+ Docker Compose FastAPI + React Linux agent EN / RU
panel.example.com/serverszoom
Servers and devices in NodeRoost

Why

Why a panel when headscale already works

The panel does not replace headscale and does not proxy traffic. It takes over the part that is otherwise done by hand — in the CLI and in the policy file.

TaskWith NodeRoostWithout the panel
Granting accessa “who → where → port” rule, by clickingedit policy.hujson by hand
Isolating personal devicesdevices never see each otheryours to get right in the ACL
A subnet behind a nodea button on the server cardapprove-routes by node ID in the CLI
Traffic to one destinationname the domain — the panel tracks itpick the CIDR, watch for the IP changing
Applying a route on the nodethe agent applies itSSH into every machine
Internet egressevery device gets its own list of gatewaysthe exit node is open to anyone the ACL allows
Who is onlinehistory and Telegram alertsnodes list — this moment only
Onboarding a machinea key and a ready command for the OSmake a key in the CLI, dictate the command
Where a server sitsa country flag from its public addressremember it yourself or go look it up
Backupsa scheduled snapshot, verifiedcopy the sqlite file yourself
A mistake in the rulesrollback to the last working versionedit the file and apply it again

Use cases

One network across any ISP and any NAT

Machines find each other wherever they sit — cloud, office, home, behind someone else's NAT. No public address and no port forwarding needed, and you decide who talks to whom.

Two LANs, no public addresses

The office and the warehouse start seeing each other's networks. Put one node on each side, it advertises its subnet, you approve it — then rules decide who may cross. Neither side needs a static address from its ISP.

A work machine behind someone else's NAT

Carrier-grade NAT, a router you don't own, nobody to ask for a port forward. Put a node on that machine and it shows up on the network. You open access to your laptop only, on one port only — no TeamViewer, no reverse tunnel.

Admin panels and databases with no open ports

The database, the IPMI board, the cluster API and the router's web UI live on tailnet addresses. Nothing is published, no bastion host needed, and you grant a port rather than the whole machine.

A contractor gets exactly what they need

Access is granted to a port, not to the whole machine: give them 5432 and they see the database and nothing else — no SSH, no admin panel. It can go to a role — a group of servers: add another one to it and it shows up for them on its own. Job done — delete their device, and the access is gone everywhere at once.

Internet through a chosen server

Mark a server as a gateway and the traffic of whoever you allowed leaves for the internet from its address. People switch between gateways themselves in the Tailscale client, but only ever see the ones you gave them.

A server's whole traffic through another server

The server reaches the internet through a node you pick, so its requests arrive from that node's address — from the country a partner expects, say. One tick on the server's card turns it on, another turns it off.

Normally a server vanishes when its traffic is tunnelled: replies to incoming connections leave through the tunnel and never arrive, so SSH drops and the site stops answering. Not here — a reply goes back the way the request came, and the server stays reachable at its own address.

A service that allows one address only

The partner allows one IP only, and you need to reach them from a laptop. In the panel you say: for this address, go through edge-1 — the server whose IP is on their list. The requests arrive from edge-1, while the rest of the laptop's traffic still goes direct.

Home NAS, cameras, Home Assistant

No public IP, no dynamic DNS, no port forwarding on the router. Put a node on the home server and reach it from anywhere — your phone, your laptop, your work machine — exactly as you would from the sofa.

Server to server

The machines sit with different providers, in different data centres, with no network in common: the runner writes to the database, the backup job ships to storage, the app talks to the next data centre.

That used to mean a port open to the world and an IP allowlist on a firewall that broke on every migration. Now the machines see each other directly and the access is one rule in the panel.

Not everything goes into the tunnel

A normal VPN swallows all your traffic. Here the laptop reaches the internet directly and fast, and only what you listed goes through the network: office subnets, specific services, individual addresses.

The ISP changed — nothing broke

You moved to another data centre, the provider handed out a new address, the machine went from the office to someone's home. On the tailnet it keeps its address and its name, so rules, scripts and bookmarks need no editing.

Instead of mailing out VPN configs

No .ovpn attachments. A person gets a single-use key and one command for their OS — a minute later they are on the network. Revoking means deleting a device, not reissuing configs for everyone else.

SSH closed to the internet for good

While the server answers on port 22, strangers' bots pick at it around the clock. With a node on it, that port can be closed on the public interface for good — you come in on the network address instead, by a rule from the panel.

There is nothing left outside to knock on, and the list of who may come in sits in one place and changes by clicking.

A way in when the usual one is shut

The provider blocked the port, the public address is under attack, somebody rewrote the firewall rules, the corporate VPN is down — and you need to get in now. A node needs no more than outbound access to stay in touch, so the machine is still reachable when nothing gets to it from outside.

The panel is not part of the connection: even while it is unavailable, the sessions already running carry on.

Hardware that takes no client

An IPMI board, cameras, a printer, a managed switch, an old NAS — nothing can be installed on them, and you still have to reach them. Put a node on any machine in that network, it advertises the subnet, you approve it, and you work with all of it on internal addresses as if you were sitting next to it.

Access is granted the same way as everywhere else: to whom, and on which port.

Model

What makes something a server and something else a device

headscale knows nothing about this — to it every machine is just a node.
The split into servers and devices is ours; it lives in the panel's database.
The difference is one thing: access can be opened to a server, never to a device.
So servers reach each other too, while devices cannot see each other at all.

Server

Access can be opened to it

  • This is what access gets opened to: a database, a hypervisor, the office router, a build machine.
  • A server is a source as well: the runner reaches the database, the backup job reaches storage, the app reaches the next data centre.
  • Servers can be grouped into roles, so you grant the role instead of every machine one by one.
  • A server can hand out its local network or work as a gateway out to the internet.
  • The agent goes on servers — it picks up the routes you set in the panel.

Device

Access is never opened to it

  • A laptop, a phone, a work computer — anything with a person sitting at it.
  • A device is given access to servers. Giving access to the device itself is not possible — that rule cannot be written.
  • Devices don't see each other: a colleague's laptop won't appear in the list, and typing its address by hand won't work either.
  • This is also where you allow exit gateways — which ones the person can switch on.

Interface

What it looks like

Four screens where nearly everything happens.

panel.example.com/nodes/12zoom
Server detail page
Server detail. Roles, advertised subnets, exit-gateway mode with the list of devices allowed to use it, and agent state.
panel.example.com/accesszoom
Access section
Access. A rule is “who → where → which port”, granted to several sources at once; the panel assembles the resulting HuJSON.
panel.example.com/routingzoom
Routing section
Routing. Directions “source → destination → via which node”, with domain resolution and automatic route updates.
panel.example.com/serverszoom
Server list
Servers and devices. Online state, addresses, roles, routes and key expiry on one screen.

Architecture

The panel runs the network without traffic passing through it

The panel reaches headscale over an internal Docker network — the management API is never exposed.
The only thing open to the world is what Tailscale clients need: joining and fetching the network map.

CONTROLBrowseradministratorReverse proxyTLS · allowlisted IPsNodeRoostpanel + Postgresheadscalecontrol serverinternal Docker network · the management API is never exposedonly keys, the network map, the policy and agent settings cross between layersuser traffic never enters hereDATAnode Aa laptop, saynode Ba server, sayWireGuard — straight between the nodesDERP steps in only when a direct connection fails zoom

Install

Docker Compose next to headscale

The panel, Postgres and headscale itself come up from one file. A reverse proxy terminates TLS and keeps the panel behind an address allowlist.

# 1. get the repository
git clone https://github.com/mihsergeev/NodeRoost.git
cd NodeRoost

# 2. domains, secrets, allowlisted addresses
cp .env.example .env && $EDITOR .env

# 3. bring it up
docker compose up -d

# 4. enrol a node — the panel hands you this command
curl -fsSL https://control.example.com/agent/<token>/setup | sh

What you need

headscale
0.29+, policy mode database
host
Docker with Compose, 1 vCPU / 1 GB
domains
one for the panel, one for the control server
proxy
any TLS-terminating one; a caddy-docker-proxy example is included
images
ghcr.io/mihsergeev/noderoost-backend and -frontend

Security

Safe by default

Everything below works from the first start — nothing to switch on or configure separately.

  • Two-factor sign-in and an audit log. A password plus TOTP, brute-force limits, and a record of who changed what. With a default admin password the panel will not start at all.
  • Personal devices are protected on their own. Laptops and phones reach servers but never each other. That lives in how the policy is built, not in a checkbox somebody can forget to tick.
  • Access is revoked in one move. Delete a device and it vanishes from every rule at once — no reissued configs, no lists to edit.
  • A bad policy will not lock you out. The panel validates the HuJSON it built, applies it, and restores the last working version if headscale refuses it.
  • A hardened perimeter ships with it. The repository carries a ready reverse-proxy example: TLS, an address allowlist, a dedicated network for the panel and headscale's management API closed from outside. It deploys as is.
  • Outbound requests are kept in check. The address the panel sends alerts to is validated on save: nothing goes to the internal network or over plain HTTP.
  • The build can be verified. Dependencies pinned with hashes, base images pinned by digest, tests and a dependency audit on every commit. The code is BSD-3 licensed — everything claimed here can be read.
panel.example.comzoom
The NodeRoost sign-in screen
Signing in: a password, a second factor, nothing else.