- Home/
- Articles/
- One Domain, Two Networks: AdGuard Conditional DNS + Caddy HTTPS for Local and VPN Access/
·4 mins
The Issue #
Let’s say you have a homelab setup that you don’t want to expose directly to the internet, but you still want to be able to access it from outside. At most, you would have a reverse proxy listening on port 443.
In our goal setup, everything should work with HTTPS, and you shouldn’t need to worry about managing DNS entries for your internet connection’s public IP address, especially if you have a dynamic IP address like I do.
This looks like a case for Tailscale MagicDNS and HTTPS certificates, but that would require you to install Tailscale on every device to be able to resolve and securely talk to each other. Local DNS should be seamless, when you’re not connected to VPN, you should be able to resolve to our local addresses and still ensure HTTPS everywhere.
Since many homelabs already use DNS filtering services, we can use these to add conditional DNS rewrites depending on which network the request comes from.
My proposed setup will look like this:
- A mesh VPN such as netbird or tailscale will provide global access to your home network services
- AdGuard Home to serve DNS both locally and on the VPN
- Caddy will handle domain certificates and act as a reverse proxy
The only notable requirement would be ownership of a domain name with a registrar supported as a dns.proviers module
on Caddy.
My Setup #
To organise my network somewhat recognisably, I’ve configured a 10.0.0.0/16 subnet and use an identifier for device categories on the third octet.
| Group | IP |
|---|---|
| Router/servers | 10.0.0.0/16 |
| Clients (PC, phone, …) | 10.0.1.0/16 |
| IoT | 10.0.2.0/16 |
Important nodes, such as my Proxmox hypervisor, carry friendly names, such as gaia. Domain names follow the network structure. For example, a server like Caddy that is hosted as an LXC on Gaia would have the domain caddy.gaia.kore.cc. A vacuum robot called Jenkins would end up on the jenkins.iot.kore.cc domain.
AdGuard Home #
I have AdGuard Home running on Proxmox as an LXC. If you are running unprivileged containers, you will need to add the following to the LXC configuration file under /etc/pve/lxc/lxc-id.conf:
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
Access to the tun device is necessary for Tailscale to work in non-userspace mode.
Caddy #
Caddy also runs within an LXC with the aforementioned /dev/net/tun passthrough.
Caddy is built with the Porkbun DNS plugin via xcaddy.
xcaddy build --with github.com/caddy-dns/porkbun
In the default block of the Caddyfile, we define the credentials for certificate DNS challenges.
{
email my@email.tld
acme_dns porkbun {
api_key my-api-key
api_secret_key my-secret-api-key
}
}
I’ve added a default snippet for the default settings, which should be applied to all hosts. Adding hosts is as simple as appending four lines, as shown below:
(DEFAULT) {
header {
Strict-Transport-Security "max-age=31536000;"
X-Frame-Options "SAMEORIGIN"
X-Content-Type-Options "nosniff"
}
encode zstd gzip
tls {
protocols tls1.3
}
}
bernard.iot.kore.cc {
import DEFAULT
reverse_proxy 10.0.2.3:80
}
adguard.gaia.kore.cc {
import DEFAULT
reverse_proxy 10.0.0.11:80
}
# ...
When the Caddy server is started, it will automatically fetch certificates and handle renewal. As these domains are intended for local use and are issued via DNS, there is no need to make any changes with the registrar.
Tailscale Setup #
We install Tailscale and start it up on every host that we want to access from outside.
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
Important: On the AdGuard host, we append --accept-dns=false to the tailscale up command, as we don’t want MagicDNS to interfere with DNS on that host.
After onboarding them with the interactive prompt, they should now be visible within our console.
Next up, head over to the DNS settings. Here we enable MagicDNS and add the adguard tailnet IP as a global nameserver. We make sure to enable the toggle to override DNS servers:
Our AdGuard Home LXC is now responding to DNS requests for devices running the Tailscale client. I won’t go into detail about the regular setup of AdGuard Home within your local network, as there are plenty of resources on that topic. However, you will need to either set the local IP of the DNS server within your router to advertise it or let your router use it as a resolver.
Network Dependent DNS Responses on AdGuard #
On AdGuard, we use custom filtering rules to differentiate between requests from our local network and from Tailscale.
||host.domain.tld^$dnsrewrite=NOERROR;A;TAILSCALE-IP,client=TAILSCALE-SUBNET
||host.domain.tld^$dnsrewrite=NOERROR;A;LOCAL-IP,client=LOCAL-SUBNET
This provides different DNS answers depending on which network the request stems from.
In this example, all services that are mapped to a .gaia domain are served via Caddy, so
we add a wildcard entry to point to the Caddy tailscale IP or local IP:
||*.gaia.kore.cc^$dnsrewrite=NOERROR;A;100.80.78.65,client=100.0.0.0/8
||*.gaia.kore.cc^$dnsrewrite=NOERROR;A;10.0.0.12,client=10.0.0.0/16
Querying on the tailnet:
david@gaia:~$ nslookup caddy.gaia.kore.cc
Server: 100.100.100.100
Address: 100.100.100.100#53
Non-authoritative answer:
Name: caddy.gaia.kore.cc
Address: 100.80.78.65
Querying on the local network:
david@themis:~$ nslookup caddy.gaia.kore.cc
Server: 10.0.0.11
Address: 10.0.0.11#53
Non-authoritative answer:
Name: caddy.gaia.kore.cc
Address: 10.0.0.12
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.