Migrate from Tailscale to Headscale

t-437·WorkTask·
·
·
Created1 month ago·Updated1 month ago

Description

Edit

Tailscale → Headscale Migration Plan

Current Setup

  • Beryllium (server): Runs Tailscale with --advertise-exit-node, AdGuard DNS filtering
  • Lithium (dev machine): Tailscale client
  • Tailscale Funnel: Ava web server exposed at beryllium.oryx-ide.ts.net
  • Other devices: Likely phones, tablets, family devices (need to inventory)

Key Considerations

⚠️ Tailscale Funnel

Headscale does not support Tailscale Funnel (the feature that exposes services to the public internet via *.ts.net). Options: 1. Set up a standard reverse proxy (Caddy/nginx) for Ava instead 2. Keep one device on Tailscale just for Funnel (hybrid approach) 3. Use Cloudflare Tunnel as a Funnel replacement

What Works the Same

  • Exit nodes ✅
  • MagicDNS ✅
  • ACLs ✅
  • Standard Tailscale clients ✅
  • NAT traversal (via DERP - you can use Tailscale's public DERP or self-host)

---

Phase 1: Preparation (No Downtime)

1.1 Inventory Current Devices

# On any device currently connected to Tailscale
tailscale status

Document all devices and their roles:

  • [ ] beryllium (exit node, server)
  • [ ] lithium (dev machine)
  • [ ] phones/tablets
  • [ ] family devices

1.2 Set Up DNS for Headscale

Add a DNS record for Headscale. Options:

  • hs.bensima.com → beryllium's public IP
  • Or use the existing domain

Manual step: Add A record in your DNS provider.

1.3 Decide on Funnel Replacement

For Ava's web interface (beryllium.oryx-ide.ts.net), choose one:

Option A: Caddy reverse proxy (recommended)

  • Expose via ava.bensima.com or similar
  • Automatic HTTPS via Let's Encrypt
  • No dependency on Tailscale infrastructure

Option B: Cloudflare Tunnel

  • Free, no public ports needed
  • Cloudflare handles SSL

---

Phase 2: Code Changes

2.1 Update Omni/Dev/Vpn.nix

# Omni/Dev/Vpn.nix
{config, ...}: let
  ports = import ../Cloud/Ports.nix;
in {
  services.headscale = {
    enable = true;  # ← CHANGE: enable headscale
    address = "0.0.0.0";
    port = ports.headscale;
    settings = {
      server_url = "https://hs.bensima.com";  # ← ADD: your headscale URL
      dns.base_domain = "hs.bensima.com";
      dns.magic_dns = true;
      dns.nameservers.global = [
        "100.64.0.1"  # AdGuard on the tailnet (see below)
      ];
      # Use Tailscale's public DERP servers (free, works fine)
      derp.server.enabled = false;
      derp.urls = ["https://controlplane.tailscale.com/derpmap/default"];
    };
  };

  # Headscale needs HTTPS - add nginx reverse proxy
  services.nginx = {
    enable = true;
    recommendedProxySettings = true;
    recommendedTlsSettings = true;
    virtualHosts."hs.bensima.com" = {
      enableACME = true;
      forceSSL = true;
      locations."/" = {
        proxyPass = "http://127.0.0.1:${toString ports.headscale}";
        proxyWebsockets = true;
      };
    };
  };

  security.acme = {
    acceptTerms = true;
    defaults.email = "ben@bensima.com";  # ← your email
  };

  networking.firewall.allowedTCPPorts = [80 443];

  # ... rest of existing config (tailscale, adguard) stays the same for now
};

2.2 Update Omni/Dev/Beryllium/Ava.nix (Funnel Replacement)

If using Caddy for Ava instead of Tailscale Funnel:

# Add to Beryllium.nix or create Omni/Dev/Beryllium/Caddy.nix

services.caddy = {
  enable = true;
  virtualHosts."ava.bensima.com".extraConfig = ''
    reverse_proxy localhost:8079
  '';
};

# Remove the Tailscale Funnel comment from Ava.nix since it won't apply

2.3 Create Headscale User/Namespace

After deploying, you need to create a user (Headscale calls them "users", formerly "namespaces"):

# SSH to beryllium after deploy
headscale users create ben
headscale users create family

---

Phase 3: Deploy & Migrate Devices

3.1 Deploy Headscale

# Build and deploy beryllium
bild Omni/Dev/Beryllium.nix
# Then rebuild NixOS on beryllium (however you normally do it)

3.2 Generate Auth Keys

# On beryllium
headscale preauthkeys create --user ben --reusable --expiration 24h
# Save this key for the next steps

headscale preauthkeys create --user family --reusable --expiration 24h
# Give this key to family members

3.3 Migrate Each Device

For NixOS machines (beryllium, lithium):

Update Omni/Dev/Vpn.nix Tailscale config:

services.tailscale = {
  enable = true;
  extraUpFlags = [
    "--login-server=https://hs.bensima.com"  # ← ADD this
    "--accept-dns=true"
    "--advertise-exit-node"  # only on beryllium
  ];
  authKeyFile = "/run/secrets/tailscale-authkey";  # ← ADD: or use --auth-key manually
};

For phones/other devices:

1. Open Tailscale app 2. Log out of current account 3. Use "Custom control server" option (may need to enable in settings) 4. Enter: https://hs.bensima.com 5. Use the preauth key or OIDC if you set that up

Manual registration (any device):

tailscale up --login-server=https://hs.bensima.com --authkey=YOUR_KEY

3.4 Approve Exit Node

# On beryllium, list routes
headscale routes list

# Enable the exit node route (find the route ID from above)
headscale routes enable -r <route-id>

3.5 Configure DNS (AdGuard Integration)

If you want all Headscale clients to use AdGuard for DNS:

1. AdGuard should listen on the Tailscale IP (e.g., 100.64.0.1) 2. Update Headscale config:

services.headscale.settings.dns.nameservers.global = ["100.64.0.1"];

---

Phase 4: Family Onboarding

4.1 Create Family User

headscale users create family
headscale preauthkeys create --user family --reusable --expiration 720h

4.2 Share Instructions

Send family members: 1. Install Tailscale app (same app, works with Headscale) 2. Go to Settings → Custom control server → https://hs.bensima.com 3. Use auth key: [their key] 4. To use your home internet: Enable "Exit Node" → select beryllium

4.3 ACLs (Optional)

Create Omni/Dev/headscale-acl.json to control who can access what:

{
  "acls": [
    {"action": "accept", "src": ["ben"], "dst": ["*:*"]},
    {"action": "accept", "src": ["family"], "dst": ["beryllium:*"]}
  ]
}
headscale policy set -f /path/to/headscale-acl.json

---

Migration Checklist

Pre-Migration

  • [ ] Inventory all Tailscale devices (tailscale status)
  • [ ] Set up DNS record for hs.bensima.com
  • [ ] Decide Funnel replacement (Caddy recommended)
  • [ ] Back up any Tailscale ACLs you want to preserve

Code Changes

  • [ ] Update Omni/Dev/Vpn.nix - enable Headscale, add nginx
  • [ ] Update Omni/Dev/Beryllium/Ava.nix - remove Funnel, add Caddy
  • [ ] Add nginx/ACME config for Headscale HTTPS

Deployment

  • [ ] Deploy to beryllium
  • [ ] Verify Headscale is running: curl https://hs.bensima.com/health
  • [ ] Create users: headscale users create ben
  • [ ] Generate auth keys

Device Migration

  • [ ] Migrate beryllium (re-register with own Headscale)
  • [ ] Migrate lithium
  • [ ] Migrate phones/tablets
  • [ ] Enable exit node route
  • [ ] Test exit node from a client device
  • [ ] Test DNS filtering (AdGuard)

Family Onboarding

  • [ ] Create family user
  • [ ] Generate long-lived auth key
  • [ ] Send setup instructions
  • [ ] Test their connectivity

Cleanup

  • [ ] Cancel Tailscale subscription
  • [ ] Remove old Tailscale account (optional, can keep free tier as backup)

---

Rollback Plan

If something goes wrong, you can always: 1. Set services.headscale.enable = false in Vpn.nix 2. Re-run tailscale up without --login-server flag (uses Tailscale.com) 3. All devices can reconnect to Tailscale.com instantly

---

Files to Modify

| File | Changes | |------|---------| | Omni/Dev/Vpn.nix | Enable Headscale, add nginx reverse proxy | | Omni/Dev/Beryllium/Ava.nix | Remove Funnel comment, document new URL | | Omni/Dev/Beryllium.nix | Possibly add Caddy module | | Omni/Cloud/Ports.nix | Already has headscale = 8844 ✓ |

New Files (Optional)

| File | Purpose | |------|---------| | Omni/Dev/headscale-acl.json | Access control policies | | Omni/Dev/Beryllium/Caddy.nix | Ava web interface reverse proxy |

Timeline (0)

No activity yet.