Overview
This guide walks you through setting up a private, secure DNS server using AdGuard Home (for ad-blocking and management) backed by Unbound (for recursive DNS resolution). This configuration enhances privacy by querying root DNS servers directly instead of relying on third-party providers.Prerequisites
VM Setup
We recommend Google Cloud Platform (GCP) for hosting, leveraging their free tier:- Google Cloud E2-micro Instance:
- Region:
us-east-1(or your preferred region) - Disk: 30GB Standard Persistent Disk
- OS: Debian 12 (Bookworm) or 13 (Trixie) x64
- Cost: Free tier eligible
- Region:
- Create VM and reserve a Static IP address
GCP Firewall Policies
Create the following firewall rules in your VPC network:Allow DNS Traffic
Rule Name:
allow-dns-traffic- Source IP ranges:
0.0.0.0/0 - Protocols/Ports: Allow
tcp:53andudp:53
Temporary Setup Access
Rule Name:
delete-later (Remove after setup)- Source IP ranges:
[YOUR HOME IP ADDRESS] - Protocols/Ports: Allow
tcp:3000(AdGuard Home initial setup)
Update Your VM
SSH into your new VM and prepare the system:Press
Y when prompted to confirm package upgrades. This may take a few minutes.Step 1: Install AdGuard Home
AdGuard Home provides the web interface, ad-blocking, and DNS query logging.Initial Configuration
- Navigate to
http://[YOUR_EXTERNAL_IP]:3000in your browser - Proceed through the setup wizard to Step 3
DNS Settings Configuration
Configure Settings → DNS Settings with these values:| Setting | Value | Why? |
|---|---|---|
| Upstream DNS servers | https://dns.cloudflare.com/dns-query | Temporary until Unbound is installed |
| Mode | ☑ Parallel requests | Best stability even with one upstream |
| Fallback DNS servers | https://dns.cloudflare.com/dns-query | Backup if primary fails |
| Rate limit | 15 | Limits queries per second per client (DoS protection) |
| Subnet prefix length (IPv4) | 32 | Tracks individual devices, not subnets |
| DNSSEC | ☑ Enable DNSSEC | Validates DNS responses aren’t tampered |
| IPv6 addresses | ☑ Disable resolving | Prevents timeouts if IPv6 isn’t configured |
| Setting | Value | Why? |
|---|---|---|
| Blocked response TTL | 86400 (24 hours) | How long clients cache blocked domains |
| Cache size | 20000000 bytes (≈19MB) | Initial cache; we’ll optimize later with Unbound |
| Override minimum TTL | 300 (5 minutes) | Prevents too-frequent re-queries |
| Override maximum TTL | 86400 (24 hours) | Limits stale data |
| Optimistic Caching | ☑ Enable | Serves cached data while refreshing in background |
We’ll reconfigure caching in Step 3 after installing Unbound for optimal performance.
Add Blocklists
Go to Filters → DNS Blocklists and add these lists:- hapara.fail Blocklist
- HaGeZi Normal
- Name:
hapara.fail Blocklist - URL:
https://raw.githubusercontent.com/hapara-fail/blocklist/main/blocklist.txt
Step 2: Set Up Unbound (Recursive Resolver)
Unbound replaces third-party DNS providers by querying root servers directly, enhancing privacy and control.Install Unbound
Configure Unbound
We’ll run Unbound on port5335 to avoid conflicts with AdGuard Home (which uses port 53).
-
Create the configuration file:
- Paste this optimized configuration:
- Save and exit (
Ctrl+O,Enter,Ctrl+X)
Download Root Hints
Root hints tell Unbound where the authoritative root DNS servers are:This cron job runs at midnight on the 1st of each month.
Start and Test Unbound
-
Restart the service:
-
Check service status:
✅ Success: You should see
active (running) -
Test DNS resolution:
✅ Success: Look for
status: NOERRORand an IP address in theANSWER SECTION
Step 3: Connect AdGuard Home to Unbound
Now we’ll point AdGuard Home to use your local Unbound instance instead of Cloudflare.Update Upstream DNS
- In AdGuard Home, go to Settings → DNS Settings
- Upstream DNS servers:
- Delete all existing entries
- Add only:
127.0.0.1:5335
- Parallel requests: Select Parallel requests (recommended for stability)
- Bootstrap DNS servers: These resolve IPs for DNS-over-HTTPS/TLS hostnames (not needed for our IP-based upstream, but good practice):
- Private Reverse DNS servers: (Optional) Point to your router if you want local hostname resolution (e.g.,
192.168.1.1) - DNSSEC: ☑ Enable (Unbound validates, but this provides a second check)
- Click Test Upstreams → Should show “Server is working” ✅
- Click Apply
Optimize Cache Settings
Since Unbound has superior caching with prefetching, we’ll minimize AdGuard’s cache:- Go to Settings → DNS Settings → DNS Cache Configuration
- Configure:
| Setting | Value | Why? |
|---|---|---|
| Cache size | 4194304 (4MB) | Small cache keeps UI responsive |
| Override minimum TTL | 0 (empty/default) | Let Unbound control TTL |
| Override maximum TTL | 0 (empty/default) | Let Unbound control TTL |
| Optimistic caching | ☐ Disable | Unbound’s prefetching is superior |
Step 4: Secure with SSL/TLS (DoH & DoT)
Enable encrypted DNS protocols: DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT).Install Certbot
Obtain SSL Certificate
Let’s Encrypt needs port 80 to verify domain ownership. We’ll temporarily stop AdGuard Home:Follow the Certbot prompts to enter your email and agree to the Terms of Service.
Configure Encryption in AdGuard
- Go to Settings → Encryption Settings
- Configure:
| Setting | Value |
|---|---|
| Enable Encryption | ☑ Checked |
| Server Name | example.org (your domain) |
| HTTPS Port | 443 (DNS-over-HTTPS) |
| DNS-over-TLS Port | 853 (DNS-over-TLS) |
| DNS-over-QUIC Port | 853 (DNS-over-QUIC, uses UDP) |
-
Certificate & Key Paths (⚠️ Use file paths, NOT file contents):
- Certificate path:
/etc/letsencrypt/live/example.org/fullchain.pem - Private key path:
/etc/letsencrypt/live/example.org/privkey.pem
- Certificate path:
- Click Save Settings
Automate Certificate Renewal
Create a post-renewal hook to restart AdGuard Home automatically:Certbot automatically renews certificates. This hook ensures AdGuard reloads the new certificate
without manual intervention.
Update GCP Firewall Rules
- Delete the temporary
delete-laterrule (port 3000 is no longer needed) - Create
allow-secure-dnsrule:- Source:
0.0.0.0/0 - TCP ports:
443,853 - UDP ports:
853
- Source:
Step 5: Protect with Fail2Ban
Prevent brute-force attacks on the AdGuard web interface.Install Fail2Ban
Create the Filter
This pattern matches failed login attempts in AdGuard’s logs:Create the Jail
Configure the ban policy:Enable and Start
Verify It’s Working
Setup Complete!
Your DNS server is now fully operational with:- Privacy: Direct recursive resolution via Unbound
- Security: DNSSEC validation, encrypted protocols (DoH/DoT/DoQ)
- Ad-blocking: Custom blocklists via AdGuard Home
- Protection: Fail2Ban guarding against brute-force attacks
- Automation: Auto-renewing SSL certificates
Need help? Check the AdGuard Home documentation or the Unbound
documentation.