//------------------------------------------------------------------- //-------------------------------------------------------------------
how to fix Headscale errors

Headscale Troubleshooting Guide: Fix DERP, DNS, Route Advertising, and Node Registration Problems

Getting Headscale up and running is one thing; keeping it working in production is a completely different story. This guide is about learning how to fix Headscale errors that show up after the initial setup, including:

  • Nodes that refuse to register.
  • Pre-auth keys that have gone stale.
  • DERP fallback that never goes away.
  • DNS names that do not resolve.
  • Subnet routes that do not appear.
  • ACL rules that block everything silently.

If you have not installed Headscale yet, start with this Headscale installation guide first, then come back here when things break to fix Headscale errors.

How to Read Headscale Logs

Before going into fixes, you need to understand what the server is actually telling you. One of the fastest ways to fix Headscale errors is to simply read the logs before changing any config.

Most people skip this step and spend hours guessing; don’t do it.

Check the Headscale service logs

Use the commands below to check the service logs:

# Systemd-managed installs
sudo journalctl -u headscale -f

# View last 100 lines
sudo journalctl -u headscale -n 100 --no-pager

# Docker installs
docker logs -f headscale

Enable debug logging

When the regular logs do not give you enough detail, switch to debug mode in your config.yaml:

log:
  level: debug

After saving, restart the service:

sudo systemctl restart headscale

Check client-side logs and status

On the Tailscale client side, these commands will help you so much:

# Check local network conditions
tailscale netcheck

# Full client status in JSON
tailscale status --json

# Check DNS resolution status
tailscale dns status --all

# View client daemon logs
tailscale debug daemon-logs

# Inspect current netmap
tailscale debug netmap

Run these checks before you do anything else. The answer is usually hiding in one of them.

Headscale Node Registration Problems

Node registration problems are the most common issue after a fresh install. The node connects to the server, but never shows up in headscale nodes list. Knowing how to fix Headscale errors with node registration saves a lot of frustration.

Problem 1: Expired or invalid pre-auth key

This is the most common cause of registration failures. The symptom is usually invalid preauth key or auth key expired in the server logs.

Check your current keys:

# Native install
headscale preauthkeys list --user <USERNAME>

# Docker install
docker exec -it headscale headscale preauthkeys list --user <USERNAME>

Create a fresh key:

# Reusable key valid for 7 days
headscale preauthkeys create --user myuser --reusable --expiration 168h

Use --reusable if you need to register multiple devices with the same key. For production environments, keep expiration under 72 hours and rotate keys regularly.

Problem 2: Wrong server URL on the client

If the client cannot even reach the registration page, check your server URL in config.yaml and make sure the client uses the same URL:

# Test if the server endpoint is reachable
curl -v https://headscale.example.com/health

# Connect the client to the correct URL
tailscale up --login-server https://headscale.example.com

The URL must include https:// and the correct port. Missing https:// or a wrong port causes a silent timeout.

Problem 3: Firewall blocking registration

If the server logs show zero incoming requests when you try to register a node, the firewall is blocking the traffic. Open these ports:

PortProtocolPurpose
443TCPHTTPS and main Headscale endpoint
80TCPHTTP redirect and ACME cert renewal
3478UDPSTUN for NAT traversal
41641UDPWireGuard direct connections
# UFW example
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
sudo ufw allow 41641/udp

Problem 4: Clock sync issues

JWT-based auth tokens are time-sensitive. If your VPS clock differs by even a few minutes from the client’s, registration will fail with token not yet valid even though the key looks fine:

# Check server time
timedatectl

# Check client time sync
tailscale debug timesync

# Force NTP sync
sudo apt install -y ntp
sudo systemctl enable --now ntp

Problem 5: Key prefix mismatch (older versions)

In older Headscale builds, the node key needed a nodekey: prefix during manual registration. If you are on an older version and manual CLI registration fails, check your key format. The fix is to upgrade to a stable recent release.

After you fix the issue, confirm the node actually registered:

headscale nodes list

You should see the node with a Tailscale IP and a status of online.

Fix Headscale Errors with DERP Relay Issues

DERP stands for Designated Encrypted Relay for Packets. It is used when two nodes cannot establish a direct peer-to-peer connection. Staying on DERP permanently is not broken, but it is slower than direct connections. Many people try to fix Headscale errors here without realizing the root cause is often a NAT or firewall problem.

Enable the built-in DERP server

The embedded DERP server is disabled by default. If you want to use your own DERP relay instead of Tailscale’s public servers, enable it in config.yaml:

derp:
  server:
    enabled: true
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    ipv4: YOUR_SERVER_PUBLIC_IP
    ipv6: ""  # optional
    stun_listen_addr: "0.0.0.0:3478"
  urls: []

Set urls: [] to stop using Tailscale’s public DERP servers. Note that this makes your own DERP a single point of failure, so only do this if you know the server will be reliable.

Test DERP connectivity

View the current DERP map that the client sees and test the connection to the embedded DERP with the commands below:

tailscale debug derp-map
tailscale debug derp headscale

If the DERP map shows your server but the test fails, check the following:

  • Port 443/tcp must be open (DERP uses TLS)
  • Port 3478/udp must be open (STUN)
  • server_url in config.yaml must use https://

Traffic always goes through DERP (no direct connection)

This is a very common complaint, especially on VPS servers. If your VPS is behind ISP-level NAT, meaning the server has an internal private IP, but the ISP maps a public IP to it, direct peer-to-peer connections may fail. The nodes fall back to DERP relay permanently.

Check with:

tailscale ping <NODE_IP>

If the output says pong from … via DERP instead of via … (direct), you are relaying.

To fix it:

1. Set the correct public IPv4 in the DERP config. Headscale needs to know the real external IP:

derp:
  server:
    ipv4: 203.0.113.1  # Your actual public IP

2. Make sure port 41641/udp is open for WireGuard; direct connections use this port.

3. If the VPS is behind double-NAT with no control over the outer NAT, direct P2P is often impossible, and DERP relay is the expected behavior.

Timezone causing DERP failure

An incorrect system timezone on the server can break DERP authentication. If your DERP was working and suddenly stopped after a server restart, check the timezone:

timedatectl
# If wrong, fix it:
sudo timedatectl set-timezone UTC

DNS and MagicDNS Not Working in Headscale

DNS problems in Headscale usually appear as nodes that can ping each other by IP but not by hostname. When you need to fix Headscale errors related to DNS, the issue is always either MagicDNS being disabled in the config or the client not accepting DNS from the server.

Enable MagicDNS in config.yaml

If DNS resolution is not working at all, check that MagicDNS is properly enabled:

dns:
  magic_dns: true
  base_domain: yourdomain.example.com
  nameservers:
    - 1.1.1.1
    - 8.8.8.8

Restart Headscale after any DNS config change:

sudo systemctl restart headscale

Client is not using the tailnet DNS

The client must be told to accept DNS settings from the control server:

# Make sure the client accepts DNS
tailscale up --login-server https://headscale.example.com --accept-dns=true

# Or update an existing connection
tailscale set --accept-dns=true

If you set --accept-dns=false earlier for any reason, DNS resolution through the tailnet will not work.

MagicDNS overriding local DNS on macOS

This is a known problem. When MagicDNS is active on macOS, it can hijack all DNS queries, including local network names. If local resolution breaks after joining the tailnet:

# Disable DNS acceptance only on this node
tailscale set --accept-dns=false

On macOS, you can also go to Preferences > Uncheck “Use Tailscale DNS settings”.

DNS not resolving for split DNS (restricted nameservers)

If you have restricted nameservers configured but --accept-routes=false on the client, those restricted nameservers will not be used. Either enable route acceptance or move the DNS nameservers to the main nameservers list.

Verify DNS is working

Check what DNS settings the client is using:

tailscale dns status --all

Test resolution of a tailnet hostname:

dig nodeA.yourdomain.example.com

Test an extra_records entry:

dig grafana.yourdomain.example.com

If the hostname resolves correctly with dig but not in the browser, the application may be bypassing the system DNS.

Headscale Subnet Routes Not Appearing

Subnet routes let a Tailscale node act as a router for a local network, making devices without Tailscale reachable from the tailnet. If you need to fix Headscale errors with routing, the most common issue is forgetting to approve the route on the server side after advertising it.

Step 1: Advertise the route from the node

On the device that will act as the subnet router:

# New connection
sudo tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.1.0/24

# Already registered node
sudo tailscale set --advertise-routes=192.168.1.0/24

Step 2: Check if the route appeared on the server

headscale nodes list-routes

You will see output like this:

ID | Hostname  | Approved        | Available       | Serving (Primary)
1  | myrouter  |                 | 192.168.1.0/24  |

The Approved column is empty; this is the problem. Routes must be manually approved unless autoApprovers are configured.

Step 3: Approve the route on Headscale

headscale nodes approve-routes --identifier 1 --routes 192.168.1.0/24

After approval:

ID | Hostname  | Approved        | Available       | Serving (Primary)
1  | myrouter  | 192.168.1.0/24  | 192.168.1.0/24  | 192.168.1.0/24

Step 4: Enable route acceptance on the client

Clients will not use advertised routes unless they are told to accept them:

sudo tailscale set --accept-routes

Step 5: Enable IP forwarding on the router node

Without this, the subnet router will receive traffic but drop it instead of forwarding it:

# Temporary
sudo sysctl -w net.ipv4.ip_forward=1

# Permanent
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

Route showing as “Pending approval” or not serving

This is a known bug in some Headscale versions where an already-approved route stops being propagated to the client. If the route shows Advertised: true but Enabled: false on the server:

# Re-advertise the route to force a refresh
sudo tailscale set --advertise-routes=""
sudo tailscale set --advertise-routes=192.168.1.0/24

In some cases, you may need to restart tailscaled on the client as well.

Use autoApprovers to skip manual approval

For production setups, autoApprovers save you from manually approving every route. Add this to your policy file:

{
  "tagOwners": {
    "tag:router": ["alice@"]
  },
  "autoApprovers": {
    "routes": {
      "192.168.1.0/24": ["tag:router"]
    }
  }
}

Then register the node with the correct tag:

sudo tailscale up --login-server https://headscale.example.com \
  --advertise-tags tag:router \
  --advertise-routes 192.168.1.0/24

ACL Policy Mistakes in Headscale

ACL mistakes are tricky because Headscale will often start up fine but silently block all traffic. When you need to fix Headscale errors caused by policy files, the first thing to check is whether the acls array is present but empty, which blocks everything by default.

The silent default: no ACL file = allow all

If you have not configured any ACL policy, all nodes can talk to each other freely. The moment you add a policy file with an acls array, anything not explicitly allowed is blocked. This surprises a lot of people.

A safe starting point is an explicit allow-all rule:

{
  "acls": [
    {
      "action": "accept",
      "src": ["*"],
      "dst": ["*:*"]
    }
  ]
}

Point Headscale to your policy file

In config.yaml, the correct way to set the policy path depends on your version:

# Newer versions
policy:
  mode: file
  path: "/etc/headscale/acls.json"

If the newer format does not work after a version upgrade, try the legacy key:

acl_policy_path: "/etc/headscale/acls.json"

Reload after any ACL change:

sudo systemctl reload headscale
# or send SIGHUP
sudo kill -HUP $(pidof headscale)

Find syntax errors in the ACL file

If Headscale fails to start or routes are not being applied, run this to get a detailed error:

sudo /usr/bin/headscale serve /etc/headscale/config.yaml

Look for the line number in the output. ACL files use HuJSON format, which allows comments (//) but is strict about commas and brackets.

Test after applying ACL rules

After applying a rule, test from both directions:

# From the source node
tailscale ping <destination-tailscale-ip>

# Check what the client sees in its netmap
tailscale debug netmap

If tailscale ping works but an application connection does not, the problem is in the port rules inside the ACL, make sure the destination port is included in "dst".

ACL not working after Headscale upgrade

Some version upgrades change the expected policy format. If your old ACL file stopped working after an upgrade, check the Headscale release notes and compare your file structure against the new example files.

Need a more reliable control plane for remote access? Deploy Headscale on a PerLod Linux VPS and get a dedicated server with consistent uptime for your tailnet control plane.

Conclusion

Most Headscale issues come from a few common problems, including login errors, network blocks, DERP setup mistakes, DNS issues, unapproved routes, or ACL file errors. The best way to fix them is to check the server logs, check the client status, and see if the problem is on the VPS or the device.

Keep your keys fresh, your ports open, and a backup of your ACL file before changes.

We hope you enjoy this guide to fix Headscale errors. For advanced network setups where devices are behind CGNAT, you can check out the MikroTik CGNAT remote access guide for complementary routing strategies.

For more detailed information to fix Headscale errors, you can check the Tailscale Official Guide.

FAQs

How do I reload the ACL without restarting Headscale?

Run sudo systemctl reload headscale or sudo kill -HUP $(pidof headscale). Headscale will log whether the ACL loaded successfully.

Why does traffic always go through DERP instead of direct?

Your VPS is probably behind ISP NAT, or the WireGuard port 41641/udp is blocked. Set the correct public IP in your DERP config and open the necessary ports.

Why is my node showing as offline even after registration?

It is usually a firewall issue. Make sure ports 443/tcp and 3478/udp are open on the VPS. Run tailscale netcheck on the client to see what is blocked.

Post Your Comment

PerLod delivers high-performance hosting with real-time support and unmatched reliability.

Contact us

Payment methods

payment gateway
Perlod Logo
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.