Self-Hosted DNS Resolver on a VPS: Unbound, DoT/DoH and Honest Privacy Limits
Run your own recursive DNS resolver on a VPS with Unbound: DNSSEC, DoT/DoH, ACLs that prevent open-resolver abuse, cache tuning — and what it can't hide.
Running your own recursive DNS resolver on a VPS is one of the few privacy changes you can make that has a clear, verifiable effect: your ISP (or hotel Wi-Fi, or mobile carrier) stops seeing your query stream, and you stop handing a complete browsing map to Cloudflare, Google or Quad9. It also introduces a trade-off almost nobody mentions: a resolver used by one person makes that person's queries trivially attributable at the authoritative side. This guide covers the full Unbound setup, the lockdown steps that keep you off amplification-abuse lists, and where the privacy claim actually stops.
Direct answer
Install Unbound on a Linux VPS, run it as a validating recursive resolver (not a forwarder), bind it to a private interface such as your WireGuard tunnel, restrict access with both Unbound ACLs and nftables, and optionally expose DNS-over-TLS on port 853 for clients that aren't on the VPN. This gives you DNSSEC validation, your own cache, no third-party resolver logs, and no ISP visibility into query content. It does not hide the destinations you connect to — the subsequent TCP/TLS connection still reveals the server IP, and usually the hostname via SNI.
What running your own resolver actually changes
Be precise about the threat model, because "encrypted DNS" is marketed far beyond what it does.
What you gain:
- Your ISP, transit networks and local network operators no longer see plaintext port 53 queries from your device. With DoT/DoH they see only an encrypted session to your VPS.
- No commercial resolver keeps a per-client query log or feeds it into anything.
- You control DNSSEC validation, so downgrade and spoofing attempts fail closed rather than being trusted from a remote resolver.
- You can bypass ISP-level DNS filtering and hijacked NXDOMAIN responses.
What you don't gain:
- Authoritative servers still see your resolver's IP and the exact names you look up. A shared public resolver hides you in a crowd of millions; a personal resolver on a static VPS IP is a stable identifier. If your threat model is "root and TLD operators shouldn't be able to profile me", a personal recursive resolver is arguably worse than a large public one — QNAME minimisation reduces but does not remove this.
- Nothing about the traffic after DNS. IP addresses and SNI still reveal what you're visiting. Encrypted DNS is not a VPN and not Tor.
- No protection if the VPS itself is compromised or seized — cached queries live in RAM, and any logging you leave on is evidence.
The honest summary: self-hosted DNS moves trust from your ISP and a large resolver operator to yourself and your VPS provider. That's usually a good trade, especially on a No-KYC VPS paid for with crypto, but it's a move, not an elimination.
Recursive or forwarding — pick deliberately
Unbound can do two very different things:
Recursive (recommended): Unbound talks directly to root servers, TLDs and authoritative name servers. No third party sees your full query stream. Downsides: your VPS IP is exposed to authoritative operators, first-hit latency is higher, and some CDNs geolocate you to the VPS location rather than your real one.
Forwarding over DoT: Unbound encrypts queries to upstreams like Quad9 or dns0.eu. You get crowd-blending at the authoritative layer, but you're back to trusting a resolver operator with your complete query history — and since your VPS is the only client, that history is a single-user profile.
For a privacy-motivated setup, recursive is the coherent choice. Forwarding makes sense mainly when your VPS network filters outbound port 53, or you want a specific upstream's filtering.
Base install and configuration
On Debian/Ubuntu:
apt update && apt install unbound unbound-anchor dns-root-data
Free port 53 first if systemd-resolved is listening. Don't disable the whole service on distros that depend on it for local resolution — switch it to stub-less mode:
mkdir -p /etc/systemd/resolved.conf.d
cat >/etc/systemd/resolved.conf.d/no-stub.conf <<'EOF'
[Resolve]
DNSStubListener=no
DNS=127.0.0.1
EOF
systemctl restart systemd-resolved
Then /etc/unbound/unbound.conf.d/resolver.conf:
server:
verbosity: 0
use-syslog: no
logfile: ""
log-queries: no
log-replies: no
hide-identity: yes
hide-version: yes
interface: 127.0.0.1
interface: ::1
interface: 10.8.0.1 # WireGuard address
access-control: 0.0.0.0/0 refuse
access-control: ::/0 refuse
access-control: 127.0.0.0/8 allow
access-control: ::1 allow
access-control: 10.8.0.0/24 allow
do-ip4: yes
do-ip6: yes
prefer-ip6: no
# DNSSEC
auto-trust-anchor-file: "/var/lib/unbound/root.key"
harden-dnssec-stripped: yes
harden-glue: yes
harden-below-nxdomain: yes
val-clean-additional: yes
# privacy / correctness
qname-minimisation: yes
qname-minimisation-strict: no
aggressive-nsec: yes
rrset-roundrobin: yes
minimal-responses: yes
# abuse resistance
ratelimit: 1000
ip-ratelimit: 200
# cache
prefetch: yes
prefetch-key: yes
cache-min-ttl: 60
cache-max-ttl: 86400
msg-cache-size: 32m
rrset-cache-size: 64m
num-threads: 2
so-reuseport: yes
# local
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: fd00::/8
Validate and start:
unbound-checkconf
systemctl enable --now unbound
dig @127.0.0.1 example.com +dnssec | grep -E 'flags|status'
You want ad in the flags for signed zones. Test that validation actually fails closed:
dig @127.0.0.1 dnssec-failed.org # expect status: SERVFAIL
dig @127.0.0.1 sigfail.verteiltesysteme.net # expect SERVFAIL
If those return answers, DNSSEC isn't validating and you've gained less than you think.
Notes on the tuning above: qname-minimisation sends only the label needed at each delegation step, so root and TLD servers see com. rather than the full hostname. Strict mode breaks a handful of badly configured zones, so leave it off unless you're prepared to debug. cache-min-ttl: 60 reduces query volume on aggressively low-TTL CDN records at the cost of slightly staler answers — don't push it much higher or failover behaviour suffers. Avoid use-caps-for-id (0x20 encoding): it looks like a hardening win but breaks resolution for a nontrivial number of authoritative servers.
Do not run an open resolver
This is the mistake that gets VPS accounts suspended. An open recursive resolver reachable from the internet will be found within hours by scanners and used for DNS amplification DDoS. Your ACLs are the first layer, but rely on the firewall too:
# in your inet filter input chain
iif "lo" accept
iifname "wg0" udp dport 53 accept
iifname "wg0" tcp dport 53 accept
# no global port 53 accept rule at all
If you already run a baseline ruleset, this slots into the input chain described in the nftables firewall guide. Verify from outside the box, not from the box:
dig @your.vps.ip example.com +short # must time out or REFUSE
Free scanners such as openresolver checks are a useful second opinion, but a timeout from an unrelated network is sufficient proof.
The cleanest architecture is: Unbound listens only on 127.0.0.1 and the WireGuard interface; clients reach it through the tunnel. That gives you encrypted transport for free and no public DNS port at all. If you already run a self-hosted WireGuard VPN, point the client DNS = line at the server's tunnel IP and you're done.
Adding DNS-over-TLS for clients off the VPN
DoT (port 853) is useful for Android's "Private DNS" field and for devices where a full tunnel isn't practical. Unbound can serve it natively:
server:
interface: 0.0.0.0@853
interface: ::@853
access-control: 0.0.0.0/0 allow # required for DoT clients
tls-service-key: "/etc/unbound/tls/privkey.pem"
tls-service-pem: "/etc/unbound/tls/fullchain.pem"
tls-port: 853
Three practical points:
You need a hostname and a certificate. DoT clients validate the certificate against the name you configure. Getting a public certificate means the hostname appears in Certificate Transparency logs forever — a correlation vector worth understanding before you pick a domain. The trade-offs are covered in the guide to anonymous domain registration and private DNS.
Certificate permissions and chroot. Debian's Unbound runs chrooted in /var/lib/unbound as the unbound user. Either set chroot: "" or copy the certificate into the chroot with a renewal deploy hook, then chown unbound:unbound the key and chmod 640. Reload Unbound after renewal (systemctl reload unbound), otherwise DoT silently serves an expired cert.
Opening DoT to the world reintroduces abuse risk. Port 853 requires TCP and a TLS handshake, so it can't be used for reflection amplification the way UDP/53 can, but it is still an open resolver others can use. Keep ip-ratelimit in place, and if you only need a handful of devices, restrict source addresses in nftables or use Unbound's tls-cert-bundle-independent option of publishing the service only on an IPv6 address you don't advertise.
DoH (https-port: 443, http-endpoint: "/dns-query") works in Unbound 1.12+, but it collides with anything else on 443. Unless you specifically need browser-native DoH, DoT plus WireGuard is simpler to operate.
Filtering without a blocklist service
Unbound supports RPZ, which is a cleaner way to apply blocklists than generating tens of thousands of local-zone lines:
rpz:
name: "blocklist.local"
zonefile: "/var/lib/unbound/blocklist.rpz"
rpz-action-override: nxdomain
Refresh the zone file from a cron job or systemd timer and unbound-control reload. Keep expectations realistic: DNS blocking stops naive telemetry and most ad domains, but any app that hardcodes IPs or uses its own resolver walks straight past it.
Logging: leave it off, on purpose
The default Unbound config on most distros logs to syslog. If your goal is privacy, query logs on the resolver are the single most sensitive file on the machine. The config above sets verbosity: 0, log-queries: no and an empty logfile — meaning DNS data lives only in the in-memory cache. Consider val-log-level: 1 temporarily when debugging DNSSEC failures, then turn it back off. The same discipline applies to the rest of the box; see VPS log minimization for journald retention and shell history handling.
For operational visibility without query content, use unbound-control stats_noreset (enable the control interface on localhost). It gives cache hit rates, query types and SERVFAIL counts with no per-name data.
Common mistakes
- Leaving
systemd-resolved's stub listener on, then wondering why Unbound won't bind or why clients get answers from the wrong resolver. - Forgetting IPv6. If the client has working IPv6 and your DNS config only covers IPv4, the OS may fall back to a router-advertised resolver. Set both, or disable IPv6 on the client path deliberately.
- Testing from the server itself.
dig @127.0.0.1proves nothing about ACLs. Always verify externally. - Cache too small on a low-RAM box. Unbound with 64m+32m caches plus threads is fine on 1 GB, but combined with a database and PHP-FPM it can push you into OOM territory. Size it with the constraints in low-RAM VPS tuning in mind.
- Assuming a DNS leak test proves anonymity. Those tests show which resolver IP contacted the authoritative side. Seeing your own VPS IP means your setup works; it says nothing about SNI, IP-level exposure or browser fingerprinting.
- Single point of failure. If Unbound dies, everything behind the tunnel loses name resolution. Add a
Restart=on-failureoverride and a secondary resolver on clients that you're comfortable falling back to.
Pros and cons, honestly
Self-hosting is a clear win when you want to remove ISP and public-resolver visibility, control filtering, validate DNSSEC yourself, and cut a dependency on a third party. It's a poor fit if you need to blend into a large anonymity set at the authoritative layer, if you can't commit to maintaining certificates and monitoring the service, or if your real requirement is hiding what you connect to rather than what you look up. In that last case, Tor or a well-configured tunnel does more for you than any resolver change; the Tor onion service guide covers where those tools genuinely apply.
FAQ
Does a personal resolver make me more identifiable? At the authoritative layer, yes. Your VPS IP becomes a stable label attached to your query pattern. Against your ISP and local network, you're significantly more private. Choose based on which observer you care about.
Recursive or DoT-forwarding for best privacy? Recursive removes the resolver operator entirely. Forwarding preserves crowd-blending. Neither is universally better; recursive is more consistent with self-hosting as a principle.
Can I use this as my household DNS? Yes, over a tunnel. Never by opening UDP/53 to your home IP range — dynamic addresses and misconfiguration turn that into an open resolver quickly.
Where should the VPS be? Somewhere with sane network quality and a jurisdiction you're comfortable with. IronBalkans runs full-root KVM VPS in Romania (EU) with No-KYC signup and crypto-only billing, which fits this use case: the resolver only needs modest CPU and RAM, and you're not tying the server to an identity.
Takeaway
A validating recursive Unbound instance, bound to a WireGuard interface, with logging off and a firewall that refuses port 53 from the internet, is a small, maintainable setup that meaningfully reduces who sees your DNS queries. Add DoT only if you need off-VPN clients, and remember what remains visible: destination IPs, TLS SNI, and your resolver's own footprint at the authoritative layer. Build it for those reasons, not for a claim of anonymity it can't deliver.
