← Holocron Logs

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

Access every homelab service from anywhere (office, phone, travel) over HTTPS with valid SSL certificates, without exposing a single port on my home router.

Why this matters beyond the homelab: This is the same pattern used for identity and access management at scale, directly mapping to SC-300 and Entra ID.

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 tima.dev/blog - my Ghost blog hosted on Ghost (migrated to tima.dev/blog). The chain was:

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

The blog is hosted externally on Ghost (migrated to tima.dev/blog), 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 tima.dev/blog:

Now the traffic flows through NPM regardless of whether you’re home or remote, and NPM forwards it to Ghost (migrated to tima.dev/blog). Consistent behavior everywhere.


Zero Trust in Practice

This setup implements zero trust at two layers:

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.

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.

← Back to Holocron Logs