Reinaldy Rafli

Setup Headscale for Private Use

Mar 31, 2025 • 858 words

The word “VPN” that most people usually know is something that can allow them to be as if they’re accessing the internet from other countries or region, it hides their own device, make everything looks more secure. But, things like Headscale, Zerotier, Tailscale, and even Firezone declares that they are VPN softwares, yet they don’t seem to behave, or at least have the capability like the VPN that we used to know.

Why is it not the VPN I usually know?

Because that’s not what VPN actually is. VPN stands for Virtual Private Network, this is the term you would use if you’re interconnecting multiple networks that lives on a separate places, as if it is a single private network. For newbies, a “private network” is as simple as something that’s can’t be accessed publicly like the web. It’s your home Wifi network that have the IP of 192.168.1.*, the one you usually remember because you had to change the Wifi router configuration once in a while. The VPN that you know is actually called a “network proxy”. It obviously, proxies your current network (the one you’re using at home, office, mobile, whatever), it jumps the network through the network proxy server (usually on a different country/region), then from there it jumps to your website destination.

Installing Headscale

You’ll need a Virtual Machine that has a public IP first. You can buy a cheap and small one somewhere. This comand below assumes you have a Debian-based distro (Debian, Ubuntu, etc) and you have a basic understanding of Linux comands.

# This is hardlinked to 0.22.3 version of Headscale. The current version may differ.
curl -LO https://github.com/juanfont/headscale/releases/download/v0.22.3/headscale_0.22.3_linux_amd64.deb

sudo dpkg -i headscale_0.22.3_linux_amd64.deb

sudo mkdir -p /{etc,var/lib}/headscale

sudo useradd \
    --create-home \
    --home-dir /var/lib/headscale/ \
    --system \
    --user-group \
    --shell /usr/sbin/nologin \
    headscale

sudo touch /var/lib/headscale/db.sqlite

curl -LO https://raw.githubusercontent.com/juanfont/headscale/v0.22.3/config-example.yaml

sudo mv config-example.yaml /etc/headscale/config.yaml
sudo chown -R headscale:headscale /etc/headscale
sudo chown -R headscale:headscale /var/lib/headscale

sudo bash -c 'cat <<EOF > /etc/systemd/system/headscale.service
[Unit]
Description=headscale controller
After=syslog.target
After=network.target

[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=always
RestartSec=5

# Optional security enhancements
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run/headscale
AmbientCapabilities=CAP_NET_BIND_SERVICE
RuntimeDirectory=headscale

[Install]
WantedBy=multi-user.target
EOF'

sudo systemctl daemon-reload

Setting up a reverse proxy

Okay, we’ve got Headscale installed. Before modifying the configuration file, I’d suggest you to install a reverse proxy that can handle TLS in front of the Headscale, so please don’t install TLS certificate directly on Headscale. My recommendation is Caddy, since it’s simple and easy.

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

# See https://caddyserver.com/docs/install

Assuming you own the domain of example.com, and also assuming we’re going to install Headscale at headscale.labs.example.com, we want to go ahead and change the Caddy configuration on /etc/caddy/Caddyfile as follows:

headscale.labs.example.com {
    reverse_proxy localhost:8080
    tls your_email@example.com
}

Then we restart Caddy by sudo systemctl restart caddy. I know that there is a better way to restart it, but since this is a new config, it will be fine.

Configuring Headscale

Now we’d like to modify the /etc/headscale/config.yaml file. These are some fields that we would like to change:

server_url: http://127.0.0.1:8080
# to
server_url: https://headscale.labs.example.com # See the domain you own above

Then you can start running Headscale by sudo systemctl start headscale.

Connecting your devices

Configuring Headscale is pretty painful if you are on iOS and Windows. There are workarounds, but you’ll need to be patient about it. See these documentations for how-to:

For Linux, OpenBSD, FreeBSD and MacOS, you will need to install the official Tailscale client.

As you can see from their documentation, connecting Headscale to your device is pretty simple:

From your Headscale server:

headscale users create node1 # or any name

headscale --user node1 preauthkeys create --expiration 1h

Now you should copy the “auth key” for node1 user as it only active for 1 hour. Then, on your Tailscale client:

tailscale up --login-server https://headscale.labs.example.com --authkey [authkey]

That’s it, you’re connected. Do this multiple times with your other devices and try to ping each other.

Is it better for security?

Nothing is ever secure enough, anyway. Making your own VPN is not going to make your devices 100% private and secure, it all depends to many things. Using something like Headscale is again, just a tool. It’s a good first step that makes you really care about your own network privacy and security, and I hope you can find other things to enhance it even further.

That being said, enjoy your Headscale setup!