Skip to content

NetBird vs Headscale vs Cloudflare — Remote Access Comparison

Date: 2026-07-05 Scope: Access layer only, evaluated against revised requirements. Follows up on 18-ARCHITECTURE-ALTERNATIVES-PROPOSAL.md.

Revised Requirements

  1. ~100 remote RPis; MQTT (EMQX, mTLS) stays unchanged — independent of this decision.
  2. Admin plane: push-based Ansible when the operator decides, plus on-demand SSH / Node-RED editor access. (Pull-based convergence explicitly rejected.)
  3. User plane: end users access the per-device Node-RED dashboard in a browser to fine-tune parameters (dashboard widgets may write values), but must not be able to modify flows or reach the editor/admin API.

The Deciding Constraint: Clientless Browser Access

End users open a dashboard URL in a browser. This splits the candidates immediately:

Cloudflare Access + Tunnel NetBird Headscale
User opens URL, logs in with Google, sees dashboard ✅ Native (clientless ZTNA) ❌ Requires NetBird client installed + enrolled on every user machine ❌ Requires Tailscale client installed + enrolled
Path-level restriction (dashboard yes, editor no) ✅ Tunnel ingress path rules + Access policies per app ⚠️ Network-level only (IP:port); path control needs a reverse proxy on each device ⚠️ Same — ACLs stop at IP:port

Mesh VPNs are network access tools. Once a user's machine is on the mesh and allowed to reach device:1880, nothing in NetBird/Headscale distinguishes /dashboard from /admin — that enforcement would have to live on each device (nginx or Node-RED itself). Cloudflare enforces it before traffic ever reaches the device.

Conclusion for the user plane: Cloudflare is effectively the only fit unless every end user installs and maintains a VPN client. NetBird/Headscale compete only for the admin plane.

Three-Way Comparison (admin plane)

Criterion Cloudflare Tunnel + Access NetBird (self-hosted) Headscale
Architecture Outbound cloudflared per device → CF edge; SSH via cloudflared access ssh ProxyCommand WireGuard mesh; self-hosted control plane (single unified binary since v0.65); direct peer-to-peer where NAT allows Self-hosted reimplementation of Tailscale control server; official Tailscale clients; embedded DERP relay
Ansible push Works today. Every SSH connection proxied through CF edge → added latency per connection; 100-host play noticeably slower than direct Devices get stable mesh IPs/DNS names; native WireGuard SSH ≈ LAN-like; fastest plays Same as NetBird (WireGuard direct); relay fallback through single DERP
On-demand connect cloudflared access ssh or browser-rendered SSH terminal ssh device.netbird.local once admin client running ssh device.tailnet via Tailscale client
Access control Access policies per hostname/path; IdP-integrated (Google/GitHub already configured) Group-based ACL policies in web UI; posture checks (OS, client version, geo, process) ACL policy file; no dynamic ACLs; OIDC groups not usable in ACLs
Self-hosted infra to run None 1 server (unified binary + Coturn); built-in IdP since v0.62 — no external IdP needed 1 binary + SQLite; embedded DERP = single point of failure for relayed peers
Feature gating Free ≤50 users, 1000 tunnels, 500 Access apps; >50 users → $7/user/mo for all Self-hosted: full feature set, no seat caps, free Free, open source; community project — Tailscale Inc. does not support it; upgrades must track Tailscale client compatibility
Maturity / ops risk Managed, SLA on paid; vendor dependence All-OSS product company; more moving parts historically, much simpler since 2026 releases Solid at ~100 nodes, but SPOF, upgrade coupling, HA requires extra work (e.g. k8s operator)
Device footprint cloudflared daemon (already deployed) netbird agent (WireGuard, kernel module — light) tailscaled (light)
Terraform support Full (already in repo) Provider exists (netbirdio/netbird) Limited; ACL file + CLI/API

Cost at 100 devices, <50 users

Option Monthly New infra to operate
Cloudflare only (status quo, tightened) $0 none
Cloudflare (users) + NetBird (admin) $0 + VPS you already have (can co-locate on EMQX host) NetBird server
Cloudflare (users) + Headscale (admin) $0 + same Headscale server (SPOF unless HA work)
Cloudflare (users) + Tailscale SaaS (admin) ~$50–100 (tagged devices >50 @ $1/mo) none

Node-RED: users fine-tune parameters, cannot touch flows

Independent of the access-layer choice — do both:

  1. Path enforcement at the edge (Cloudflare): dashboard hostname's tunnel ingress routes only the dashboard path (/dashboard for Dashboard 2.0, /ui for classic — include the websocket sub-paths, e.g. /dashboard/socket.io/*), catch-all → 404. Editor hostname (admin-only Access policy) routes /. This replaces the current Host-header middleware in settings.js — enforcement moves out of Node-RED into infrastructure users cannot reach around.
  2. adminAuth as backstop on every device: admins get "*" permission; end users get no editor accounts at all. Dashboard widgets run on httpNodeRoot, completely separate from the admin API — users can write parameters through widgets without any editor permission. Even if someone reaches port 1880 directly, the editor demands a login.

Parameter writes via dashboard widgets flow through the flows themselves (inject → change → persist), never through the admin API, so "users adjust setpoints but can't edit flows" is exactly the boundary these two layers enforce.

Recommendation

  1. User plane: stay on Cloudflare. Clientless browser access + IdP login + path-level policy is the requirement, and only Cloudflare provides it without installing clients on user machines. Free at current scale (1000-tunnel limit is 10× headroom).
  2. Tighten Node-RED now (both steps above) — this is the real security gap and costs nothing.
  3. Admin plane: keep Cloudflare tunnels for now. Push Ansible over cloudflared access ssh already works; the pain is only speed/latency. If 100-host plays become too slow or you want direct low-latency SSH:
  4. Add NetBird self-hosted on the EMQX VPS for the admin plane only. Preferred over Headscale: full web UI + group ACLs + posture checks, all-OSS with vendor backing, built-in IdP, no DERP single point of failure coupled to a community project's release cycle.
  5. Pilot on 2–3 devices; Ansible inventory simply gains mesh IPs as ansible_host. Cloudflare SSH hostnames stay as break-glass.
  6. Headscale only makes sense if you specifically want official Tailscale clients and accept static-file ACLs, no OIDC groups in ACLs, and single-DERP SPOF. For this fleet NetBird dominates it.

Sources