Banner: split-horizon DNS and Nginx Proxy Manager

Split-Horizon DNS and Nginx Proxy Manager: Why My VM Couldn’t Resolve Its Own Blog

I recently ran into a DNS problem that looked like a WordPress or reverse-proxy failure: the VM hosting my automation tools could reach the network, but resolving the public name for my own blog was inconsistent. The cause was split-horizon DNS: internal clients were receiving a local answer for the www name, while the zone apex still returned SERVFAIL.

The fix was not to change WordPress. I verified the DNS path, documented the router’s local override, and kept the reverse proxy responsible for HTTPS rather than trying to make every client bypass DNS.

1. Separate DNS from application connectivity

First I check whether the name resolves at all. A successful network route or TCP connection does not prove that DNS, TLS, or WordPress is healthy.

getent ahostsv4 www.<site-domain>
getent ahostsv4 <site-domain>

resolvectl status

ip route

curl -k -I --max-time 10 https://www.<site-domain>/

On the affected VM, the www name resolved to the internal reverse-proxy address and HTTPS returned 200. That told me the application path was working. The remaining problem was the difference between the www record and the zone apex.

2. Query the DNS server directly

I use dig to see the status code, answer, and server that responded. This is more useful than looking only at the result of a browser request.

dig +time=2 +tries=1 www.<site-domain> A
dig +time=2 +tries=1 <site-domain> A
dig +time=2 +tries=1 <site-domain> CNAME

The useful fields are:

  • NOERROR with an answer means the resolver found a usable record.
  • SERVFAIL means the resolver could not complete the lookup. It is not the same as “the host does not exist.”
  • The SERVER line identifies which resolver actually answered the query.
  • A CNAME shows the next name that must also resolve. A broken target can make the original lookup fail.

In my case, the internal resolver returned a local CNAME for www and an internal address for the proxy target. The apex lookup returned SERVFAIL. That combination explained why the browser worked with the full www name while tools using the bare domain failed.

3. Check the resolver path and search domains

Linux systems commonly use the systemd-resolved stub listener. I check the effective configuration instead of editing /etc/resolv.conf blindly.

resolvectl status
resolvectl query www.<site-domain>
resolvectl query <site-domain>

readlink -f /etc/resolv.conf
cat /etc/resolv.conf

A search domain can also change what an unqualified name means. I use fully qualified names while troubleshooting, including the trailing dot when I want to avoid search-list processing:

dig +time=2 +tries=1 www.<site-domain>.
dig +time=2 +tries=1 <site-domain>.

If the fully qualified query fails in the same way, the issue is in the zone or resolver path rather than a shell search-domain expansion.

4. Verify the router’s local DNS override

My router provides the internal DNS override. The public name is deliberately mapped to the local reverse proxy for clients on the LAN. This is the split-horizon part: internal and external clients can receive different answers for the same public name.

I verify the override in the router’s DNS or host-record configuration:

# Use the router's actual management UI or API, then verify from the VM:
dig @<router-dns-address> www.<site-domain> A
dig @<router-dns-address> <site-domain> A

The important detail is consistency. If the site is intended to work at both the apex and www, both names need deliberate records. If only www is supported, I make that canonical URL clear and redirect the apex at the reverse proxy or public DNS provider.

5. Keep Nginx Proxy Manager focused on HTTP and TLS

Nginx Proxy Manager does not fix a missing DNS record. It receives a request after the client has resolved the hostname. I check the proxy host configuration and certificate separately:

curl -k -I --max-time 10 https://www.<site-domain>/
curl -k -I --max-time 10 https://<site-domain>/

# From a client that uses the intended address, test the Host header:
curl -k -I --resolve www.<site-domain>:443:<proxy-address> https://www.<site-domain>/

The --resolve test is useful because it bypasses normal DNS while preserving the HTTPS hostname and SNI. If this works but a normal request fails, DNS is still the first place to look. If it fails too, then I investigate the proxy host, certificate, upstream service, or WordPress response.

6. Flush caches only after correcting the record

After changing the router record, I clear the resolver cache on the client and repeat the same queries. Flushing a cache before correcting the authoritative or local record only makes the old answer disappear temporarily.

sudo resolvectl flush-caches
resolvectl statistics

dig +time=2 +tries=1 www.<site-domain> A
curl -k -I --max-time 10 https://www.<site-domain>/

Browsers and applications may maintain their own DNS cache, so I also restart the affected application or browser when the command-line results are correct but the application still uses the old answer.

7. The troubleshooting checklist

  1. Check the exact hostname with getent and dig.
  2. Record the DNS status, answer, CNAME target, and responding resolver.
  3. Compare the apex and www names instead of assuming they are equivalent.
  4. Verify the router’s internal override and the public record independently.
  5. Test HTTPS with curl, including --resolve when isolating DNS.
  6. Flush caches only after the DNS configuration is correct.

The practical lesson is that a reverse proxy cannot compensate for an incomplete DNS design. Once I treated DNS, TLS, proxy routing, and WordPress as separate layers, the SERVFAIL was straightforward to isolate without changing the blog application.

Leave a Reply