Cory Dransfeldt · 1mo · discuss
When I started setting up private, self-hosted services one of the things I wanted to maintain was a pattern of friendly, human readable names to access them. I didn't want the overhead of bookmarking domains available via Tailscale's MagicDNS and I wanted real SSL certificates that were trusted and that clients wouldn't complain about. The tools To accomplish this, I have DNSimple managing the public zone (for things like this site, my Mastodon instance and so forth). I have Headscale set up as the control plane and I use Tailscale's apps to connect to it. It assigns addresses to each node, dictates what each node can reach and which resolver each uses for a given domain. I'm using CoreDNS to map friendly names to mesh addresses: coryd.dev { cache 300 hosts { 100.xx.xx.xx music.coryd.dev 100.xx.xx.xx books.coryd.dev 100.xx.xx.xx photos.coryd.dev 100.xx.xx.xx git.coryd.dev fallthrough } forward . 45.90.28.0 1.1.1.1 log errors } fallthrough allows anything not defined above it in the hosts block to hit forward, returning the public answer. Connecting things Headscale distributes the resolver config to each client with the split DNS config looking like this: dns: magic_dns: true base_domain: net.coryd.dev nameservers: global: - https://dns.nextdns.io/<profile> split: coryd.dev: - 100.xx.xx.xx - 100.xx.xx.xx I'm using NextDNS as my global resolver because NextDNS is awesome and it allows my network filters to remain in place. All members of the mesh route queries to coryd.dev through CoreDNS and send everything else upstream. I configure the mesh in the Tailscale client, connect and get access to everything. Certificates you can trust Because everything here is private, HTTP-01 challenges needed to obtain certs will fail. Thankfully, DNSimple supports DNS-01, which lets me prove domain ownership via a TXT record and obtain a wildcard cert (*.coryd.dev). On my Linux boxes I'm using certbot with the dns-dnsimple authenticator to write certs to /etc/letsencrypt. Caddy mounts this directory as read-only, has auto_https off and the file names are referenced explicitly. On my NAS, I'm running Caddy as a native binary with the DNSimple module handling the challenge. Breaking things Because I'm an idiot, I have the mesh running on the same domain CoreDNS resolves. If a client can't resolve a name, it can't connect to the mesh. I have a second instance of CoreDNS running on a separate server to mitigate this. If you're using MagicDNS, use separate subdomains. It'll refuse a base domain that's equal to, or a parent of, the control plane's hostname. The client would end up taking over the zone holding the server it needs. I used mesh and net as independent names. Point Docker's dns settings at the bridge address or you'll run into lookup delays with CoreDNS. CoreDNS listens on the mesh address, containers live on a bridge, so packets go in a loop, the resolver times out and then it tries the next resolver and succeeds. That CoreDNS binds to the mesh address created another issue — the address only appears after tailscaled registers with Headscale and Headscale is a container on the same host. I opened the socket first before the address was available by setting net.ipv4.ip_nonlocal_bind = 1. The other thing — the addresses behind these names spread out everywhere: they're in the Corefile, yes, but they're also in my ACL policy, port bindings, extra_hosts, daemon.json and deploy scripts, so documentation is crucial. It works! It all works! So far, anyways. Most things I host are private and they have addresses I can remember. They have certificates so there are no complaints by clients and browsers when I access them. Better yet: if my family needs to access something, the address is intelligible. Adding a new subdomain is easy, changing the address tied to one is tedious given the myriad places they flow through.1 There's probably a better way to do this, so enlighten me. ↩︎