Skip to main content
  1. Blog/

Secure Homelab Access with Headscale and Tailscale

·1024 words·5 mins
Table of Contents

Introduction
#

Connect to your homelab from anywhere without opening ports or exposing services to the internet.

Managing remote access to your homelab can be challenging. Traditional VPN solutions often require complex configurations, port forwarding, and expose your network to potential security risks. Tailscale offers an elegant solution with its mesh networking approach, but relies on their coordination servers. Enter Headscale - a self-hosted, open-source implementation of the Tailscale coordination server that gives you complete control over your network.

In this guide, we’ll set up Headscale on a VPS and connect both your homelab and laptop for secure, seamless access from anywhere.


Why Headscale + Tailscale?
#

The Problem with Traditional Approaches
#

  • Port forwarding exposes services directly to the internet
  • Traditional VPNs require complex routing, a public IP address and DNS might be challenging
  • SSH tunnels are manual and don’t scale well
  • Cloud VPN services create vendor lock-in and privacy concerns

The Headscale Solution
#

  • Zero-trust networking: Each device authenticates independently
  • NAT traversal: Works behind firewalls and routers without configuration
  • Automatic routing: Devices find the most efficient path to communicate
  • Self-hosted control: You own the coordination server and all metadata
  • Easy device management: Simple commands to add/remove devices

Headscale is a self-hosted, open-source implementation of the Tailscale coordination server that gives you complete control over your network. Tailscale works by creating a mesh network of devices that can communicate with each other. That means that you only need to have one server where every client connects to. Then, if possible, the clients try to build a direct connection to each other.

Client traffic is always end-to-end encrypted, but you can even restrict through which servers the clients are relaying the packets by using your own DERP servers. The DERP protocol is used to establish a secure connection between Tailscale nodes. It is explained in more detail here.


Architecture Overview
#

architecture-beta
    group tailscale(internet)[Internet]
    group homelab(server)[homelab]
    group wifi(internet)[public wifi]

    service laptop(disk)[Laptop] in wifi
    service server1(database)[Homelab Server] in homelab
    service server2(database)[Homelab Server] in homelab
    service headscale(server)[headscale] in tailscale
    service nginx(server)[nginx] in homelab
    service internet(cloud)[internet] in tailscale
    service labnet(cloud)[Labnet] in homelab

    nginx:L -- R:labnet
    server1:B -- T:labnet
    server2:T -- B:labnet

    nginx:R -- L:internet
    laptop:T -- B:internet
    headscale:B <-- T:internet

Only the headscale server is exposed to the internet. And after the initial connection to the headscale server, the clients try to connect directly using the DERP protocol. The lab servers are only reachable through the nginx, which is also a tailscale client.

Setup
#

install
#

Follow the installation instructions on the Headscale website and install it on your VPS.

For me the current version was 0.26.1 and I use an amd64 bit architecture. You can find your architecture by running uname -m, lscpu or arch.

HEADSCALE_VERSION="0.26.1"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb  "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install ./headscale.deb

configure
#

headscale
#

I have setup headscale.kohnkenet.de to point to the IP of my VPS publicly. Then configure the config.yaml file according to your need. In my case I want to server it on HTTPS:

sudo nano /etc/headscale/config.yaml

I have changed the following settings:

server_url: https://headscale.kohnkenet.de:443
listen_addr: 0.0.0.0:443
acme_email: "cert@****.de"
tls_letsencrypt_hostname: "headscale.kohnkenet.de"

ufw
#

Your UFW should already be setup on the VPS. I will allow the following ports to

sudo ufw status
sudo ufw allow 3478/udp
sudo ufw allow https
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
3478/udp                   ALLOW       Anywhere                  
443                        ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)             
3478/udp (v6)              ALLOW       Anywhere (v6)             
443 (v6)                   ALLOW       Anywhere (v6)              

verify
#

use the following commands to verify that everything is working as expected:

sudo netstat -lnpt
sudo ss -tulnp | grep headscale
sudo headscale user list
sudo headscale node list

It should look something like this:

ssh@vps1:~$ sudo netstat -lnpt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.54:53           0.0.0.0:*               LISTEN      3795/systemd-resolv 
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN      18742/headscale     
tcp        0      0 127.0.0.1:50443         0.0.0.0:*               LISTEN      18742/headscale     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1/systemd           
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      3795/systemd-resolv 
tcp6       0      0 :::22                   :::*                    LISTEN      1/systemd           
tcp6       0      0 :::80                   :::*                    LISTEN      18742/headscale     
tcp6       0      0 :::443                  :::*                    LISTEN      18742/headscale     

ssh@vps1:~$ sudo ss -tulnp | grep headscale
udp   UNCONN 0      0                     *:3478             *:*    users:(("headscale",pid=18742,fd=10))                  
tcp   LISTEN 0      128           127.0.0.1:9090       0.0.0.0:*    users:(("headscale",pid=18742,fd=17))                  
tcp   LISTEN 0      128           127.0.0.1:50443      0.0.0.0:*    users:(("headscale",pid=18742,fd=15))                  
tcp   LISTEN 0      128                   *:80               *:*    users:(("headscale",pid=18742,fd=14))                  
tcp   LISTEN 0      128                   *:443              *:*    users:(("headscale",pid=18742,fd=16))       

connect
#

To connect to your homelab, you can use the Tailscale client on your laptop or mobile device. Follow the instructions on the Tailscale website to install the client and then run the following command to connect:

sudo tailscale up --login-server https://headscale.kohnkenet.de

This will give you a command to copy and enter in the hadscale server, something like this:

headscale nodes register --user nginx --key YYa-VKuiFPpZqdsafasdgw

you can verify the connection on the server like this:


ssh@vps1:~$ sudo headscale user list
ID | Name | Username | Email | Created            
1  |      | bjarne   |       | 2025-09-28 18:24:23
3  |      | nginx    |       | 2025-09-28 18:24:24

ssh@vps1:~$ sudo headscale node list
ID | Hostname | Name  | MachineKey | NodeKey | User  | IP addresses                  | Ephemeral | Last seen           | Expiration | Connected | Expired
1  | nginx    | nginx | [jsUnm]    | [gfdAS] | nginx | 100.64.0.2, fd7a:115c:a1e0::2 | false     | 2025-09-28 19:11:07 | N/A        | online    | no   

create extra records
#

In my case I want to route HTTP/S traffic through my lab nginx server. As you can see in the output above, the server got the VPN IP 100.64.0.2. In the homelab I would create a DNS record for the hostname nginx.kohnkenet.de and point it to the IP 100.64.0.2. In Tailscale we create an extra_record in the headscale config.yaml.

Create one entry for every website where the nginx is the reverse proxy. In my example:

  • nginx.kohnkenet.de
  • git.kohnkenet.de

  # Extra DNS records
  # so far only A and AAAA records are supported (on the tailscale side)
  # See: docs/ref/dns.md
  extra_records:
        - name: "nginx.kohnkenet.de"
          type: "A"
          value: "100.64.0.2"
        - name: "nginx.kohnkenet.de"
          type: "AAAA"
          value: "fd7a:115c:a1e0::2"
        - name: "git.kohnkenet.de"
          type: "A"
          value: "100.64.0.2"
        - name: "git.kohnkenet.de"
          type: "AAAA"
          value: "fd7a:115c:a1e0::2"
Bjarne
Author
Bjarne
I’m a network engineer from Germany and like to tinker with my homelab