Local DNS, Filtering, and Reverse Proxy

Every self-hosting guide ends at 'now point a name at it' and waves. This is that layer: AdGuard Home filtering in front of Unbound resolving behind, a reverse proxy for the names that need one, and the two things nobody writes up honestly, which are that you cannot have both recursive privacy and encrypted DNS, and that a service with a LAN address and a tailnet address needs a certificate covering both or it breaks on whichever path you forgot.

7 steps 22 min read 2026-08-20
Tools UsedSee full toolkit below →
AdGuard Home
The front half; filtering, per-client policy, query log, and the rewrites that make internal names resolve
Unbound
The back half; the actual resolver, recursing or forwarding depending on which threat you care about
Nginx Proxy Manager
Routes HTTP by hostname for the services that benefit, and terminates TLS in one place
Tailscale
Off-LAN access, and the reason split DNS and certificate SANs become a real problem

Read enough self-hosting guides and you notice they all end in the same place. The container is running, the port is open, and the final instruction is some version of “now point a name at it.” Sometimes there’s a hosts file entry. Usually there’s nothing at all, and you’re left typing an IP and a port number for the next two years.

This is the layer that fixes it, and it’s the one piece of home infrastructure that sits underneath everything else you run. It does three separable jobs that tend to get conflated:

The stack here is AdGuard Home filtering in front of Unbound resolving behind, both on one small always-on box, with Nginx Proxy Manager handling HTTP routing on a separate host and a local certificate authority issuing certs for internal names.

Each of those components is individually well documented, and this guide doesn’t rehearse their installers. What isn’t written up honestly anywhere is how they interact, and in particular two things that bite everyone who builds this:

This site’s Proxmox guide builds a local CA with mkcert and explains why internal names can’t have public certificates. This guide is the naming and routing that sits around it, and it deliberately points back rather than repeating that material.

Step 1

What This Layer Actually Does

The shape, before any configuration:

client -> [router hands out one resolver via DHCP]
       -> AdGuard Home  (filter, per-client policy, rewrites)
       -> Unbound       (resolve: recurse, or forward encrypted)
       -> the internet

client -> service.lan -> [AGH rewrite] -> Nginx Proxy Manager -> the actual service
                                       -> or straight to the host, for services that must not be proxied

AdGuard Home and Unbound belong on the same box, and the reason is that every DNS query on your network crosses this boundary. The hand-off between them happens on loopback rather than over the wire. The saved latency per query is small, genuinely a fraction of a millisecond, but it’s on the critical path for every connection every device makes, and there is no upside to splitting them across two machines. A Raspberry Pi is more than enough hardware; resolution is a caching problem, not a compute one.

Be clear about what this layer is not. It is not a firewall, it is not a VPN, and DNS filtering is not a security boundary. A blocked name stops a lookup, not a connection: anything with a hardcoded IP address ignores your resolver completely. Filtering is a broad, cheap, high-value control that removes an enormous amount of nuisance traffic, and it is trivially bypassed by anything that wants to. Step 7 is about the layer that catches what this one misses.

Step 2

Recursion or Encryption, Pick One

Here is the fork that guides skip, usually by presenting one option and not mentioning the other exists.

Option A: full recursion. Unbound walks the hierarchy itself. It asks a root server which servers handle .com, asks those which servers handle example.com, then asks that server for the record. There is no upstream resolver, which means no single operator ever sees your complete query history. Cloudflare doesn’t have it, Google doesn’t have it, your ISP doesn’t have it.

What recursion does not give you is encryption. Those conversations with root, TLD, and authoritative servers are largely cleartext on port 53, because the authoritative side of the DNS system broadly does not offer TLS. Two things soften that. DNSSEC validation, which Unbound does by default, proves an answer wasn’t tampered with, though it proves nothing about who saw it. And QNAME minimisation, on by default in current Unbound, sends each server only the labels it needs: the root learns you asked about something in .com and nothing more, the .com servers learn the domain but not the specific host. Your queries are spread thin across many servers, each seeing a fragment, none seeing the whole.

Option B: forwarding over DNS-over-TLS. Unbound forwards everything to an upstream on port 853 with forward-tls-upstream: yes and a tls-cert-bundle for validation. Now the query leaves your network encrypted, which defeats your ISP, a hostile local network, and anyone watching the wire. The cost is that the operator on the far end sees every query you make, and you are trusting their retention policy.

You cannot have both, and the reason is structural rather than a configuration gap: encrypting the last hop requires a single endpoint to encrypt to, and having a single endpoint is precisely what recursion avoids.

So choose by naming the adversary:

For most home networks defending against ambient ISP data collection, forwarding to a privacy-respecting operator over DoT is the pragmatic answer. For people whose objection is to the aggregation itself, recursion is the honest one. Neither is wrong; picking without knowing there was a choice is.

How AI can help

This is a good decision to talk through rather than a good one to have generated for you. Describe your actual situation, who runs your network, what your ISP does with data in your jurisdiction, whether you already route traffic through a VPN, and ask it to argue both sides. What it's genuinely useful for afterwards is the config: an Unbound configuration with QNAME minimisation, DNSSEC validation, sensible cache sizing, and access control locked to loopback is fiddly to write and easy to get subtly wrong. Ask it to annotate every directive so you understand what you deployed, and specifically to explain which lines prevent the resolver being reachable from outside your network.

Step 3

AdGuard Home in Front, Unbound Behind

The split is straightforward: AdGuard Home listens on port 53 and is what every client talks to. Its only upstream is Unbound, on loopback at a non-standard port (127.0.0.1:5335 is the convention, chosen to stay clear of 53). Unbound does the resolving.

AdGuard Home’s half: blocklists, per-client rules so the kids’ tablets can have a stricter policy than the workstation, a query log that tells you what a device is actually contacting, and DNS rewrites, which are what make internal names resolve at all and which Step 5 depends on.

Unbound’s half: resolution and caching, in whichever mode you picked in Step 2.

The reason to split them rather than let AdGuard Home talk to public upstreams directly is separation of concerns. Filtering policy and resolution strategy change for completely different reasons, and keeping them in different processes means you can switch from recursion to DoT forwarding, or the reverse, without touching a single blocklist. It also means the query log and the resolver cache are independent, which is genuinely useful when debugging.

Warning: Do not become an open resolver. Unbound should listen on loopback only, since AdGuard Home is its sole client, and AdGuard Home should be bound to your LAN and tailnet interfaces with access control limiting which networks may query it, never to every interface with no restrictions. A resolver reachable from the public internet gets conscripted into DNS amplification attacks, where your box is used to flood a stranger with traffic you paid to send. This is the single most consequential mistake available in this entire guide, it is silent, and the first sign of it is usually your ISP getting in touch. Verify from outside your network that port 53 answers nothing.

Step 4

Making It Stick

Handing out the resolver is easy: the router’s DHCP tells every client to use it, which covers the overwhelming majority of devices and requires no per-device configuration. The interesting question is what happens with devices that don’t cooperate, and the honest answer has limits worth stating plainly.

Hardcoded plaintext DNS is the common case. Plenty of consumer devices, smart TVs and IoT gear especially, ship with a public resolver baked in and never consult DHCP at all. This one you can handle at the router: redirect outbound port 53 traffic back to your own resolver, or block outbound 53 from everything except the resolver itself. The device thinks it’s talking to a public server and gets your filtered answers instead. This is worth doing, and it catches a real and surprisingly large category.

Client-side DoH is the case you largely cannot handle. A browser or device resolving over HTTPS on port 443 to a public endpoint produces traffic that is, without deep inspection you should not be doing on your own network, indistinguishable from ordinary web browsing. You can push policy to managed browsers, you can block the known endpoints of the largest providers and watch that list rot, and beyond that you are relying on the device’s cooperation.

So this is a leaky control, and useful anyway. It catches everything that isn’t deliberately evading it, which is most things. What it doesn’t do is guarantee that every name resolution on your network passed your filter, and a guide that implies otherwise is selling you something. That gap is the argument for Step 7.

Step 5

Names, Certificates, and the Two-Address Problem

There are two ways a name on your network turns into a working service, and picking the right one per service is most of the design work.

Rewrite straight to the host. An AdGuard Home rewrite points something.lan at a machine’s IP, and you reach the service on its own port. Simple, no extra hop, and mandatory for anything that a proxy breaks. The Proxmox hypervisor UI is the canonical example: its authentication cookie doesn’t survive a proxy hop unless the proxy is configured exactly right, and the symptom is a 401 that looks like wrong credentials. That guide’s advice is to never proxy the console, and a DNS rewrite plus the native port gives you a friendly name without the hop.

Rewrite to the proxy. The name points at Nginx Proxy Manager, which routes by hostname to the real service. Worth it when several services share a host, when you want one place to terminate TLS, or when you want names without port numbers. This is the default for ordinary web services.

The split in practice is unremarkable: things that must not be proxied get direct rewrites, everything else goes through the proxy.

Then certificates, which is where this gets genuinely annoying. A service on your network can be reached at up to four different addresses: its LAN hostname, its LAN IP, its tailnet name, and its tailnet IP. A certificate is only valid for the names and addresses listed in it. Miss one, and the service works perfectly on every path you tested and fails on the one you didn’t, with a certificate error that reads like a DNS or proxy problem.

So put all of them in the SAN list, and verify rather than assume:

openssl x509 -in service.pem -noout -ext subjectAltName

The related decision is where the certificate comes from, and the short version is that internal names can’t have public ones. No public authority will sign service.lan, and the workarounds that do work, a real domain or Tailscale’s own HTTPS, publish the hostname into Certificate Transparency logs permanently. A local CA never publishes anything. The Proxmox guide covers standing one up with mkcert and distributing the root to your devices; this guide simply assumes you have one.

Important: Two failure modes here are documented in detail elsewhere on this site and both look like something they aren’t. A missing SAN for the tailnet address produces a certificate error only when you’re away from home, which reads as a remote-access fault. And HSTS is scoped to the hostname, not the port: one HTTPS service on a shared host name pins that entire name to HTTPS in your browser, so a plain-HTTP sibling on a different port of the same name silently stops loading. Both are covered with fixes in the Proxmox and Jellyfin guides.

How AI can help

The mechanical part here is worth delegating and the design part isn't. Give it your service inventory and ask it to produce the rewrite table: which name, pointing where, direct or proxied, and why. It's good at catching the ones you'd miss, particularly services that expose a second port for something (an API, a metrics endpoint) that also needs to resolve. For certificates, hand it the list of addresses each service answers on and have it generate the exact SAN list plus the verification command, since the whole class of bug here is an address you forgot rather than a concept you got wrong. Have it explain the difference between a DNS name and an IP entry in a SAN list; tools quietly treat a mistyped address as a DNS name and produce a certificate that looks right and isn't.

Step 6

Tailscale and Split DNS

Adding a tailnet means running two naming systems at once, and how they interact decides whether your setup works away from home.

Tailscale’s MagicDNS gives every machine a *.ts.net name that resolves from anywhere on the tailnet, with no configuration. Your resolver knows .lan names and does the filtering. The question is what a client uses when.

The naive fix is pointing your tailnet’s global nameserver at your home resolver. Everything then resolves through your box, .lan works from anywhere, and filtering follows you onto cellular. It also means every DNS query from every device traverses the tunnel to your house, which adds latency to everything and makes your home connection a dependency for all name resolution everywhere. When the Pi reboots, your phone loses DNS in a coffee shop.

Split DNS is the better answer. Tailscale lets you restrict a nameserver to specific domains, so you send only .lan (or whatever internal suffix you use) to your home resolver and leave everything else to the client’s normal path. Internal names work remotely, ordinary browsing doesn’t detour through your house, and a home outage costs you internal name resolution rather than all name resolution.

The tradeoff is that filtering then only applies on the LAN, since public lookups from a remote device never reach AdGuard Home. If you specifically want filtering to follow your devices, the global-nameserver approach is how you get it, and the latency and dependency are the price. Both are defensible; pick knowing which you chose.

Step 7

Below DNS

Everything so far operates on names, and Step 4 established that a determined or merely stubborn device can avoid names entirely. Hardcoded IPs never consult your resolver. Client-side DoH goes around it. And a blocked name does nothing to stop a connection to an address the device already knows.

So the last layer works on addresses instead of names, at the router, where all the traffic actually passes. Where DNS filtering says “you may not look that up,” this says “you may not talk to that host,” and the two catch different things.

The worked example here is Skynet on Asuswrt-Merlin, which maintains IP blocklists and firewall rules on the router itself. Its value in this stack is precisely that it sits at a different level: it sees traffic the resolver never saw, and it enforces on connections rather than lookups.

This is also the least portable part of the guide, and it depends entirely on what your router runs. The equivalents, roughly in order of how much they’ll ask of you: OpenWrt has address-blocking packages and is the usual answer for consumer hardware you can reflash, RouterOS covers Mikrotik gear, and OPNsense or pfSense on dedicated hardware gives you the most control and the most administration. A simplified home build with capable firmware on the main router is a reasonable middle, and a dedicated firewall appliance is a bigger commitment than most home networks need.

If your router can’t do any of this, you haven’t lost much. DNS filtering plus a resolver you control is the bulk of the practical benefit; this layer is the increment that catches the traffic designed to slip past it.

What You Spent

Nothing recurring.

The recurring cost is attention rather than money. This layer sits under everything, which means when it breaks, everything looks broken, and the failure presents as “the internet is down” rather than “DNS is down.” That’s the real argument for keeping the DNS pair on dedicated hardware that does nothing else and gets rebooted rarely, and for knowing how to point a device at a public resolver by hand while you fix it.

Toolkit Reference

The components that appear across this guide, and the concrete spots where an AI assistant earns its keep.

The Stack

AdGuard Home
Filtering, per-client policy, query log, and DNS rewrites. Listens on 53; its only upstream is Unbound on loopback.
Unbound
The resolver. Recurses from the root, or forwards over DoT, depending on which threat you decided to care about. DNSSEC validation and QNAME minimisation are on by default.
Nginx Proxy Manager
Routes HTTP by hostname and terminates TLS in one place. Caddy and Traefik are the obvious alternatives, the latter if your services are already container-orchestrated.
Tailscale
Off-LAN access. Split DNS is the setting that decides whether internal names work remotely without routing all your DNS through your house.
mkcert
The local CA for internal names, so certificates never reach public Certificate Transparency logs. Standing one up is covered in the Proxmox guide.
Skynet
Router-level address blocking on Asuswrt-Merlin. OpenWrt, RouterOS, and OPNsense are the equivalents on other hardware.

Where AI Earns Its Keep

Arguing both sides of the DNS decision
Recursion versus encrypted forwarding is a judgement call about your threat model. Have it argue both, then decide yourself.
Annotated resolver config
Unbound configuration is fiddly and quietly consequential. Ask for every directive to be explained, and specifically which lines keep the resolver unreachable from outside.
The rewrite table
Feed it your service inventory, get back name, target, and proxied-or-direct with reasoning. Good at catching secondary ports you forgot needed a name.
Certificate SAN lists
Every address a service answers on, turned into an exact SAN list plus the verification command. The bug class here is an address you forgot, not a concept you missed.
Router rule translation
Turning "redirect outbound 53 to my resolver" into the correct syntax for your particular firmware, which differs meaningfully across Merlin, OpenWrt, RouterOS, and OPNsense.