I self-host many apps on my home network, two of which I need access to when I am not at home: Nextcloud and Home Assistant. For years, I ensured remote access by simply exposing these two apps to the public Internet. I secured both apps as well as I could, having set up HTTPS, 2FA and CrowdSec, and keeping the apps up to date. Over time, I had grown less confident in that setup, amid the rise of software vulnerabilities discovered by LLMs.
I decided to finally stop exposing my apps to the outside world, while still having a way to access them externally. I use Tailscale for that purpose. Tailscale can be described as a mesh VPN. It works by creating a virtual network, called a "tailnet", that client devices can then join, after which they get assigned a Tailscale IP address in the 100.64.0.0/10 range. Thanks to these IP addresses, clients on a tailnet can communicate with each other using encrypted peer-to-peer VPN connections. So, if you want external access to a computer on your LAN from your phone for example, you simply install the Tailscale app on both your computer and your phone, and connect them to the same tailnet. Tailscale itself only acts as a coordinator; it cannot read the data being exchanged between clients.
I usually access my apps using subdomains of a domain that I own. Before setting up Tailscale, I would simply use these subdomains to access my exposed apps from outside of my LAN. A port forward on my router would then forward the request to my reverse proxy, which would in turn forward it to the proper application based on the hostname.
My goal while setting up Tailscale was to change as little as possible to my existing infrastructure. The easiest way was to add my reverse proxy to my tailnet. However, in order to access the desired application through the reverse proxy, the correct hostname needs to be set in the HTTP request. I could have simply changed my DNS records so my subdomains would point to my reverse proxy's Tailscale IP instead of my home network's public IP address. There are caveats to that approach though:
- My reverse proxy currently uses HTTP challenges to renew certificates with Let's Encrypt, so my subdomains need to remain routable normally (which also means I have to keep the port forward to my reverse proxy, and rely on the reverse proxy itself to block traffic from the public Internet).
- I want to keep the possibility of making my applications accessible directly (without Tailscale) from specific IP addresses, which this approach would prevent.
Because of these caveats, I decided to go with a solution relying on DNS rewrites. This is how I already accessed my applications locally: I have an AdGuard Home server that acts as my local DNS server, in which I configured DNS rewrites to local IP addresses for all my self-hosted applications.
I could have simply changed my AdGuard Home rewrites for Nextcloud and Home Assistant to use my reverse proxy's Tailscale IP address instead of its actual LAN address, and then set up AdGuard Home as my tailnet's DNS server. That, however, would have forced me to install Tailscale on all my local computers for them to still be able to access Nextcloud and Home Assistant. I preferred to avoid that.
I figured I needed a second layer of DNS rewrites, which would be used only on my tailnet. Since it has become so easy to create simple custom tools now, I used OpenCode to put together a very simple DNS proxy in about 5 minutes. This proxy maps specific domains to hardcoded IP addresses, and forwards any other request to an actual DNS server. I configured it to rewrite my Nextcloud and Home Assistant subdomains to my reverse proxy's Tailscale IP address, and installed it on the same machine as the reverse proxy itself. I then set the proxy's Tailscale IP address as a DNS server on my tailnet, and enabled split DNS so only requests concerning my domain are sent to that server. It works like a charm!
After putting all this together, I found out about Tailscale's subnet router feature, which might have provided another way to achieve my goals. I also found this blog post, which describes how to use a subnet router to do exactly that. That being said, I like the simplicity of my current solution, and how it keeps Tailscale away from most of my infrastructure.
