The Setup
I have been a long time (long time as 1 year) Tailscale user, and I absolutely love it. I’ve tried to use Wireguard only, and other VPN solutions, but nothing matches the ease of execution and consistency that Tailscale provides. In fact, my experience has only gotten better as I’ve upgrade my home network with a pfsense box, with built in Tailscale support (via a package).
The only gripe I’ve had with Tailscale is that it’s not a self-hostable solution. I don’t really have a reason for it be self-hostable, besides me wanting to maintain the extra level of privacy, and have a little fun while doing it.
That is where Headscale comes in. Headscale provides an open-source implementation of the only not open source part of Tailscale - the control plane. And seeing this, I wanted to try it out. And I have to say, I have mixed reviews. To add and remove nodes, you need access to the command line of your headscale node. Which is fine, but at the end of the day makes things very painful when you don’t have a computer with the right SSH keys (speaking from personal experience). It is totally doable to admin headscale from the commandline of your VPS node - in fact, after this experience I might recommend doing that - but it is not as user friendly.
One of the biggest benefits of using Tailscale is the UI, and the OIDC clients associated with it. It makes adding and doing admin on nodes super convenient. I wanted to try and make a similar experience using the many UIs for Headscale, along with and OIDC client I have a lot of experience with in my homelab, Authelia.
Config
The full config for this setup can be found in this GitHub repo. I used Digital Ocean as my VPS provider, mostly because I have the most experience with it. My DNS provider is Cloudflare, again because I have the most experience with it.
Prereqs
For this kind of set up, you need a small node on a VPS. There is really no way around that. I got things working pretty well on a 1 Gb/1 vCPU node from Digital Ocean - because things are using Docker, I wouldn’t recommend going any lower on the specs than that.
There are lots of other tutorials on setting up droplets on Digital Ocean, so I won’t go into detail here. Just make sure you have the following set up:
- A droplet with either Debian 11 or Alma 9 linux. These are what I have tested with, I’m sure other distros will work.
- A static IP. This is important for networking.
- An A record on your DNS provider of choice. I’m choosing cloudflare here, but you can use whatever you like. You should have it point to your static IP, and use two domains - hs.yourdomain.com and auth.yourdomain.com.
- Have Docker installed and configured to your liking. I like using firewalld as my firewall, this tutorial is a great way to configure docker to play nice. I will upload an Ansible playbook for installing docker and configuring firewalld when I get around to it.
And now we are set to go!
Docker Compose
My headscale ui of choice is headscale-ui, and the only criteria is that it looked the nicest (I’m a shallow man).
Here is my docker-compose for the setup:
So let’s talk through this:
- This is using the standard configuration for headscale/headscale-ui found here. I’m not doing anything fancy with Traefik because Headscale is in charge of it’s own SSL certificates. So as long as you set up your DNS entry, you should be okay.
- For traefik, I’ve set up Cloudflare as my DNS resolver. Even though headscale can take care of is SSL cert, authelia cannot. So we still need to get our Let’s Encrypt certificate. Make sure to add your email and api key into the secret files for this to work, and update the traefik.yml with your email address for ACME stuff.
- The authelia configuration is pretty standard for Traefik, you can find it in their documentation here. Authelia and Traefik work really well together, which is why I chose them for my proxy and authentication backbone.
- Obviously make sure to update your authelia secrets and keys - I recommend just generating 64 digit alphanumeric strings.
- I’ve commented out the duo authentication stuff for 2FA, mostly because this is not a permanent set up for me and I didn’t want to have to worry about it. But if you want push notifications from DUO, you can follow the documentation. This works really well for my personal homelab setup, and I highly recommend setting it up if you can. You can also use normal 2FA without the duo integration.
- Last thing, make sure to update all the domain entries in the traefik labels, or else you will have issues.
Authelia
This is not a tutorial on Authelia. If you want more information on the configuration, go check out the well maintained documentation of one of the many tutorials on Youtube. I will just point out a couple of things that you will need to adjust to make things work for your Headscale setup
In the configuration.yml
:
- Adjust all
yourdomain.com
to your domain - If you want to do OIDC for Headscale node authentication, uncomment this section. I had a lot of problems getting this to work, so be aware there will likely be a lot of troubleshooting involved.
- Additionally, if you want to set up an OIDC client you will need to generate a private key, this can be generated using the following commands:
Copy the contents of the private.pem
file to the oidc section.
In the users_database.yml
- Update this file with you username, name and email. You will need to generate a secure value/password - use the following command:
The only thing that I will really note in my Authelia set up is the access_control section. The way headscale-ui works is by piggy backing off the SSL cert for headscale, so they are on the same domain. You do not want to put headscale behind authelia. There is no reason to, and you will block connections to the VPN. No good. We do however want to put the UI behind Authelia. So, we only set access rules for the /web
postfix (I don’t know what that is actually called). If you want 2FA, change the policy to two_factor.
Headscale
Again, not a ton to change here. In the config/config.yml:
- Change server url to your domain. Leave the port number.
- If you want OIDC for node authentication, uncomment the oidc block. Obviously change the client id and client password, and make sure it matches whatever is in the authelia config. Use the command I showed above to generate the secure values.
And after all of those edits, you can run docker compose up -d and magically have headscale up with a UI behind auth proxy! Pretty cool. There will be some trouble shooting with this setup, just due to the general nature of networking. Make sure that if you are using firewalld, that you have opened up ports 80/tcp and 443/tcp, or else you will not have any internet traffic.
The final bit of configuration comes when you want to connect headscale to headscale-ui. To generate an API key with 90d expiry, use the following command:
Put this into headscale-ui, and you should be able to now create users and start to add nodes to your tailnet. The way headscale-ui works, it stores secrets in-browser as cookies, so do not clear your cookies for the site or you will loose your configuration.
Conclusions
So now let’s talk about why I am not going to keep this setup. Headscale-ui is a pretty new piece of software, and did not work as well as I had hoped. I also had some DNS issues with authelia and headscale, and was never able to get OIDC to work for headscale.
I am willing to mess with things like docker containers and new services because to me most of these things are non-essential. If they don’t work, I’m not going to have a terrible time. VPN is not one of those things (though Tailscale is not technically a VPN lol). I need to be able to connect to my home network, and I don’t want to have an additional worry with a self-hosted setup.
But for those of you who are willing to accept this risk, I think headscale provides a really cool solution. I would recommend running it as a systemd service before delving into this setup. I had the most consitent performance with it as a binary rather than a docker container. But to each their own!