Skip to content

Repository files navigation

NodeRoost

A self-hosted web panel for headscale — the open-source implementation of the Tailscale coordination server.

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. The panel does not replace headscale and does not proxy traffic: it takes over the part that is otherwise done by hand.

Русская версия · Why and what for · Changelog · noderoost.ru

Servers

Access
Access — who reaches which server, on which port
Routing
Routing — who goes where, through which node
Server detail
Server detail — roles, subnets, exit gateway, agent
Sign-in
Sign-in — password and a second factor

Screens are rendered from the panel's own stylesheet with example data; the addresses are documentation ranges (RFC 5737), not a live tailnet.


Why a panel when headscale already works

The panel does not replace headscale and does not carry your traffic. It takes over the part that is otherwise done by hand: granting access becomes a who → where → on which port rule instead of an edit to policy.hujson; a subnet behind a node is a button instead of approve-routes by node id; a route is applied by an agent instead of an SSH session on every machine.

What people build with it — two office LANs seeing each other without a public address anywhere, a work machine reachable from behind someone else's NAT, a database open to a contractor on one port and nothing else, a laptop leaving for the internet through a server whose address a partner allows. Reasons, goals and the full set of scenarios → docs/why.md

What it does

Servers and devices. Nodes split into servers (things you connect to) and personal devices. Devices are isolated from one another and can never be a rule's target — enforced in the policy engine, not in the UI, because a rule can also arrive through the API.

Grants, not an ACL file. A rule is who → where → on which port. Grant it by clicking, to several nodes at once, or from inside a node's own page. The HuJSON is generated, pushed, and rolled back if headscale refuses it.

Roles. A role is a group of servers (a headscale tag). Grant access to the role; adding a server to it later needs no rule changes.

A node's page

Routing. Directions — these nodes reach that address through this node. Give a hostname and the panel resolves it, keeps the route in sync when the site moves, and refuses addresses that would hijack traffic inside the mesh.

Names inside the network. A service behind an address allowlist — or with no ports open to the world at all — is reached by the name it already has: the panel hands the nodes a name → address on the network record. Public DNS is left alone, so from outside the name leads where it always did. The address is not remembered but taken from the node, so a node reconnecting does not break the record.

Certificates for those names. Each name has a "certificate" tick: the panel signs it with its own root while the key is generated on the node itself and never leaves it — only a request to sign goes up. No domain, no DNS record, no open port and no internet: the name can be anything, nas.mesh included. The panel hands the root out to the nodes itself, and that root can only sign invented internal domains: every real one is forbidden to it, so the panel cannot impersonate someone else's site to your machines — and a new project needs no reissuing. Renewal takes care of itself.

Internet egress through gateways. Mark a server as an exit gateway and pick which devices may use it. In the Tailscale tray each user sees only the exits you allowed — the choice is theirs, the set is yours. A node's whole outbound traffic can also be forced through a gateway while the node stays reachable on its public IP.

An agent on the node. headscale can approve routes but has no channel to tell a node what to advertise, so NodeRoost ships a small POSIX-sh agent: a systemd timer pulls the desired state, applies it with tailscale set, and reports the hash of what it actually applied.

Where a server sits. A country flag next to the node name, resolved from the node's public address offline against a table in the repository: no need to send every address in your fleet to a geo service for the sake of a flag, and no internet required. The node's name plays no part in it.

Backups, monitoring, alerts. Consistent snapshots of the headscale database and panel settings with a self-test and a rehearsed restore; uptime history; Telegram or webhook alerts for a downed server or an expiring key; and an independent host watchdog for when the panel itself goes quiet.

Security

The panel runs a VPN network, so the guarantees live in code and are covered by tests:

  • devices never reach each other, and a device is never a rule's destination — not by picking it from a list, not by typing its tailnet address by hand;
  • a node cannot promote itself: its class and its tags follow only what the administrator approved, never what the node announces about itself;
  • tags are owned by an empty group, so no node can apply one to itself;
  • sign-in is JWT + optional TOTP with attempt limits and an audit log; the panel refuses to start with a weak secret or the default password;
  • releases build from a hash-verified dependency lock, with base images pinned by digest.

Deployment notes that matter — network isolation, what is exposed publicly — are in SECURITY.md. Read it before putting this on the internet.

Requirements

  • Docker with Compose
  • a reverse proxy terminating TLS (the compose overlay ships caddy-docker-proxy labels; give NodeRoost a network of its own — see SECURITY.md)
  • two DNS names: one for the panel, one public one for the control server

Quick start

Full guide: docs/install.md — one-command install, your own domain, the address allowlist, the firewall and what to do when something is wrong.

git clone https://github.com/mihsergeev/NodeRoost.git
cd NodeRoost
cp .env.example .env

Fill in .env — at minimum a random NODEROOST_JWT_SECRET (openssl rand -hex 32), a strong NODEROOST_ADMIN_PASSWORD, NODEROOST_DB_PASSWORD, your two domains and the addresses allowed to reach the panel. Then put the headscale config in place:

mkdir -p data/headscale/config
cp deploy/headscale/config.example.yaml data/headscale/config/config.yaml
$EDITOR data/headscale/config/config.yaml   # server_url, base_domain, prefixes
docker compose up -d

The backend needs a headscale API key, which can only be created once headscale is running:

docker compose exec headscale headscale apikeys create --expiration 999d
# put it in .env as NODEROOST_HEADSCALE_API_KEY, then:
docker compose up -d backend

Sign in with the admin credentials from .env and add your first node — the panel hands you a one-time key and a ready-made command for Linux, Windows, macOS or Android.

Architecture

            ┌───────── public ──────────┐      ┌──── allow-listed ────┐
 nodes ───► │ hs.example.com            │      │ panel.example.com    │
            │ headscale, node endpoints │      │ SPA + panel API      │
            │ only (/api/v1 → 404)      │      └──────────┬───────────┘
            └──────────┬────────────────┘                 │
                       │    internal docker network       │
                       └─────────► headscale API ◄────────┘

The panel reaches headscale's management API only over the internal network; on the public vhost /api/v1 and /swagger return 404, leaving the node-facing endpoints. Panel state lives in Postgres, headscale keeps its own SQLite.

Development

cd backend  && python -m venv .venv && .venv/bin/pip install -e . && pytest
cd frontend && npm install && npm run dev

The country flag next to a node is resolved from its public address offline — none of your fleet's addresses leave the host. The table (backend/app/data/geoip.csv.gz, DB-IP IP-to-Country Lite, CC BY 4.0) ships in the repository; refresh it every few months:

python ops/build-geoip.py

License

BSD-3-Clause — see LICENSE.

About

Self-hosted web panel for headscale — your own Tailscale control server with a real admin panel: access grants instead of hand-written HuJSON, device isolation, roles, subnets, per-device exit gateways, country flags by IP, node agent, backups, alerts, 2FA. FastAPI + React, Docker.

Topics

Resources

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages