“The best infrastructure is the infrastructure you delete.” — Me, staring at 23 proxy pods
The Problem: Proxy Sprawl
I’ve been running Tailscale in my homelab for remote access, using the Tailscale Operator’s Ingress feature. It works great — you add a tailscale ingress class to your app, and the operator spins up a proxy pod that appears in your Tailnet. Access it via MagicDNS (paperless.${TAILNET_DNS_NAME}) and you’re in.
The problem? I had 23 of these proxy pods:
|
|
Each app gets its own Tailscale device, its own WireGuard tunnel, its own memory footprint. And the URLs are ugly — paperless.${TAILNET_DNS_NAME} instead of just paperless.${SECRET_DOMAIN}.
What I really wanted: type paperless.${SECRET_DOMAIN} from anywhere and have it Just Work. On the LAN, on Tailscale, wherever.
The Goal: Same URL Everywhere
The dream:
| Location | URL | Result |
|---|---|---|
| LAN | paperless.${SECRET_DOMAIN} |
Resolves to internal gateway, works |
| Tailscale (remote) | paperless.${SECRET_DOMAIN} |
Resolves to internal gateway via WireGuard, works |
| Public internet | paperless.${SECRET_DOMAIN} |
No access (internal-only app) |
One URL. Zero extra infrastructure. No per-app proxy pods.
The Solution: Split DNS + Connector
The key insight: my internal gateway (10.90.3.202) is already reachable via Tailscale — I just need DNS queries to return that IP when I’m connected to the Tailnet.
This requires two pieces:
- Tailscale Connector — A pod that advertises my cluster subnet (
10.90.0.0/16) to the Tailnet - Split DNS — Configure Tailscale to forward
*.${SECRET_DOMAIN}queries to my UDM Pro (which has DNS records created by external-dns-unifi)
How It Works
|
|
The beauty: my internal gateway doesn’t know or care whether the request came from the LAN or through Tailscale. It’s just another client hitting 10.90.3.202.
Setting Up the Connector
First, I needed a subnet router so Tailscale clients can reach my cluster IPs. The Tailscale Operator makes this easy with the Connector CRD:
|
|
Add it to your kustomization and push:
|
|
After Flux reconciles, you’ll see a new pod and Tailscale device:
|
|
Warning
You need to approve the subnet routes in Tailscale Admin. Go to Machines, find home-subnet-router, and approve the 10.90.0.0/16 route.
Configuring Tailscale Split DNS
Now the fun part. In the Tailscale Admin Console:
- Navigate to the DNS tab
- Under Nameservers, click Add nameserver → Custom…
- Configure:
- Nameserver:
10.90.254.1(your UDM Pro) - Check Restrict to domain
- Domain:
${SECRET_DOMAIN}
- Nameserver:
The dialog looks like this:
|
|
I also enabled Override local DNS to ensure Tailscale’s DNS config takes precedence when connected.
Info
Why UDM Pro instead of k8s-gateway? I previously used k8s-gateway for internal DNS, but later migrated to using external-dns-unifi which pushes DNS records directly to my UDM Pro. This simplifies the architecture — UDM serves the same DNS records to LAN clients, pods (via CoreDNS forwarding), and Tailscale clients. One source of truth for internal DNS.
Testing It Works
From a Tailscale-connected device (away from home):
|
|
From LAN (without Tailscale):
|
|
Both return the same IP — my internal gateway. Same URL, same destination, regardless of where I am.
The Migration: Removing Tailscale Ingresses
With Split DNS working, those 23 proxy pods became redundant. Time to delete them.
Before (per-app Tailscale proxy):
|
|
After (just the internal route):
|
|
The migration is straightforward:
- Configure Split DNS in Tailscale admin (done above)
- Verify access works via
paperless.${SECRET_DOMAIN}on Tailscale - Remove the
ingress.tailscaleblocks from HelmReleases - Clean up orphaned Tailscale devices in admin console
The Numbers
| Metric | Before (Ingress) | After (Split DNS) |
|---|---|---|
| Tailscale proxy pods | 23 | 0 |
| Tailscale devices | 24 (proxies + connector) | 1 (connector) |
| New infrastructure | - | 1 Connector pod |
| URLs to remember | 23 MagicDNS names | 0 (same as LAN) |
Why This Works
Tailscale’s Split DNS feature intercepts DNS queries at the OS level. When I look up paperless.${SECRET_DOMAIN}:
- On LAN: Query goes to my UDM Pro, which has internal DNS records (created by external-dns-unifi) pointing to
10.90.3.202 - On Tailscale: Query is intercepted and forwarded through the WireGuard tunnel to
10.90.254.1(UDM Pro), which returns10.90.3.202
In both cases, the browser gets 10.90.3.202. The subsequent HTTPS request goes directly to the internal gateway — on LAN via the local network, on Tailscale via the WireGuard mesh.
The Connector’s subnet advertisement is what makes 10.90.3.202 reachable from Tailscale. Without it, DNS would resolve correctly but the connection would timeout.
Lessons Learned
-
Split DNS is the right pattern — Per-app proxies were solving the wrong problem. I didn’t need 23 WireGuard tunnels; I needed one subnet route and proper DNS.
-
Connectors are underrated — The Tailscale Operator’s Connector CRD is incredibly simple. One YAML file, and suddenly your entire cluster subnet is on your Tailnet.
-
Same URL everywhere matters — Having to remember
paperless.${TAILNET_DNS_NAME}vspaperless.${SECRET_DOMAIN}was annoying. Now I just use the real URL regardless of where I am. -
Delete infrastructure when you can — Those 23 proxy pods weren’t free. They consumed memory, created noise in
kubectl get pods, and cluttered my Tailscale device list. Sometimes the best optimization is removal.
References
- Tailscale DNS Documentation — Official Split DNS guide
- What is Split DNS? — Conceptual overview
- Tailscale Subnet Routers — Making internal networks reachable
- Tailscale Kubernetes Operator — Connector CRD docs