Tailscale Split DNS + AdGuard: Remote Access Without Opening a Single Port


The Goal

Access every homelab service from anywhere (office, phone, travel) over HTTPS with valid SSL certificates, without exposing a single port on my home router. No port forwarding, no dynamic DNS hacks, no split-horizon DNS nightmares.

The answer: Tailscale for the network layer, AdGuard Home for DNS resolution, and NPM for TLS termination. Three tools, zero open ports.


The Architecture

[Travel Laptop / Phone]
    │
    ├── Tailscale tunnel (WireGuard, NAT traversal)
    │
    ├── Split DNS: *.tima.dev → AdGuard Home (192.168.1.4)
    │
    ├── AdGuard wildcard rewrite: *.tima.dev → NPM (192.168.1.101)
    │
    ├── NPM terminates SSL (*.tima.dev wildcard cert)
    │
    └── NPM proxies to backend service (192.168.20.x)

Why This Works

Tailscale creates an encrypted WireGuard tunnel between your device and the homelab. It punches through NAT on both sides - no router configuration needed. Your device gets an IP on the Tailscale network (100.x.x.x) and can reach any subnet you've approved for routing.

Split DNS means only *.tima.dev queries get sent to your homelab's AdGuard resolver. Everything else - work domains, public internet - uses the device's normal DNS. This prevents your homelab DNS from interfering with corporate networks.

AdGuard receives the *.tima.dev query and returns the NPM IP via a wildcard rewrite rule. NPM handles SSL and routes to the correct backend.


Tailscale Configuration

Subnet Router

One node in the cluster runs Tailscale with subnet routing enabled, advertising all four VLANs:

tailscale up --advertise-routes=192.168.1.0/24,192.168.20.0/24,192.168.30.0/24,192.168.40.0/24

In the Tailscale admin console, approve each subnet route. This allows any Tailscale-connected device to reach internal IPs across all VLANs.

Split DNS Setup

In Tailscale Admin → DNS:

  1. Add nameserver → Custom → 192.168.1.4 (AdGuard Home IP)
  2. Restrict to domaintima.dev

This is the critical setting. Do not enable "Override local DNS" - that routes ALL DNS through your homelab, which breaks corporate network resolution when you're at the office.

With split DNS restricted to tima.dev, only homelab queries use AdGuard. Work queries, public internet, everything else stays on the device's default resolver.

AdGuard Wildcard Rewrite

In AdGuard Home → Filters → DNS Rewrites:

*.tima.dev → 192.168.1.101

Verification: Office to Homelab

From the office PC with Tailscale connected:

ipconfig /flushdns

Then open https://grafana.tima.dev in the browser:

  1. Browser requests grafana.tima.dev
  2. Tailscale split DNS sends the query to AdGuard (192.168.1.4) via the WireGuard tunnel
  3. AdGuard wildcard returns 192.168.1.101 (NPM)
  4. Browser connects to NPM through Tailscale tunnel
  5. NPM serves the wildcard SSL cert and proxies to Grafana (192.168.20.40:3000)
  6. Grafana login page loads with valid HTTPS

Every service - Grafana, Wazuh, Authentik, Portainer, n8n, Vaultwarden, OpenWebUI - works identically from the office.


What Went Wrong: The DNS Override Bug

Early in the setup, I enabled "Override local DNS" instead of split DNS. This sent ALL DNS queries through AdGuard - including my work PC's queries for Team Liquid's internal domains.

The immediate symptom: work applications broke. Internal tools, email, VPN - everything that resolved via the corporate DNS suddenly couldn't find its servers.

The fix was switching from full override to split DNS restricted to tima.dev. Five-second change, but a good reminder that DNS misconfigurations cascade fast.

The Ghost Blog Incident

A related DNS issue surfaced with holocron-labs.tima.dev - my Ghost blog hosted on Ghost(Pro). The chain was:

holocron-labs.tima.dev
→ Tailscale split DNS → AdGuard
→ AdGuard wildcard *.tima.dev → NPM (192.168.1.101)
→ NPM had no proxy host for holocron-labs.tima.dev
→ SSL error / 404

The blog is hosted externally on Ghost(Pro), not in my homelab. But the wildcard rewrite was catching it and routing it to NPM, which had no idea what to do with it.

Fix: Add an NPM proxy host for holocron-labs.tima.dev:

  • Domain: holocron-labs.tima.dev
  • Scheme: https
  • Forward Hostname: holocron-labs.ghost.io
  • Forward Port: 443
  • SSL: Let's Encrypt

Now the traffic flows through NPM regardless of whether you're home or remote, and NPM forwards it to Ghost(Pro). Consistent behavior everywhere.


Zero Trust in Practice

This setup implements zero trust at two layers:

  1. Network access - Tailscale. You can't reach any homelab IP without being authenticated to the Tailscale network. The ACL policy controls which devices can reach which subnets.

  2. Application access - Authentik SSO. Even if you're on the Tailscale network, hitting any service through NPM triggers an Authentik login challenge with TOTP MFA.

Passing one gate doesn't bypass the other. A compromised Tailscale device still can't access Grafana without valid Authentik credentials. A stolen Authentik password is useless without Tailscale network access.


Summary

Component Role Config
Tailscale Encrypted tunnel + NAT traversal Subnet routes for all VLANs
Split DNS Route *.tima.dev to homelab Restricted to tima.dev domain only
AdGuard Internal DNS resolution Wildcard rewrite *.tima.dev → 192.168.1.101
NPM TLS termination + reverse proxy Wildcard cert *.tima.dev via Cloudflare DNS challenge
Authentik Application-layer authentication SSO + TOTP MFA on all services

Zero open ports. Valid SSL everywhere. Works from any network on earth.


Related: Post 013 - NPM Rebuild and Cloudflare DNS Migration covers the NPM and wildcard cert setup that makes this possible.