Private plane: self-hosted Headscale mesh for operator access + fleet (MagicDNS), de-coupling admin from Cloudflare #96

Open
opened 2026-07-08 16:46:27 +00:00 by plat · 0 comments
Owner

Two-plane edge: public serving is open ports + ACME; private is a self-hosted mesh (Headscale)

Deciding the domain data-plane surfaced a clean split the platform should adopt. Public serving of client apps + their own domains must go through a public entrypoint (VPS 80/443 + cert-manager HTTP-01) — there's no VPN escape from that (a client's end-users aren't on any mesh). That work is tracked separately (BYO-domains). This issue is the private plane.

What Headscale is (and isn't) for here

Headscale = self-hosted, open-source Tailscale control server (WireGuard mesh + MagicDNS). It is the right tool for private connectivity, NOT public serving:

  • NOT a substitute for the public entrypoint — Tailscale Funnel only serves *.ts.net with a Tailscale cert (same custom-domain wall as the Cloudflare tunnel), and Headscale has no production Funnel. MagicDNS resolves only inside the tailnet.

Why adopt it (open-standard, de-couples from Cloudflare)

  1. Operator/admin access without public exposure. Reach Forgejo admin, the K8s API (kubectl), and dashboards over the tailnet instead of exposing them publicly or tunnelling ssh + port-forward. Today the granddaughter is administered via ssh root@droplet + kubectl port-forward — a mesh replaces that with forgejo.<tailnet> / direct apiserver access, private by default.
  2. Fleet / horizontal scale (relates to #87 "kube agent add"). New compute nodes and additional client platforms join the mesh to talk privately (control-plane ↔ workers, cross-platform), no public interconnect.
  3. MagicDNS gives stable internal names for platform services across the fleet.
  4. Standards, not adapters. WireGuard + self-hosted control plane keeps the private plane vendor-neutral, matching the same principle that moves public serving off the Cloudflare tunnel onto open ports + ACME. Cloudflare becomes an optional edge, never load-bearing.

Scope (design first)

  • Deploy Headscale (self-hosted control server) + join the platform node(s); expose Forgejo/apiserver/dashboards over the tailnet only; keep them off the public edge.
  • MagicDNS for internal service names; ACL model (who reaches what).
  • Fleet story: how a new node (#87) enrolls; cross-platform mesh for multi-client.
  • Explicitly OUT of scope: public app serving / BYO client domains (that's the public plane — open ports + HTTP-01).

Acceptance

An operator administers a platform (Forgejo + kubectl + a dashboard) entirely over the Headscale tailnet with those surfaces NOT publicly reachable; a second node joins the mesh and reaches internal services by MagicDNS; documented.

Filed from the domain-data-plane design session, 2026-07-08. Design-only until the BYO-domain public-plane work lands.

## Two-plane edge: public serving is open ports + ACME; **private** is a self-hosted mesh (Headscale) Deciding the domain data-plane surfaced a clean split the platform should adopt. **Public serving** of client apps + their own domains must go through a public entrypoint (VPS 80/443 + cert-manager HTTP-01) — there's no VPN escape from that (a client's end-users aren't on any mesh). That work is tracked separately (BYO-domains). This issue is the **private plane**. ### What Headscale is (and isn't) for here Headscale = self-hosted, open-source Tailscale control server (WireGuard mesh + MagicDNS). It is the right tool for **private connectivity**, NOT public serving: - **NOT** a substitute for the public entrypoint — Tailscale Funnel only serves `*.ts.net` with a Tailscale cert (same custom-domain wall as the Cloudflare tunnel), and Headscale has no production Funnel. MagicDNS resolves only inside the tailnet. ### Why adopt it (open-standard, de-couples from Cloudflare) 1. **Operator/admin access without public exposure.** Reach Forgejo admin, the K8s API (kubectl), and dashboards over the tailnet instead of exposing them publicly or tunnelling `ssh + port-forward`. Today the granddaughter is administered via `ssh root@droplet` + `kubectl port-forward` — a mesh replaces that with `forgejo.<tailnet>` / direct apiserver access, private by default. 2. **Fleet / horizontal scale (relates to #87 "kube agent add").** New compute nodes and additional client platforms join the mesh to talk privately (control-plane ↔ workers, cross-platform), no public interconnect. 3. **MagicDNS** gives stable internal names for platform services across the fleet. 4. **Standards, not adapters.** WireGuard + self-hosted control plane keeps the private plane vendor-neutral, matching the same principle that moves *public* serving off the Cloudflare tunnel onto open ports + ACME. Cloudflare becomes an optional edge, never load-bearing. ### Scope (design first) - Deploy Headscale (self-hosted control server) + join the platform node(s); expose Forgejo/apiserver/dashboards over the tailnet only; keep them off the public edge. - MagicDNS for internal service names; ACL model (who reaches what). - Fleet story: how a new node (#87) enrolls; cross-platform mesh for multi-client. - Explicitly OUT of scope: public app serving / BYO client domains (that's the public plane — open ports + HTTP-01). ### Acceptance An operator administers a platform (Forgejo + kubectl + a dashboard) entirely over the Headscale tailnet with those surfaces NOT publicly reachable; a second node joins the mesh and reaches internal services by MagicDNS; documented. *Filed from the domain-data-plane design session, 2026-07-08. Design-only until the BYO-domain public-plane work lands.*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-platform/mitosis#96
No description provided.