Author | Nejat Hakan |
nejat.hakan@outlook.de | |
PayPal Me | https://paypal.me/nejathakan |
Recursive DNS Resolver Unbound
Introduction to Recursive DNS and Unbound
Welcome to this comprehensive guide on self-hosting your own recursive DNS resolver using Unbound. In an increasingly interconnected digital world, Domain Name System (DNS) resolution is a fundamental process that underpins nearly every online activity. While most users rely on DNS resolvers provided by their Internet Service Provider (ISP) or public services like Google DNS or Cloudflare DNS, self-hosting offers significant advantages in terms of privacy, security, control, and learning. This guide is designed for university students and enthusiasts who wish to delve deep into the workings of DNS and gain practical experience by setting up and managing their own Unbound resolver. We will journey from the basic concepts of DNS to advanced Unbound configurations, equipping you with the knowledge and skills to run a robust and efficient recursive DNS service.
What is DNS?
The Domain Name System (DNS) is often referred to as the "phonebook of the Internet." Its primary function is to translate human-readable domain names (like www.example.com
) into machine-readable Internet Protocol (IP) addresses (like 93.184.216.34
for IPv4 or 2606:2800:220:1:248:1893:25c8:1946
for IPv6). Without DNS, navigating the internet would require memorizing long strings of numbers for every website and service you wish to access, an obviously impractical task.
DNS is a hierarchical and distributed naming system for computers, services, or any resource connected to the Internet or a private network. It operates as a critical component of the internet infrastructure, enabling the seamless user experience we often take for granted.
Core Concepts (Domains, Zones, Records, etc.)
To understand DNS, several core concepts are essential:
- Domain Name:
A human-friendly name used to identify a resource on the internet (e.g.,google.com
,wikipedia.org
). Domain names are structured hierarchically. - IP Address:
A numerical label assigned to each device participating in a computer network that uses the Internet Protocol for communication. IPv4 addresses (e.g.,192.168.1.1
) are 32-bit, while IPv6 addresses (e.g.,2001:0db8:85a3:0000:0000:8a2e:0370:7334
) are 128-bit. - DNS Server (Name Server):
A server that stores DNS records and responds to DNS queries from clients. There are different types of DNS servers, including recursive resolvers and authoritative name servers. - DNS Client (Resolver):
A program or part of an operating system that makes DNS queries. Your web browser, email client, and even your operating system itself act as DNS clients when they need to resolve a domain name. - DNS Zone:
A distinct portion of the domain name space that is managed by a specific organization or administrator. A zone contains DNS records for the domains within it. For example,example.com
could be a zone, containing records forwww.example.com
,mail.example.com
, etc. - DNS Record:
A piece of information within a DNS zone file that provides mapping between a domain name and other data. Records have a type (e.g., A, AAAA, MX, CNAME), a name (the domain name they refer to), a Time-To-Live (TTL), and data (e.g., an IP address). - Time-To-Live (TTL):
A value in a DNS record that specifies how long (in seconds) a resolver is allowed to cache the information for that record. After the TTL expires, the resolver must query an authoritative server again for the record. This mechanism helps ensure that changes to DNS records propagate throughout the internet. - FQDN (Fully Qualified Domain Name):
A domain name that specifies its exact location in the tree hierarchy of the DNS. It includes all domain levels, including the top-level domain and usually the root zone. For example,www.example.com.
(note the trailing dot) is an FQDN. The trailing dot signifies the root of the DNS hierarchy.
The DNS Hierarchy (Root, TLD, Authoritative Servers)
The DNS is structured as a hierarchical tree, similar to a file system.
- Root Zone:
At the very top of the hierarchy is the root zone, represented by a single dot (.
). The root zone contains information about the Top-Level Domain (TLD) servers. There are 13 logical root name server clusters (named A through M), geographically distributed around the world for resilience and performance. These servers are managed by various organizations under the coordination of ICANN (Internet Corporation for Assigned Names and Numbers). - Top-Level Domains (TLDs):
Below the root are the TLDs. These are the last part of a domain name, such as.com
,.org
,.net
,.gov
,.edu
, and country-code TLDs (ccTLDs) like.uk
,.de
,.jp
. TLD servers hold information about the authoritative name servers for second-level domains (e.g.,example.com
).- gTLDs (Generic TLDs):
Like.com
,.org
,.info
. - ccTLDs (Country Code TLDs):
Like.us
,.ca
,.fr
. - sTLDs (Sponsored TLDs):
Like.gov
,.edu
. - New gTLDs:
A large number of new gTLDs have been introduced, such as.app
,.tech
,.blog
.
- gTLDs (Generic TLDs):
- Second-Level Domains (SLDs):
These are the names directly to the left of the TLD, likeexample
inexample.com
. Organizations or individuals typically register these. - Subdomains:
Further subdivisions of a domain, likewww
inwww.example.com
orapi.service.example.com
. The owner of an SLD can create and manage subdomains within their zone. - Authoritative Name Servers:
For any given zone (e.g.,example.com
), there are specific DNS servers designated as "authoritative" for that zone. These servers hold the master copies of the DNS records for that zone and are the ultimate source of truth for information about domains within that zone. When a recursive resolver needs to find the IP address forwww.example.com
, it will eventually query one of the authoritative name servers forexample.com
.
DNS Query Types (Records)
DNS uses various record types to store different kinds of information. Some of the most common types include:
- A (Address Mapping) Record:
Maps a hostname to an IPv4 address.- Example:
www.example.com. IN A 93.184.216.34
- Example:
- AAAA (IPv6 Address) Record:
Maps a hostname to an IPv6 address.- Example:
www.example.com. IN AAAA 2606:2800:220:1:248:1893:25c8:1946
- Example:
- CNAME (Canonical Name) Record:
Creates an alias from one domain name to another (the "canonical" name). Queries for the alias will resolve to the IP address of the canonical name.- Example:
ftp.example.com. IN CNAME www.example.com.
- Example:
- MX (Mail Exchange) Record:
Specifies the mail server(s) responsible for accepting email messages on behalf of a domain. It includes a preference value to indicate priority if multiple mail servers are listed.- Example:
example.com. IN MX 10 mail.example.com.
- Example:
- NS (Name Server) Record:
Delegates a DNS zone to use the given authoritative name servers.- Example:
example.com. IN NS ns1.auth-server.com.
- Example:
- PTR (Pointer) Record:
Used for reverse DNS lookups, mapping an IP address back to a hostname. These are primarily found in reverse lookup zones likein-addr.arpa
(for IPv4) andip6.arpa
(for IPv6).- Example:
34.216.184.93.in-addr.arpa. IN PTR www.example.com.
- Example:
- SOA (Start of Authority) Record:
Contains administrative information about a DNS zone, including the primary name server, email of the domain administrator, domain serial number, and timers relating to refreshing the zone. Every zone must have an SOA record. - TXT (Text) Record:
Allows an administrator to insert arbitrary text into a DNS record. Often used for verification purposes (e.g., domain ownership verification for SSL certificates, Google Search Console), Sender Policy Framework (SPF), DomainKeys Identified Mail (DKIM), and DMARC records for email authentication. - SRV (Service) Record:
Specifies the hostname and port number of servers for specified services, rather than a simple IP address. Used for protocols like SIP, XMPP, and LDAP.- Example:
_sip._tcp.example.com. IN SRV 10 60 5060 bigbox.example.com.
(Priority 10, Weight 60, Port 5060, Target bigbox.example.com)
- Example:
Recursive vs. Authoritative DNS Servers
It's crucial to distinguish between two primary types of DNS servers involved in the resolution process: recursive resolvers and authoritative name servers.
The Role of a Recursive Resolver
A recursive DNS resolver (also known as a DNS recursor or full-service resolver) is the server that your computer or device queries when it needs to resolve a domain name. Its job is to find the answer to your query, even if it means querying multiple other DNS servers across the internet.
When a recursive resolver receives a query from a client (e.g., your browser asking for the IP address of www.example.com
):
- Cache Check:
It first checks its local cache. If it has recently resolved this domain name and the information (the DNS record) is still valid (within its TTL), it returns the answer directly from its cache. This is fast and efficient. - Recursive Resolution:
If the information is not in its cache or has expired, the recursive resolver will perform the full resolution process. This involves:- Querying one of the root name servers.
- The root server will respond with a referral to the TLD servers for the TLD in the query (e.g.,
.com
servers). - The recursive resolver then queries one of these TLD servers.
- The TLD server will respond with a referral to the authoritative name servers for the specific domain (e.g.,
example.com
servers). - Finally, the recursive resolver queries one of these authoritative name servers.
- The authoritative name server provides the actual IP address (or other requested record) for the domain.
- Caching the Result:
The recursive resolver caches the received answer (respecting its TTL) for future queries. - Responding to the Client:
The recursive resolver returns the answer to the original client.
Recursive resolvers are typically operated by ISPs, public DNS providers (Google, Cloudflare, Quad9), or, as we'll learn, can be self-hosted. Unbound is a recursive DNS resolver.
The Role of an Authoritative Server
An authoritative DNS server is responsible for holding the definitive DNS records for a specific domain or set of domains (a zone). It "owns" the DNS records for that zone and provides answers to queries about those records. It does not perform recursive lookups for other domains.
When an authoritative server receives a query for a domain it is authoritative for:
- It checks its zone data for the requested record.
- If the record exists, it returns the record.
- If the record does not exist for a domain it is authoritative for, it returns an
NXDOMAIN
(Non-Existent Domain) response. - If it receives a query for a domain it is not authoritative for, it typically returns a referral or an error. It does not try to find the answer by querying other servers.
For example, the company that owns example.com
would configure its authoritative name servers with all the A, AAAA, MX, CNAME, etc., records for example.com
and its subdomains.
The DNS Resolution Process (Step-by-Step)
Let's trace the typical journey of a DNS query when you type www.example.com
into your browser, assuming the recursive resolver has no cached information:
- Client Query:
Your computer (the DNS client) sends a DNS query forwww.example.com
to its configured recursive DNS resolver (e.g., your Unbound server, your ISP's resolver, or 8.8.8.8). - Recursive Resolver to Root Servers:
The recursive resolver, not knowingwww.example.com
, starts by querying one of the DNS root name servers. "Where can I find information about.com
?" (The root servers' IP addresses are pre-configured in the resolver via a "root hints" file). - Root Server Response:
A root server responds with a list of IP addresses for the TLD name servers responsible for the.com
domain. It doesn't know wherewww.example.com
is, but it knows who manages.com
. - Recursive Resolver to TLD Servers:
The recursive resolver then queries one of the.com
TLD name servers. "Where can I find information aboutexample.com
?" - TLD Server Response:
The.com
TLD server responds with a list of IP addresses for the authoritative name servers for theexample.com
domain. It doesn't knowwww.example.com
, but it knows who managesexample.com
. - Recursive Resolver to Authoritative Servers:
The recursive resolver now queries one of the authoritative name servers forexample.com
. "What is the A record (IP address) forwww.example.com
?" - Authoritative Server Response:
The authoritative name server forexample.com
checks its zone files, finds the A record forwww.example.com
, and returns the IP address (e.g.,93.184.216.34
) to the recursive resolver. - Recursive Resolver Caches and Responds to Client:
The recursive resolver stores this IP address in its cache (with the associated TTL) and sends the IP address back to your computer. - Client Accesses Website: Your computer now has the IP address for
www.example.com
and your browser can establish a TCP connection to that IP address to fetch the website content.
This entire process, while involving multiple steps, usually happens in milliseconds. Caching at each level (client OS, recursive resolver) significantly speeds up subsequent requests for the same or related domains.
Why Self-Host a Recursive DNS Resolver?
While using your ISP's DNS or public DNS services is convenient, self-hosting your own recursive resolver like Unbound offers compelling advantages:
Privacy Benefits
- Reduced Data Collection: When you use a third-party DNS resolver, that provider can see every domain name you query. This data can be logged, analyzed, and potentially sold or shared. By running your own resolver, your DNS query history stays on your own server, significantly enhancing your online privacy. You control the logs, or if logging is even enabled.
- No Third-Party Profiling: Your DNS queries reveal a lot about your browsing habits, interests, and online activities. Self-hosting prevents third-party DNS providers from building a profile based on this data.
Security Enhancements
- DNSSEC Validation: Unbound performs DNSSEC (DNS Security Extensions) validation by default. DNSSEC helps protect against DNS spoofing and cache poisoning attacks by cryptographically verifying the authenticity and integrity of DNS responses. While many public resolvers also perform DNSSEC validation, self-hosting gives you direct control and assurance.
- QNAME Minimisation: Unbound implements QNAME minimisation (RFC 7816) to reduce the amount of query data sent to upstream DNS servers. Instead of sending the full domain name (e.g.,
www.example.com
) to each server in the resolution chain, it only sends the necessary part. For example, it asks the root server only about.com
, then the.com
server only aboutexample.com
, and so on. This further enhances privacy. - Control Over Blocklists/Filtering: You can configure Unbound to block access to malicious domains, ad servers, or trackers by providing custom DNS responses (e.g., redirecting them to a non-existent IP address). This gives you network-level ad-blocking and malware protection.
- Resistance to Local Network Snooping/Manipulation: If you trust your own resolver on your local network or a secure server, you are less susceptible to DNS manipulation attacks originating from within a compromised local network (e.g., a malicious actor on a public Wi-Fi).
Performance Considerations
- Local Caching: A self-hosted resolver, especially one on your local network, can provide very fast responses for frequently accessed domains due to its local cache. The latency to your local resolver is minimal compared to querying an external server.
- Tuned for Your Needs: You can fine-tune Unbound's caching parameters, prefetching behavior, and other performance settings to match your specific usage patterns and network conditions.
- Reduced Dependency on ISP or Public Resolvers: If your ISP's DNS servers are slow or unreliable, or if a public resolver experiences an outage, your self-hosted resolver can continue to function independently (as long as it can reach the root and authoritative servers).
Learning and Control
- Deep Understanding of DNS: Setting up and managing your own resolver is an excellent way to learn the intricacies of DNS, network configuration, and server administration.
- Full Control: You have complete control over the software, configuration, logging, and security policies of your DNS resolver. You decide what features to enable, how data is handled, and who can use your resolver.
- Customization: You can implement custom DNS records for your local network (e.g., resolving
my-nas.local
to an internal IP address), set up split-horizon DNS, or integrate Unbound with other services.
Introducing Unbound
Now that we understand the "why," let's introduce the "what." Unbound is the software we will be focusing on for self-hosting our recursive DNS resolver.
What is Unbound?
Unbound is a validating, recursive, and caching DNS resolver. It is open-source software, primarily developed by NLnet Labs, the same organization behind NSD (an authoritative DNS server) and other important internet infrastructure software. Unbound is designed with a strong focus on security, performance, and modern standards. It is widely used by individuals, enterprises, and even large ISPs.
Key Features of Unbound
Unbound boasts a rich feature set, making it an excellent choice for a self-hosted resolver:
- Security-Focused Design:
- DNSSEC Validation: Robust and compliant DNSSEC validation is a core feature, ensuring data integrity and authenticity.
- QNAME Minimisation (RFC 7816): Enhances privacy by minimizing the query data sent to upstream servers.
- Aggressive NSEC (RFC 8198): Uses NSEC/NSEC3 records to synthesize negative answers (NXDOMAIN) from the cache, reducing queries and improving performance for non-existent domains.
- Resilience against DoS attacks: Designed to be robust against various forms of denial-of-service attacks.
- Chroot and Privilege Dropping: Can run in a chroot jail with dropped privileges for enhanced security.
- High Performance:
- Efficient Caching: Sophisticated caching mechanisms for RRsets (Resource Record sets) and messages.
- Prefetching: Proactively refreshes popular cache entries before they expire, improving perceived performance.
- Serve Expired: Can serve expired records from cache if authoritative servers are unreachable, improving availability.
- Multi-threaded Architecture: Leverages multiple CPU cores for concurrent query processing.
- Standards Compliance: Adheres to relevant DNS standards (RFCs).
- Modularity and Extensibility:
- Python Module: Allows for extending Unbound's functionality with custom Python scripts (e.g., for advanced filtering, logging, or query manipulation).
- Libunbound: A library version of Unbound that can be integrated into other applications.
- Flexible Configuration: Offers a wide range of configuration options to fine-tune its behavior.
- Modern Protocol Support:
- DNS-over-TLS (DoT): Can act as a DoT client (forwarding queries over TLS) and as a DoT server (accepting queries over TLS).
- DNS-over-HTTPS (DoH): Can act as a DoH client and, with a reverse proxy, as a DoH server.
- IPv6 Support: Full support for IPv6, both for transport and for resolving AAAA records.
- Local DNS Features: Supports defining local zones and data, allowing for custom internal DNS records, overrides, and ad-blocking.
History and Development
Unbound was first released in 2008. It was designed from the ground up as a modern recursive resolver with a strong emphasis on DNSSEC validation. Its development was motivated by the need for a secure, open-source alternative to BIND (Berkeley Internet Name Domain), which, while powerful, has a larger codebase and historically had more security vulnerabilities. Unbound's smaller, more focused codebase contributes to its security and stability. It is actively maintained and developed by NLnet Labs, with contributions from the community.
Workshop Understanding DNS with dig
and nslookup
Before we install Unbound, let's get our hands dirty with some common DNS lookup utilities: dig
(Domain Information Groper) and nslookup
(Name Server Lookup). These tools allow you to query DNS servers directly and inspect the responses, providing invaluable insight into how DNS works. dig
is generally preferred for its more detailed output and flexibility.
Prerequisites
- A computer with internet access.
- A terminal or command prompt.
Installing dig
and nslookup
These tools are often part of a larger package, typically named dnsutils
or bind-utils
depending on your Linux distribution.
-
On Debian/Ubuntu:
-
On CentOS/RHEL/Fedora:
-
On macOS:
dig
andnslookup
are usually pre-installed. If not, they can be installed with Homebrew:brew install bind
. - On Windows:
nslookup
is built-in.dig
can be obtained by downloading BIND for Windows from the ISC (Internet Systems Consortium) website and extractingdig.exe
and its required DLLs, or by using the Windows Subsystem for Linux (WSL).
Step 1: Basic A Record Query
Let's find the IPv4 address for www.google.com
.
-
Using
dig
:Expected Output (simplified):
;; QUESTION SECTION: ;www.google.com. IN A ;; ANSWER SECTION: www.google.com. 200 IN A 142.250.180.132 ;; Query time: 15 msec ;; SERVER: 192.168.1.1#53(192.168.1.1) <-- This is your current default resolver ;; WHEN: Mon Jun 20 10:00:00 PDT 2023 ;; MSG SIZE rcvd: 60
Explanation:
QUESTION SECTION
: Shows what we asked for (A
record forwww.google.com
).ANSWER SECTION
: Provides the answer.www.google.com.
has anA
record with the IP142.250.180.132
.200
is the TTL (Time-To-Live) in seconds.SERVER
: Shows which DNS server answered your query. This is likely your router, ISP's DNS, or a public DNS server configured on your system.
-
Using
nslookup
:Expected Output (simplified):
Server: 192.168.1.1 Address: 192.168.1.1#53 Non-authoritative answer: Name: www.google.com Address: 142.250.180.132
nslookup
by default queries for A and AAAA records. "Non-authoritative answer" means the answer came from a recursive resolver's cache, not directly from Google's authoritative name server.
Step 2: Querying for Other Record Types (AAAA, MX)
-
AAAA Record (IPv6):
Look for an AAAA record in theANSWER SECTION
. -
MX Record (Mail Exchange):
Expected Output (simplified):
This shows mail servers for;; ANSWER SECTION: google.com. 245 IN MX 10 smtp.google.com. google.com. 245 IN MX 20 alt1.smtp.google.com. google.com. 245 IN MX 30 alt2.smtp.google.com. ...
google.com
with their preference values (lower is more preferred).
Step 3: Querying a Specific DNS Server
You can tell dig
or nslookup
to use a specific DNS server instead of your system's default. Let's query Cloudflare's public DNS server (1.1.1.1
).
-
Using
Thedig
:@1.1.1.1
part tellsdig
to send the query to that server. -
Using
nslookup
:
Step 4: Tracing a DNS Query with +trace
The +trace
option in dig
is incredibly useful for understanding the recursive resolution process. It simulates what a recursive resolver does by querying root servers, then TLDs, then authoritative servers.
Expected Output (abbreviated and annotated):
; <<>> DiG 9.18.12-0ubuntu0.22.04.3-Ubuntu <<>> +trace www.wikipedia.org
;; global options: +cmd
. 518400 IN NS m.root-servers.net. <-- Hint from root hints file
. 518400 IN NS a.root-servers.net.
... (other root servers) ...
;; Received 239 bytes from 192.168.1.1#53(192.168.1.1) in 21 ms <-- Your local resolver gave root hints
org. 172800 IN NS a0.org.afilias-nst.info. <-- Querying a root server for .org NS
org. 172800 IN NS a2.org.afilias-nst.info.
... (other .org TLD servers) ...
;; Received 799 bytes from m.root-servers.net#53(202.12.27.33) in 50 ms <-- Response from a root server
wikipedia.org. 86400 IN NS ns0.wikimedia.org. <-- Querying an .org TLD server for wikipedia.org NS
wikipedia.org. 86400 IN NS ns1.wikimedia.org.
wikipedia.org. 86400 IN NS ns2.wikimedia.org.
;; Received 205 bytes from a0.org.afilias-nst.info#53(199.19.56.1) in 120 ms <-- Response from .org TLD server
www.wikipedia.org. 3600 IN CNAME dyna.wikimedia.org. <-- Querying wikimedia.org NS for www.wikipedia.org
dyna.wikimedia.org. 600 IN A 91.198.174.192
;; Received 103 bytes from ns0.wikimedia.org#53(208.80.154.238) in 70 ms <-- Response from authoritative server
dig +trace
follows the chain from root servers down to the authoritative name servers for wikipedia.org
. It first asks a root server for .org
servers, then asks an .org
server for wikipedia.org
servers, and finally asks a wikimedia.org
(authoritative for wikipedia.org
) server for www.wikipedia.org
. Notice the CNAME record; www.wikipedia.org
is an alias for dyna.wikimedia.org
, which then has an A record.
Step 5: Examining DNS Record Details (TTL, +short)
- Look at the TTL (second column in the answer section for many records from
dig
). Query the same record again before the TTL expires; the query might be faster if served from a cache. - Use
dig +short
for a concise answer (often just the IP address or target name): Output:
Step 6: Reverse DNS Lookup (PTR Record)
Let's find the hostname associated with an IP address. For this, we use a specially formatted name in the in-addr.arpa
(for IPv4) or ip6.arpa
(for IPv6) domain. dig -x
simplifies this.
Expected Output (simplified):
This shows that8.8.8.8
is associated with the hostname dns.google
.
This workshop should give you a practical feel for DNS queries. As we proceed to set up Unbound, you'll use these tools to test your own resolver. Understanding their output is key to diagnosing DNS issues and verifying your Unbound configuration.
1. System Preparation and Installation
With a foundational understanding of DNS and the benefits of self-hosting a recursive resolver like Unbound, we can now move to the practical steps of preparing our system and installing the Unbound software. This section will primarily focus on Linux-based systems, as they are commonly used for server applications. We will cover popular distributions like Debian/Ubuntu and CentOS/RHEL.
Choosing an Operating System
Unbound is designed to be portable and can run on a wide variety of operating systems, including Linux, FreeBSD, OpenBSD, macOS, and even Windows (though less common for server deployments). For self-hosting a dedicated resolver, a stable Linux distribution is an excellent choice.
- Debian or Ubuntu Server: These are popular choices due to their large communities, extensive documentation, and robust package management systems (APT). They often have recent versions of Unbound in their repositories.
- CentOS Stream / RHEL / AlmaLinux / Rocky Linux: These are known for their stability and long-term support, making them suitable for enterprise environments or servers where stability is paramount. They use YUM or DNF for package management.
- Other Linux Distributions: Arch Linux, Fedora Server, etc., are also viable options, each with its own strengths. The installation process will be similar, mainly differing in package manager commands.
- FreeBSD/OpenBSD: These operating systems are renowned for their security and networking capabilities. Unbound is often well-supported and readily available in their ports/packages systems.
For the examples in this guide, we will primarily use commands suitable for Debian/Ubuntu, but we will also provide equivalents for CentOS/RHEL-based systems where applicable. The underlying principles and Unbound configuration remain largely the same across different Linux distributions.
Hardware Considerations: Unbound is lightweight. For a personal or small network resolver, modest resources are sufficient:
- CPU: 1 core is often enough, but 2 cores can help with responsiveness under load.
- RAM: 128-256 MB RAM is a good starting point for basic use. For larger caches or heavier query loads, 512MB to 1GB+ might be beneficial. Unbound's memory usage is configurable (e.g., cache sizes).
- Disk Space: A few hundred MBs for the OS, Unbound, and logs. The root hints file is tiny. A Raspberry Pi (Model 3B+ or newer), a small Virtual Private Server (VPS), or a virtual machine (VM) can easily run Unbound.
Updating System Packages
Before installing any new software, it's a crucial best practice to update your system's package list and upgrade existing packages. This ensures you have the latest security patches and software versions.
-
On Debian/Ubuntu:
sudo apt update
: Refreshes the list of available packages from the repositories defined in your system's sources.sudo apt upgrade -y
: Upgrades all currently installed packages to their newest versions. The-y
flag automatically answers "yes" to prompts.
-
On CentOS/RHEL/AlmaLinux/Rocky Linux (using DNF, common in newer versions):
sudo dnf check-update
: Checks for available updates.sudo dnf upgrade -y
: Upgrades all packages with available updates.
-
On older CentOS/RHEL (using YUM):
It's also a good idea to reboot your server if the upgrade included a kernel update or other critical system components:
Wait for the system to come back online and reconnect.Installing Unbound
Unbound can be installed either from your distribution's pre-compiled binary packages (the easiest and recommended method for most users) or by compiling it from source (which offers more control but is more complex).
From Package Managers (apt, yum/dnf)
This is the most straightforward method.
-
On Debian/Ubuntu: Unbound is available in the default repositories.
This command will download and install Unbound and its dependencies. -
On CentOS/RHEL/AlmaLinux/Rocky Linux: Unbound is typically available in the EPEL (Extra Packages for Enterprise Linux) repository or base repositories depending on the version. First, ensure the EPEL repository is enabled if needed (it often is by default on newer systems, or you might need to install
For older CentOS/RHEL 7 (using YUM):epel-release
). For CentOS/RHEL 8+, AlmaLinux, Rocky Linux (using DNF):
Compiling from Source (Brief Overview and When to Consider)
Compiling Unbound from source gives you access to the very latest version (which might not be in your distribution's repositories yet) and allows you to enable or disable specific features at compile time. However, it requires more effort and means you are responsible for updates and managing dependencies manually.
When to consider compiling from source:
- You need a feature present only in the newest Unbound version.
- You have specific performance or security requirements that necessitate custom compile-time options.
- Your distribution's version is very old and unsupported.
General steps to compile from source (example):
- Install build dependencies: You'll need a C compiler (like GCC), make, libssl-dev (OpenSSL development files), libexpat1-dev (for XML parsing, if enabling certain features), etc. The exact package names vary by distribution.
- Debian/Ubuntu:
sudo apt install build-essential libssl-dev libexpat1-dev
- CentOS/RHEL:
sudo yum groupinstall "Development Tools"
thensudo yum install openssl-devel expat-devel
- Debian/Ubuntu:
- Download the source code: Get the latest tarball from the NLnet Labs Unbound website.
-
Configure the build:
./configure --prefix=/usr/local --sysconfdir=/etc/unbound --with-ssl=/usr/bin/openssl --enable-dnstap # Review available options with ./configure --help
--prefix
: Installation directory.--sysconfdir
: Location for configuration files.--with-ssl
: Path to OpenSSL.--enable-dnstap
: Example of enabling an optional feature.- Compile:
- Install:
- Post-installation: You'll need to set up a systemd service file or init script manually, create the Unbound user/group, and set up directories like
/etc/unbound
.
For most users, especially those starting out, installing from the distribution's package manager is highly recommended.
Initial Unbound Service Check
Once Unbound is installed (typically from a package manager), it usually sets up a system service that can be managed by systemd
(on most modern Linux distributions) or an older init system.
Let's check the status of the Unbound service:
-
Using
systemd
:Expected Output (if running):
● unbound.service - Unbound DNS server Loaded: loaded (/lib/systemd/system/unbound.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2023-06-20 10:30:00 PDT; 5min ago Docs: man:unbound(8) Main PID: 1234 (unbound) Tasks: 1 (limit: 4661) Memory: 15.0M CPU: 100ms CGroup: /system.slice/unbound.service └─1234 /usr/sbin/unbound -d -p
Active: active (running)
: Indicates the service is running.Loaded: ... enabled; ...
: Indicates the service is set to start automatically on boot.
If it's not running (
And enable it to start on boot:Active: inactive (dead)
orActive: failed
), you can try to start it: -
Using older init systems (e.g., SysVinit on very old systems):
The default configuration provided by the package manager usually allows Unbound to start and function as a basic caching resolver, typically listening only on localhost
(127.0.0.1
) for security reasons. We will customize this configuration extensively in the next sections.
Workshop Installing Unbound on a Linux Server
This workshop will guide you through provisioning a basic Linux server (as a Virtual Machine for local testing or a cloud VM) and installing Unbound using the package manager.
Prerequisites
- Virtualization software (e.g., VirtualBox, VMware Workstation Player/Fusion) if creating a local VM, OR an account with a cloud provider (e.g., AWS, Google Cloud, Azure, DigitalOcean, Linode, Vultr) if using a cloud VM.
- An SSH client (OpenSSH on Linux/macOS, PuTTY or Windows Terminal with SSH on Windows).
Step 1: Provisioning a Virtual Machine or Cloud Server
Option A: Local Virtual Machine (using VirtualBox as an example)
- Download an ISO: Download a server ISO image for your chosen Linux distribution (e.g., Ubuntu Server 22.04 LTS from ubuntu.com or AlmaLinux 9 from almalinux.org).
- Create a New VM in VirtualBox:
- Open VirtualBox.
- Click "New".
- Name: e.g., "Unbound DNS Server"
- Type: Linux
- Version: Select the appropriate version (e.g., "Ubuntu (64-bit)" or "Red Hat (64-bit)").
- Memory size: Allocate at least 512 MB, preferably 1024 MB (1 GB).
- Hard disk: Create a virtual hard disk now, VDI type, dynamically allocated, size 10-20 GB.
- Configure Network:
- Select your new VM, click "Settings".
- Go to "Network".
- Adapter 1: Attached to "Bridged Adapter" (this allows the VM to get an IP address from your local network, making it accessible like any other device) or "NAT" (simpler, but requires port forwarding for external access, not needed for this initial setup). If using Bridged, select your active host network interface.
- Install the OS:
- Start the VM.
- It will ask for a startup disk; select the ISO image you downloaded.
- Follow the on-screen instructions to install the Linux distribution.
- Choose a minimal server installation.
- Set a hostname (e.g.,
unbound-server
). - Create a user account with a strong password.
- During installation, ensure the "OpenSSH server" package is selected for installation if prompted (it often is by default on server editions).
- Get IP Address: Once installed and rebooted, log in to the VM's console. Find its IP address:
Look for the IP address associated with an interface like
eth0
,ens33
, orenp0s3
.
Option B: Cloud Virtual Machine (using a generic cloud provider example)
- Sign up/Log in: Access your cloud provider's console.
- Launch a New Instance/VM/Droplet:
- Choose a region closest to you.
- Select an OS image (e.g., Ubuntu 22.04 LTS, AlmaLinux 9).
- Choose the smallest/cheapest instance size that meets the minimum requirements (1 vCPU, 512MB-1GB RAM).
- Configure SSH keys for access (highly recommended) or a root password (less secure, change it immediately).
- Ensure the security group/firewall rules allow inbound SSH traffic (TCP port 22) from your IP address.
- Note the Public IP Address: The cloud provider will assign a public IP address to your new server.
Step 2: Connecting to the Server via SSH
Once your server is running and you have its IP address:
-
From Linux/macOS:
(e.g.,ssh myuser@192.168.1.105
orssh myuser@203.0.113.50
) If you used SSH keys with a cloud VM, it might bessh -i /path/to/your/private_key user@server_ip_address
. -
From Windows (using PuTTY):
- Enter the server's IP address in the "Host Name (or IP address)" field.
- Ensure "Port" is 22 and "Connection type" is SSH.
- Click "Open".
- Accept the server's host key if prompted.
- Enter your username and password.
-
From Windows (using Windows Terminal/PowerShell with OpenSSH client): Similar to Linux/macOS:
ssh your_username@server_ip_address
.
Step 3: Updating the System
Once connected to your server via SSH, update the package lists and upgrade installed packages.
-
For Debian/Ubuntu:
If you rebooted, reconnect via SSH. -
For CentOS/RHEL/AlmaLinux/Rocky Linux:
If you rebooted, reconnect via SSH.
Step 4: Installing Unbound
Now, install Unbound using the package manager.
-
For Debian/Ubuntu:
-
For CentOS/RHEL/AlmaLinux/Rocky Linux: If
Then install Unbound:epel-release
is needed and not installed (common on minimal RHEL-derivatives):
Step 5: Verifying the Installation and Service Status
Check if Unbound installed correctly and if the service is running and enabled.
You should seeActive: active (running)
and Loaded: ... enabled; ...
.
If it's not active, try:
sudo systemctl start unbound
sudo systemctl enable unbound
sudo systemctl status unbound # Check again
You can also check the Unbound version:
This output will show the version and compiled-in features like DNSSEC validation support.Congratulations! You have successfully prepared a Linux server and installed Unbound. In the next sections, we will dive into configuring it to become your personalized recursive DNS resolver.
2. Core Unbound Configuration Concepts
After successfully installing Unbound, the next crucial step is to understand its configuration. Unbound is highly customizable through its main configuration file, typically named unbound.conf
. This section will introduce you to the structure of this file, key configuration blocks, and important directives that control Unbound's behavior.
Understanding unbound.conf
The primary configuration file for Unbound is usually located at /etc/unbound/unbound.conf
on most Linux systems installed via package managers. If you compiled from source, the location might differ based on the --sysconfdir
option you used during configuration (e.g., /usr/local/etc/unbound/unbound.conf
).
The unbound.conf
file uses a simple syntax:
- Lines starting with
#
are comments and are ignored. - Configuration is organized into blocks, with the main block being
server:
. - Directives are specified as
directive: value
. - Some directives can appear multiple times (e.g.,
access-control:
). - Whitespace (spaces, tabs) is generally used for readability but is often flexible. Indentation is not strictly required by the parser but helps in organizing the file.
- String values containing spaces should usually be enclosed in double quotes (
"
).
It's highly recommended to back up the default unbound.conf
file before making any changes:
After modifying the configuration file, you need to tell Unbound to reload or restart to apply the changes. Before doing so, it's wise to check the configuration for syntax errors:
If the output isunbound-checkconf: no errors in /etc/unbound/unbound.conf
, your syntax is correct. Otherwise, it will point you to the line with the error.
Key Configuration Blocks
While the server:
block contains most global settings, Unbound's configuration can be organized into several distinct blocks. Some of these might be included from separate files for better organization, especially in complex setups.
server:
block (General server settings)
This is the main and most important block. It contains directives that control the overall behavior of the Unbound daemon, such as listening interfaces, port numbers, user identity, logging, security features, and caching parameters. Almost all general settings reside here.
Example structure:
server:
# Server behavior directives
verbosity: 1
interface: 127.0.0.1
port: 53
# ... many more directives
remote-control:
block (For unbound-control
)
This block configures the remote control facility for Unbound, which allows you to manage the server (e.g., reload, get stats, dump cache) using the unbound-control
utility. This is essential for administration without restarting the entire service.
Example structure:
remote-control:
# Enable remote control
control-enable: yes
# Interface for control commands (e.g., localhost only)
control-interface: 127.0.0.1
# Port for control commands
# control-port: 8953 (default)
# Server key and cert files for secure communication
# server-key-file: "/etc/unbound/unbound_server.key"
# server-cert-file: "/etc/unbound/unbound_server.pem"
# Control key and cert files for unbound-control
# control-key-file: "/etc/unbound/unbound_control.key"
# control-cert-file: "/etc/unbound/unbound_control.pem"
unbound-control
securely, you typically run sudo unbound-control-setup
. This command generates the necessary SSL/TLS keys and certificates and places them in the Unbound configuration directory (e.g., /etc/unbound/
). It also outputs the relevant remote-control:
configuration snippet that you can add to unbound.conf
.
forward-zone:
block (Forwarding queries)
This block allows you to specify that queries for certain DNS zones (or all queries) should be forwarded to other specific DNS servers (forwarders) instead of Unbound performing full recursive resolution for them.
Example structure:
forward-zone:
# Name of the zone to forward
name: "." # "." means forward all queries
# IP addresses of forwarders
forward-addr: 8.8.8.8 # Google Public DNS
forward-addr: 8.8.4.4 # Google Public DNS
# forward-tls-upstream: yes # If forwarders support DNS-over-TLS
forward-zone:
blocks for different domains.
stub-zone:
block (Stub resolvers)
A stub zone is similar to a forward zone, but it's typically used when you want Unbound to query specific authoritative name servers directly for a particular zone, rather than general forwarders. This is useful for private domains or when you want to bypass the normal recursion path for certain zones.
Example structure:
stub-zone:
name: "internal.example.com"
stub-addr: 192.168.1.10 # Authoritative server for internal.example.com
stub-addr: 192.168.1.11
# stub-prime: yes # Optional: prime the stub zone by querying for NS records first
Important server:
Directives
Let's delve into some of the most commonly used and important directives within the server:
block. The default unbound.conf
file that comes with the package usually has many of these commented out with brief explanations. For a full list, refer to the official unbound.conf(5)
man page (man unbound.conf
).
-
interface: <IP address[@port]>
Specifies the IP address(es) Unbound should listen on for incoming DNS queries. You can have multipleinterface:
lines.interface: 0.0.0.0
(Listen on all available IPv4 interfaces)interface: ::0
(Listen on all available IPv6 interfaces)interface: 127.0.0.1
(Listen on localhost IPv4 only - often the default for security)interface: 192.168.1.53
(Listen on a specific local network IP) If you don't specify a port with@port
, it uses the default port specified by theport:
directive.
-
port: <port number>
The UDP and TCP port number Unbound listens on for DNS queries. Default is53
.port: 53
-
do-ip4: <yes/no>
,do-ip6: <yes/no>
,do-udp: <yes/no>
,do-tcp: <yes/no>
These control whether Unbound uses IPv4/IPv6 and UDP/TCP for outgoing queries (to authoritative servers) and for listening. Defaults are usuallyyes
.do-ip4: yes
do-ip6: yes
(Enable if your server has IPv6 connectivity)do-udp: yes
do-tcp: yes
-
access-control: <IP netblock> <action>
Crucial for security. Defines which clients are allowed to query your Unbound server.action
can beallow
,deny
,refuse
,allow_snoop
, etc. Processed in order; the first match applies.access-control: 127.0.0.0/8 allow
(Allow queries from localhost)access-control: 192.168.1.0/24 allow
(Allow queries from the 192.168.1.x network)access-control: 0.0.0.0/0 refuse
(Refuse queries from all other IPv4 addresses - good default to prevent becoming an open resolver)access-control: ::0/0 refuse
(Refuse queries from all other IPv6 addresses)access-control: ::1/128 allow
(Allow queries from IPv6 localhost) An "open resolver" (one that accepts queries from anyone on the internet) can be abused for DNS amplification attacks. It's vital to restrict access appropriately.
-
chroot: "<directory>"
If specified, Unbound willchroot
to this directory after initialization. This limits the part of the filesystem Unbound can access, enhancing security. If you use chroot, ensure all necessary files (like/dev/log
or/dev/random
if needed, and the Unbound working directory) are accessible within the chroot. Package maintainers often set this up carefully. If you enable it manually, ensure thedirectory:
(see below) is relative to the chroot path, or an absolute path within the chroot.chroot: "/etc/unbound"
(Common, but ensure write permissions forunbound
user on necessary files/subdirs ifdirectory
is also there). Many packaged versions will use/var/lib/unbound
or similar.- If using chroot, file paths in the configuration (like
root-hints
,pidfile
,logfile
) might need to be relative to the chroot directory or you might need to create device nodes within the chroot (e.g., for/dev/log
).
-
username: "<user>"
The user Unbound drops privileges to after binding to port 53 (which requires root). Running as a non-root user is a critical security measure. Package installations usually create anunbound
user and set this automatically.username: "unbound"
-
directory: "<path>"
The working directory for Unbound. Relative paths for other files (likepidfile
,root-hints
if not absolute) are relative to this directory.directory: "/etc/unbound/"
(Common, but check permissions. Some setups use/var/lib/unbound/
)
-
logfile: "<filepath>"
anduse-syslog: <yes/no>
Configures logging.logfile: "/var/log/unbound.log"
(Log to a specific file. Ensure theunbound
user can write to it or its directory).use-syslog: yes
(Log to syslog. This is often the default and preferred for systemd integration, as logs go to the journal). Ifyes
,logfile:
is ignored.
-
verbosity: <level>
Controls the amount of detail in the logs.0
is minimal,1
is operational info,2
is detailed,3
is query/answer logging,4
is algorithm-level logging,5
is client identification for cache misses. Higher levels generate significantly more log data. Start with1
or2
.verbosity: 1
-
pidfile: "<filepath>"
Path to the file where Unbound stores its process ID.pidfile: "/run/unbound.pid"
(Common location for pid files).
-
root-hints: "<filepath>"
Path to the file containing the IP addresses of the root DNS servers. This file (often namedroot.hints
ornamed.cache
) is essential for Unbound to start the recursive resolution process. Unbound usually ships with a recent version, and it can also auto-update it.root-hints: "/usr/share/dns/root.hints"
(Common path, check your installation) orroot-hints: "/etc/unbound/root.hints"
It's good practice to periodically update this file. Unbound can do this automatically ifauto-trust-anchor-file
is configured and it has write access to a trust anchor file, as root hints are also part of the DNSSEC trust chain bootstrap. Alternatively, you can fetch it manually:
-
harden-glue: <yes/no>
Defaultyes
. Strengthens validation by checking if glue records (IP addresses of NS servers provided by a parent zone) are consistent with what the authoritative child zone provides. Recommended. -
harden-dnssec-stripped: <yes/no>
Defaultyes
. Protects against DNSSEC stripping attacks where an attacker removes DNSSEC records to make a domain appear unsigned. Unbound will mark data as "bogus" (invalid) if it expects DNSSEC records but doesn't receive them. Recommended. -
val-permissive-mode: <yes/no>
Defaultno
. Ifyes
, Unbound will attempt DNSSEC validation but will not fail a query if validation fails. This is primarily for debugging and should generally beno
for a secure resolver. -
cache-min-ttl: <seconds>
Minimum time-to-live (TTL) value Unbound will store in its cache, even if an authoritative server provides a lower TTL. Default0
.cache-min-ttl: 300
(5 minutes) - Can improve cache hit rates but might serve slightly stale data if the authoritative TTL was very low.
-
cache-max-ttl: <seconds>
Maximum TTL Unbound will store in its cache. Default86400
(1 day).cache-max-ttl: 604800
(1 week) - Can also improve cache hit rates but increases the risk of serving stale data if records change.
-
num-threads: <number>
Number of worker threads Unbound will create to handle queries. A good starting point is the number of CPU cores available.num-threads: 4
(For a 4-core CPU) You can find the number of cores withnproc
orlscpu
.
This list covers many fundamental directives. As we progress to intermediate and advanced topics, we will introduce more specialized options.
Workshop Creating Your First Basic Unbound Configuration
In this workshop, we'll create a basic unbound.conf
file from scratch (or by heavily modifying the default one). This configuration will set up Unbound to listen on all IPv4 interfaces on your server, allow queries only from the local machine and a specific private network, and enable basic logging.
Prerequisites
- Unbound installed on your Linux server (from the previous workshop).
- SSH access to your server.
- Root or sudo privileges.
Step 1: Backing up the Default Configuration (If not done already)
If a default unbound.conf
exists, back it up:
Step 2: Creating a Minimal unbound.conf
We'll create a new configuration. You can either delete the existing content of /etc/unbound/unbound.conf
or edit it. Let's open the file with a text editor like nano
or vim
:
Remove all existing content (if any) and add the following. Adjust IP addresses and paths according to your system and network.
# Unbound configuration file - Basic Setup
server:
# Logging
verbosity: 1 # Log level 1 (operational information)
# logfile: "/var/log/unbound.log" # Uncomment to log to a file
# Ensure 'unbound' user can write here
use-syslog: yes # Log to syslog (recommended for systemd)
# Performance and Resource Usage
num-threads: 2 # Adjust to number of CPU cores (e.g., nproc)
msg-cache-size: 4m # Size of message cache (e.g., 4m, 8m, 16m)
rrset-cache-size: 8m # Size of RRset cache (e.g., 8m, 16m, 32m)
# Security and Privacy
harden-glue: yes # Recommended for security
harden-dnssec-stripped: yes # Recommended for security
# Unblock DNS Rebinding protection if needed for local services using public names
# private-domain: "yourlocaldomain.com" # Domains that are RFC1918 but not bogon
# Root hints - path may vary depending on your OS distribution
# For Debian/Ubuntu, often /usr/share/dns/root.hints or /var/lib/unbound/root.hints
# For CentOS/RHEL, often /var/lib/unbound/root.hints or /etc/unbound/root.hints
# Check your system or download it:
# wget -O root.hints https://www.internic.net/domain/named.root
# sudo mv root.hints /etc/unbound/root.hints
root-hints: "/usr/share/dns/root.hints" # Example path, verify this file exists!
# If it doesn't exist, Unbound might fail to start
# or use built-in hints. Best to specify a valid one.
# DNSSEC - Automatically manage the root trust anchor
# The file needs to be writable by the unbound user if it doesn't exist.
# Or unbound-anchor can be run as root to initialize it.
# Debian/Ubuntu packages often place this in /var/lib/unbound/root.key
# CentOS/RHEL packages often place this in /etc/unbound/root.key or /var/lib/unbound/root.key
auto-trust-anchor-file: "/var/lib/unbound/root.key" # Verify path and permissions
# Network Interfaces and Port
interface: 0.0.0.0 # Listen on all IPv4 interfaces
interface: ::0 # Listen on all IPv6 interfaces (if IPv6 is enabled and used)
# If you don't have IPv6, comment this line or set do-ip6: no
port: 53 # Standard DNS port
# Protocol Support for Outgoing Queries
do-ip4: yes
do-ip6: yes # Set to no if your server has no IPv6 connectivity
do-udp: yes
do-tcp: yes
# Access Control - VERY IMPORTANT
# Deny all by default and then allow specific networks
access-control: 0.0.0.0/0 refuse # Deny all IPv4 by default
access-control: ::0/0 refuse # Deny all IPv6 by default
access-control: 127.0.0.0/8 allow # Allow localhost (IPv4)
access-control: ::1/128 allow # Allow localhost (IPv6)
# Add your local network(s) here
# Example: Allow your home/office network
# access-control: 192.168.1.0/24 allow
# access-control: 10.0.0.0/8 allow
# access-control: 2001:db8:abcd::/48 allow # Example IPv6 local network
# Specify user and chroot if not handled by service defaults (often they are)
# username: "unbound"
# chroot: "/var/lib/unbound" # Or "/etc/unbound" if setup for it
# directory: "/etc/unbound" # Working directory
# QNAME Minimisation (RFC 7816) for enhanced privacy
qname-minimisation: yes
# Aggressive NSEC (RFC 8198) for faster NXDOMAIN and performance
aggressive-nsec: yes
# Remote control setup (optional, but very useful)
# Run 'sudo unbound-control-setup' first to generate keys.
# Then, uncomment and add the generated config here.
# remote-control:
# control-enable: yes
# control-interface: 127.0.0.1
# # control-port: 8953 # Default
# # server-key-file: "/etc/unbound/unbound_server.key"
# # server-cert-file: "/etc/unbound/unbound_server.pem"
# # control-key-file: "/etc/unbound/unbound_control.key"
# # control-cert-file: "/etc/unbound/unbound_control.pem"
Important Notes for the Configuration:
-
root-hints:
:
Verify the path toroot.hints
.- On Debian/Ubuntu, it's often
/usr/share/dns/root.hints
. Sometimes a copy is in/var/lib/unbound/root.hints
which Unbound might use or create. The package might also configure Unbound to use a built-in list if the file is missing. - On CentOS/RHEL, it's often
/var/lib/unbound/root.hints
. - If you're unsure, you can download it:
- On Debian/Ubuntu, it's often
-
auto-trust-anchor-file:
:
This file stores the DNSSEC root trust anchor. Unbound needs to be able to update it. Theunbound
user must have write permission to this file or its directory if the file doesn't exist. Package managers often set this up correctly in/var/lib/unbound/
or/etc/unbound/
. If Unbound cannot write to this file, DNSSEC validation might not work correctly over time as root keys are updated. You can initialize it withsudo unbound-anchor -a /var/lib/unbound/root.key
(adjust path). -
interface:
:0.0.0.0
makes Unbound listen on all IPv4 addresses of the server.::0
makes Unbound listen on all IPv6 addresses. If your server doesn't have IPv6 or you don't intend to use it, you can comment outinterface: ::0
and setdo-ip6: no
.access-control:
:
This is critical.- The
0.0.0.0/0 refuse
and::0/0 refuse
lines are catch-alls that deny queries from any IP not explicitly allowed. 127.0.0.0/8 allow
and::1/128 allow
permit queries from the server itself (localhost).- You MUST uncomment and modify one of the
access-control
lines for your local network(s) (e.g.,access-control: 192.168.1.0/24 allow
) if you want other machines on your network to use this resolver. Otherwise, only the server itself can query Unbound. username
andchroot
:
These are often set by the systemd service file provided by the package. If you set them inunbound.conf
, they might override service defaults or conflict. For a packaged install, it's often safe to leave them commented out initially unless you have specific needs. Ifchroot
is active, paths likelogfile
,pidfile
,root-hints
,auto-trust-anchor-file
might need to be relative to the chroot directory, or the chroot environment needs to be set up with these files/devices available.num-threads
:
Adjust based onnproc
output. For a simple server, 1 or 2 is fine.- Cache Sizes:
msg-cache-size
andrrset-cache-size
can be increased for better performance on busier resolvers or systems with more RAM.4m
and8m
are modest starting points. (e.g.,16m
and32m
).
Step 3: Setting up unbound-control
(Recommended)
unbound-control
allows you to manage Unbound without restarting it (e.g., reload configs, view stats).
-
Generate the control keys:
This will createunbound_server.key
,unbound_server.pem
,unbound_control.key
, andunbound_control.pem
in/etc/unbound/
. It will also print theremote-control:
block you need to add tounbound.conf
. -
Edit
/etc/unbound/unbound.conf
again (sudo nano /etc/unbound/unbound.conf
). - Uncomment the
remote-control:
section at the end of your file and paste the output fromunbound-control-setup
. It should look similar to this:Make sure the paths to the key/cert files are correct.remote-control: control-enable: yes control-interface: 127.0.0.1 control-port: 8953 # Default, can be changed server-key-file: "/etc/unbound/unbound_server.key" server-cert-file: "/etc/unbound/unbound_server.pem" control-key-file: "/etc/unbound/unbound_control.key" control-cert-file: "/etc/unbound/unbound_control.pem"
Step 4: Testing the Configuration and Restarting Unbound
-
Check the configuration file for syntax errors:
If it reports "no errors", proceed. If there are errors, fix them based on the line number and message provided. Common issues are typos, missing colons, or incorrect paths. -
Restart Unbound to apply the new configuration:
-
Check the status of Unbound:
Look forActive: active (running)
. If it failed, check the logs for more details: (This shows the latest log entries for theunbound
unit). Common failure reasons include incorrect file paths (especially forroot-hints
orauto-trust-anchor-file
), permission issues for files or directories Unbound needs to access, or problems binding to the specified interfaces/port (e.g., another service already using port 53).
Step 5: Querying Your Resolver
Now, let's test if Unbound is working. We'll use dig
to query your Unbound server directly. Since we configured interface: 0.0.0.0
, it should be listening on its own IP address.
-
Find your server's IP address (if you don't know it):
Let's assume your server's IP is192.168.1.100
. -
Perform a DNS query from the server itself, targeting its own IP:
dig @192.168.1.100 www.example.com A # Or, more simply, targeting localhost if 127.0.0.1 is allowed dig @127.0.0.1 www.nlnetlabs.nl A
Expected Output (first query might be slower, subsequent ones faster due to caching):
Key things to look for:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ;; QUESTION SECTION: ;www.nlnetlabs.nl. IN A ;; ANSWER SECTION: www.nlnetlabs.nl. 3600 IN A 185.49.140.63 ;; Query time: X msec <-- Should be low for subsequent queries ;; SERVER: 127.0.0.1#53(127.0.0.1) ;; WHEN: Mon Jun 20 11:00:00 PDT 2023 ;; MSG SIZE rcvd: 63
status: NOERROR
: The query was successful.flags: qr rd ra
:qr
(query response),rd
(recursion desired - client asked for it),ra
(recursion available - server offers it).ANSWER SECTION
: Contains the IP address.SERVER: 127.0.0.1#53
(or your server's IP): Confirms your Unbound server answered.
If you get status: REFUSED
, it means your access-control
rules are blocking the query from the IP dig
is using. Double-check your access-control
list and the source IP of your query.
If you get status: SERVFAIL
on the first query, especially for DNSSEC-enabled domains, it might indicate an issue with auto-trust-anchor-file
(permissions or initial priming). Check logs: sudo journalctl -u unbound -e
. You might need to run sudo unbound-anchor -a /var/lib/unbound/root.key
(or your path) once to initialize the root key if Unbound can't do it itself.
This basic configuration provides a good starting point for a private recursive resolver. The next sections will build upon this foundation.
3. Basic DNS Operations and Testing
Once Unbound is installed and you have a basic configuration in place, it's important to know how to manage the Unbound service, test its functionality thoroughly, and understand its logs. This section covers these essential operational aspects.
Starting, Stopping, and Restarting Unbound
Managing the Unbound service is typically done using systemd
on modern Linux distributions. The main commands are:
-
Check Status: To see if Unbound is running, its PID, recent log entries, etc.:
Look forActive: active (running)
. -
Start Unbound: If the service is stopped and you want to start it:
-
Stop Unbound: To stop the service:
This will terminate the Unbound process. Clients will no longer be able to resolve DNS queries through it until it's restarted. -
Restart Unbound: To stop and then immediately start the service (useful after configuration changes that require a full restart, though
reload
is often preferred): -
Reload Unbound Configuration: To apply most configuration changes without dropping active connections or clearing the cache (if possible):
Alternatively, if you haveunbound-control
set up, you can use: This is generally the preferred way to apply configuration changes as it's less disruptive. Some changes (likechroot
or fundamental interface changes) might still require a fullrestart
. -
Enable Unbound on Boot: To ensure Unbound starts automatically when the server boots:
-
Disable Unbound on Boot: To prevent Unbound from starting automatically on boot:
On systems not using systemd
(e.g., older systems with SysVinit, or FreeBSD with rc.d
), the commands would be different:
-
Example for SysVinit:
-
Example for FreeBSD:
Checking Unbound Status with unbound-control
If you have configured remote-control:
in unbound.conf
and run unbound-control-setup
, the unbound-control
utility provides a powerful way to interact with the running Unbound daemon.
-
Basic Status:
Expected Output:
This output gives you:version: 1.13.1 # Or your installed version verbosity: 1 threads: 2 modules: 2 [ validator iterator ] uptime: 3668 seconds options: <various options like pidfile, root-hints path> queries: 50 total, 10 recursive, 0 prefetch, 0 recursover cache: 0.000000 sec cache: 8388608 bytes (8.0MB) rrset, 4194304 bytes (4.0MB) msg recursion answ/tot: 10/10 (100.0%) recursion time avg: 0.123456 recursion time median: 0.098765 tcp usage: 0 queries, 0 answers
- Unbound version.
- Current verbosity level.
- Number of threads and loaded modules (e.g.,
validator
for DNSSEC,iterator
for recursion). - Server uptime.
- Key configuration options.
- Query statistics (total, recursive, prefetch).
- Cache sizes (configured and current usage might be shown with more detailed stats commands).
- Recursion statistics.
Other useful unbound-control
commands (we'll explore more later):
sudo unbound-control stats
: Prints detailed statistics counters.sudo unbound-control stats_noreset
: Prints detailed statistics counters without resetting them.sudo unbound-control dump_cache > unbound_cache.txt
: Dumps the content of the cache to a file.sudo unbound-control lookup www.example.com
: Performs a lookup through Unbound, showing if it's from cache or resolved.
Testing DNS Resolution
The primary way to test if your Unbound server is working correctly is to send it DNS queries and check the responses. The dig
utility is excellent for this.
Key things to test:
-
Basic Resolution (A and AAAA records): Query for common domains.
Check forSERVER_IP
is the IP address where your Unbound server is listening (e.g.,127.0.0.1
if testing locally, or the server's LAN/public IP if testing from another client).status: NOERROR
and valid IP addresses in theANSWER SECTION
. -
Resolution of Different Record Types (MX, TXT, NS):
Verify that you receive the correct record types and data. -
Cache Behavior: Query the same domain twice in quick succession.
This indicates the record was served from Unbound's cache. -
DNSSEC Validation: Unbound should be performing DNSSEC validation by default if
auto-trust-anchor-file
is set up and the root anchor is primed.-
Test a valid DNSSEC-signed domain:
Look forflags: ... ad;
(Authentic Data) in the response header. This means Unbound successfully validated the DNSSEC signatures. TheANSWER SECTION
should also containRRSIG
records. -
Test an intentionally broken DNSSEC-signed domain:
This query should result instatus: SERVFAIL
. This is the correct behavior because Unbound detected a DNSSEC validation failure and refused to serve the bogus data. If you get an IP address, DNSSEC validation is not working correctly. Check Unbound logs for errors. -
Test a domain that is not DNSSEC-signed:
This should resolve successfully (status: NOERROR
) but will not have thead
flag, becauseexample.com
(as of writing) is not DNSSEC signed. Unbound will simply return the data as insecure.
-
-
Querying from Allowed vs. Denied Clients:
If you've configuredaccess-control
rules, test them:- From a machine whose IP is in an
allow
range, queries should work. - From a machine whose IP is in a
deny
orrefuse
range (or not explicitly allowed), queries should fail (e.g.,status: REFUSED
or timeout). For example, try querying your Unbound server from an external IP testing tool if your server is internet-facing and you haven't allowed0.0.0.0/0
.
- From a machine whose IP is in an
-
Non-Existent Domains (NXDOMAIN):
This should result instatus: NXDOMAIN
.
Understanding Unbound Logs
Unbound's logs are invaluable for troubleshooting and monitoring. The location and format depend on your configuration (logfile:
vs. use-syslog:
) and the verbosity:
level.
-
If
use-syslog: yes
(common default): Logs are typically sent to the system journal (managed bysystemd-journald
).-
View all Unbound logs:
bash sudo journalctl -u unbound
-
View the latest log entries and follow new ones (like
tail -f
):bash sudo journalctl -u unbound -f
-
View logs since a certain time:
bash sudo journalctl -u unbound --since "1 hour ago"
-
Filter by priority (e.g., errors):
bash sudo journalctl -u unbound -p err
-
-
If
logfile: "/path/to/unbound.log"
is configured: Logs are written to the specified file.
Interpreting Log Entries:
The content of the logs depends heavily on the verbosity
level in unbound.conf
:
verbosity: 0
:
Only errors are logged.-
Example entry (DNSSEC trust anchor update):verbosity: 1
:
Operational information, such as startup, shutdown, chosen interfaces, module loading, and significant errors or warnings. This is a good default for general use. Example entry (startup): -
verbosity: 2
:
Detailed operational information. Logs more about cache operations, prefetches, and resolutions. -
verbosity: 3
:
Logs all queries and replies processed by Unbound. This can generate a lot of data but is very useful for debugging specific resolution problems. Example entry (query and reply): -
verbosity: 4
:
Algorithm-level logging. Shows details of the iteration process, DNSSEC validation steps. Useful for deep DNSSEC debugging. verbosity: 5
:
Client identification for cache misses. Helps understand which client queries are causing cache misses.
When troubleshooting, temporarily increasing the verbosity level can provide crucial insights. Remember to set it back to a lower level (e.g., 1 or 2) for normal operation to avoid excessive disk space usage and performance impact.
Workshop Testing and Verifying Your Unbound Installation
This workshop will guide you through configuring a client machine to use your new Unbound resolver and then performing a series of tests to ensure it's working as expected, including basic resolution, caching, and DNSSEC validation.
Prerequisites
- Your Unbound server set up and running from the previous workshop.
- The IP address of your Unbound server.
- A separate client machine on the same network that is allowed to query your Unbound server (based on your
access-control
rules). This could be your desktop/laptop or another VM. dig
(ornslookup
) installed on the client machine.
Step 1: Configuring a Client to Use Your Unbound Resolver
You need to tell your client machine's operating system to use your Unbound server's IP address for DNS resolution. Let your Unbound server's IP be 192.168.1.100
for this example.
Option A: Linux Client (Temporary Change - Modifying /etc/resolv.conf
)
Most modern Linux distributions use systemd-resolved
or NetworkManager to manage /etc/resolv.conf
. Directly editing it might be overwritten. For a quick test:
- Open
/etc/resolv.conf
with sudo: - Comment out any existing
nameserver
lines by adding a#
at the beginning. - Add a new line for your Unbound server:
- Save the file. This change is usually temporary and might be reverted on reboot or network restart.
For a more permanent change on a Linux client (if using NetworkManager):
Use nm-connection-editor
(GUI) or nmtui
(terminal UI) to edit your network connection settings and specify 192.168.1.100
as the DNS server. Or, if configuring via /etc/netplan/
(common on Ubuntu server), you would modify the YAML configuration there.
For a more permanent change on a Linux client (if using systemd-resolved
):
Edit /etc/systemd/resolved.conf
, uncomment and set DNS=192.168.1.100
. Then run sudo systemctl restart systemd-resolved
.
Option B: Windows Client (Network Adapter Settings)
- Open "Control Panel" -> "Network and Internet" -> "Network and Sharing Center".
- Click on "Change adapter settings" on the left.
- Right-click on your active network connection (e.g., "Ethernet" or "Wi-Fi") and select "Properties".
- Select "Internet Protocol Version 4 (TCP/IPv4)" and click "Properties".
- Select "Use the following DNS server addresses:".
- In "Preferred DNS server", enter
192.168.1.100
. - Click "OK" on both dialogs.
- You might need to clear your local DNS cache: open Command Prompt as Administrator and run
ipconfig /flushdns
.
Option C: macOS Client (Network Settings)
- Open "System Settings" (or "System Preferences" on older macOS).
- Go to "Network".
- Select your active network connection (e.g., "Wi-Fi" or "Ethernet") from the list.
- Click the "Details..." button (or "Advanced..." on older macOS).
- Go to the "DNS" tab.
- Click the
+
button under "DNS Servers" and add192.168.1.100
. - If there are other DNS servers listed, you might want to remove them or move yours to the top of the list to ensure it's used preferentially.
- Click "OK" and then "Apply".
Step 2: Performing Basic Queries from the Client
On your configured client machine, open a terminal or command prompt.
-
Test A record resolution:
The output should showSERVER: 192.168.1.100#53
(or your Unbound server's IP) and a successful resolution. -
Test AAAA record resolution:
-
Test MX record resolution:
If these fail (e.g., timeout, SERVFAIL
for everything, or REFUSED
), double-check:
- The client's DNS settings are correctly pointing to your Unbound server's IP.
- Your Unbound server's
access-control
rules inunbound.conf
allow queries from the client's IP address. - The Unbound service is running on the server (
sudo systemctl status unbound
). - There's network connectivity between the client and server (e.g.,
ping 192.168.1.100
from the client). - Check Unbound logs on the server (
sudo journalctl -u unbound -f
) for any error messages when the client makes a query.
Step 3: Testing Cache Performance
-
Query a domain for the first time:
Note the "Query time" in the output. -
Immediately query the same domain again:
The "Query time" should now be very low (e.g., 0 msec or 1 msec), and the TTL in the answer section might be slightly less than the first query. This indicates the response came from Unbound's cache.
Step 4: Testing DNSSEC Validation
-
Test a known good DNSSEC-signed domain:
Look for thead
(Authentic Data) flag in theflags:
line of the header. Example:flags: qr rd ra ad;
. If thead
flag is present, DNSSEC validation was successful. -
Test a known bad (intentionally misconfigured) DNSSEC-signed domain:
This command should result instatus: SERVFAIL
. No IP address should be returned. This is the correct behavior, as Unbound detected a DNSSEC error and refused to serve potentially compromised data. If you get an IP address andstatus: NOERROR
, DNSSEC validation is likely not working correctly on your Unbound server. Check theauto-trust-anchor-file
configuration and Unbound's logs. -
Check Unbound server logs for DNSSEC messages: On the Unbound server, if
Look for lines containing "validator" or "DNSKEY" or "secure" or "bogus".verbosity
is 1 or higher, you should see messages related to DNSSEC validation, especially if issues occur or when the trust anchor is updated.
Step 5: Using unbound-control status
on the Server
Log back into your Unbound server via SSH.
-
Get status and stats:
Observe thequeries: total, recursive
counts. As you perform queries from your client, these numbers should increase. Look at theuptime
and cache sizes. -
Dump detailed stats (optional):
This provides many counters, includingnum.query.type.A
,num.query.cachehit
,num.query.cachemiss
,num.query.dnscrypt.shared_secret.cachemiss
,num.query.secure
(DNSSEC validated),num.query.bogus
(DNSSEC validation failed). These can be very informative.
If all these tests pass, your Unbound server is correctly resolving DNS queries, caching results, and performing DNSSEC validation for your client(s). You have successfully set up a basic, secure, and private recursive DNS resolver! Remember to revert your client's DNS settings if the change was temporary, or make it permanent if you intend to use your Unbound server full-time.
4. Securing Your Basic Unbound Installation
Security is paramount when running any internet-facing or even network-local service. While Unbound is designed with security in mind, several best practices and configurations should be implemented to harden your installation further. This section focuses on fundamental security measures for your basic Unbound setup.
Principle of Least Privilege (Running Unbound as a non-root user)
One of the most fundamental security principles is the "Principle of Least Privilege." This means that any process should run with only the minimum permissions necessary to perform its job.
Unbound needs root privileges initially to bind to port 53 (as ports below 1024 are privileged). However, after successfully binding to the port and performing other initial setup tasks (like reading certain configuration files or chrooting), Unbound should drop root privileges and run as a dedicated, unprivileged user.
-
Most package installations of Unbound automatically:username: "<user>"
Directive: Theusername
directive in theserver:
block ofunbound.conf
specifies the user account Unbound will switch to.- Create a dedicated system user (e.g.,
unbound
or_unbound
) with no login shell and a locked password. - Configure the Unbound service (e.g., via systemd unit file) or the default
unbound.conf
to use this user.
Verification: You can check which user the Unbound process is running as:
You should see the main Unbound process running as root (because it needs to manage threads, etc., and might have been started as root by systemd), but worker threads or the primary query-handling process should be running as the unprivilegedunbound
user. The effective user ID (EUID) for query processing should be the non-root user. The details of process management can be complex, but the key is that the parts handling untrusted network input operate with reduced privileges.If
username
is not set or is set toroot
(which is highly discouraged), any vulnerability exploited in Unbound could grant an attacker root access to your server. - Create a dedicated system user (e.g.,
-
chroot: "<directory>"
Directive: Chrooting (change root directory) is another powerful security mechanism. When Unbound chroots to a specific directory (e.g.,chroot: "/var/lib/unbound"
), that directory becomes the new root (/
) of the filesystem for the Unbound process. This means Unbound can no longer see or access files outside this designated directory.server: chroot: "/var/lib/unbound" # Or /etc/unbound, depending on setup directory: "" # Often set to empty or "." if chroot is used, # meaning working dir is the chroot dir itself. # Or an absolute path *within* the chroot.
Considerations for chroot:
- The chroot directory must contain all files and device nodes Unbound needs after chrooting. This can include:
- Its working directory (
directory:
). pidfile
(if not outside chroot and managed by systemd).root.hints
file.auto-trust-anchor-file
./dev/log
(for syslog) or/dev/null
,/dev/random
if needed directly by Unbound (often handled by systemd or OpenSSL).
- Its working directory (
- Package installations that enable chroot usually handle this setup. If you enable it manually, it requires careful planning. For example, if
directory: "/etc/unbound"
andchroot: "/etc/unbound"
, thenpidfile: "unbound.pid"
would resolve to/etc/unbound/unbound.pid
inside the chroot (which is the real/etc/unbound/unbound.pid
). - The
unbound
user needs appropriate permissions within the chroot directory (e.g., write access toauto-trust-anchor-file
and thepidfile
if applicable, and the log file if not using syslog).
Chrooting significantly limits the potential damage if an attacker compromises the Unbound process, as they would be confined within the chroot jail. Many packaged Unbound installations enable chroot by default, often to a directory like
/var/lib/unbound
or/etc/unbound
. - The chroot directory must contain all files and device nodes Unbound needs after chrooting. This can include:
Restricting Access with access-control
We've touched on access-control
previously, but its importance for security cannot be overstated. This directive dictates which IP addresses or networks are allowed to send queries to your Unbound server. Failing to configure this correctly can turn your server into an "open resolver."
Why Open Resolvers are Bad:
An open resolver is a DNS server that accepts and processes queries from anyone on the internet. Open resolvers can be abused in several ways:
- DNS Amplification Attacks:
Attackers can send small DNS queries to your open resolver with a spoofed source IP address (the victim's IP). If the DNS response is significantly larger than the query, your server "amplifies" the traffic towards the victim, contributing to a Distributed Denial of Service (DDoS) attack. Unbound has some mitigations against this (like rate limiting), but restricting access is the primary defense. - Information Leakage/Reconnaissance: Attackers might use your open resolver to perform DNS lookups, hiding their own origin.
- Resource Consumption: Unsolicited queries can consume your server's bandwidth, CPU, and memory.
Best Practices for access-control
:
The strategy should be to deny all by default, then explicitly allow only trusted clients/networks.
server:
# ... other settings ...
# ACCESS CONTROL: Processed in order. First match wins.
# Deny all by default. These should ideally be the last rules if you have specific deny rules above them.
# Or, place your allow rules first, followed by a deny-all.
access-control: 0.0.0.0/0 refuse # Refuse all IPv4 queries by default
access-control: ::0/0 refuse # Refuse all IPv6 queries by default
# Allow queries from the server itself (localhost)
access-control: 127.0.0.0/8 allow
access-control: ::1/128 allow
# Allow queries from your trusted local network(s)
# Replace with your actual network CIDR(s)
access-control: 192.168.1.0/24 allow
# access-control: 10.0.0.0/8 allow
# access-control: 2001:db8:cafe::/48 allow # Example IPv6 local network
# If you need to allow specific external IPs (e.g., a VPN client IP), add them here:
# access-control: 203.0.113.42/32 allow
Explanation of access-control
actions:
deny
:
Silently drops the query. The client will time out.refuse
:
Sends back a DNS response with theRCODE
(Response Code) set toREFUSED
. This informs the client that the query was intentionally rejected. This is generally preferred overdeny
as it's more explicit.allow
:
Allows queries from this network/IP. Unbound will attempt to resolve them.allow_snoop
:
Allows queries and also allows Unbound to provide more information about its cache contents to clients from this network (used withcache-snoop
directive, generally not needed for typical resolver setups).allow_cookie
:
(Relevant for DNS Cookies, RFC 7873) Allows queries if a valid DNS cookie is present, even if the IP would otherwise be denied. This is more advanced.deny_empty_referral
/refuse_empty_referral
:
These handle empty referral responses.
Order Matters:
Unbound processes access-control
rules from top to bottom. The first rule that matches the client's IP address is applied. Therefore, it's common to put your specific allow
rules first, followed by a catch-all refuse
or deny
for 0.0.0.0/0
and ::0/0
. Alternatively, as shown above, start with deny/refuse all, then poke holes with allow rules. The latter is often considered safer as it makes the default-deny explicit.
Basic Firewall Configuration (e.g., ufw
, firewalld
)
While Unbound's access-control
directive provides application-level control over who can query it, a host-based firewall adds an additional layer of defense at the network level. A firewall can block unwanted traffic before it even reaches the Unbound application.
This is particularly important if your Unbound server has a public IP address. Even for internal resolvers, a firewall helps enforce network segmentation and protect against threats from within your network.
Unbound listens for DNS queries on UDP port 53 and TCP port 53. You should configure your firewall to:
- Allow incoming traffic on UDP/53 and TCP/53 only from the IP addresses/networks that you have also specified in Unbound's
access-control
list. - Allow other necessary traffic (e.g., SSH on TCP/22 from your admin IP).
- Block all other incoming traffic by default (default deny policy).
Using ufw
(Uncomplicated Firewall - common on Debian/Ubuntu):
- Install
ufw
(if not already installed): -
Set default policies:
This blocks all incoming connections by default and allows all outgoing connections (Unbound needs to make outgoing queries). -
Allow SSH (crucial if you're connected via SSH!): Replace
your_admin_ip_range
with your actual IP or network, or omitfrom ...
to allow from anywhere (less secure for SSH). -
Allow DNS traffic from trusted sources: Let's say your local network is
If you have multiple trusted networks or specific IPs, add rules for each.192.168.1.0/24
and localhost. -
Enable
It will warn you that this may disrupt existing SSH connections. If you've correctly allowed SSH, it should be fine. Typeufw
:y
to proceed. -
Check status:
This will list all active rules.
Using firewalld
(common on CentOS/RHEL/Fedora):
- Ensure
firewalld
is running and enabled: -
Define your trusted zone or use rich rules:
firewalld
uses zones (e.g.,public
,internal
,home
). You can add your trusted source IPs to a specific zone and then allow the DNS service for that zone, or use rich rules for more granularity.Example using rich rules (allows specific source IPs/networks): Replace
192.168.1.0/24
with your trusted network.The# Allow DNS from your local network sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="dns" accept' # If you also use IPv6 for local clients: # sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" source address="2001:db8:cafe::/48" service name="dns" accept' # Allow DNS from localhost (often covered by default trusted lo interface) # sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" service name="dns" accept' # Ensure SSH is allowed (usually part of the default 'public' zone service list or your admin zone) # sudo firewall-cmd --permanent --add-service=ssh # Reload firewalld to apply permanent rules sudo firewall-cmd --reload
service name="dns"
predefinition covers both TCP and UDP port 53. -
List active rules/services:
By combining Unbound's access-control
with a host firewall, you create a robust defense-in-depth strategy for your DNS resolver. The firewall acts as the first line of defense, blocking unwanted packets at the network layer, while access-control
provides finer-grained, application-level control.
Workshop Implementing Basic Security Measures
This workshop will guide you through verifying Unbound's user context, refining access-control
rules, and setting up ufw
(for Debian/Ubuntu systems) or firewalld
(for CentOS/RHEL systems) to protect your Unbound server.
Prerequisites
- Your Unbound server set up and running.
- SSH access to your Unbound server with sudo privileges.
- Knowledge of your local network's IP range (e.g.,
192.168.1.0/24
). - Knowledge of the IP address you use to SSH into the server (your admin IP).
Step 1: Verifying Unbound User and Chroot (If Applicable)
-
Check the
Ensure it's set to a non-root user likeusername
inunbound.conf
:unbound
. If it's commented out, the service default (usuallyunbound
) is likely in effect. -
Check the running process (optional, for deeper understanding):
Look for processes owned by theunbound
user. -
Check for
If enabled, note the directory. If it's commented out, it might be set by the systemd service file. You can inspect the service file: Look forchroot
inunbound.conf
:User=
,Group=
, andRootDirectory=
(systemd's equivalent of chroot for services) orExecStart=
options that might specify chroot. Packaged installs usually configure this well.
No action is needed if your package manager's Unbound setup already runs as a non-root user (which is typical). This step is for verification and understanding.
Step 2: Refining access-control
Rules
Ensure your access-control
rules in /etc/unbound/unbound.conf
are correctly configured to only allow queries from trusted sources.
-
Edit
unbound.conf
: -
Locate the
access-control
directives. A secure configuration should look something like this (adjust192.168.1.0/24
to your actual local network range):# Inside the server: block # Deny all by default (place these first or last, depending on your preference) access-control: 0.0.0.0/0 refuse access-control: ::0/0 refuse # Allow localhost access-control: 127.0.0.0/8 allow access-control: ::1/128 allow # Allow your specific local network(s) access-control: 192.168.1.0/24 allow # MODIFY THIS TO YOUR NETWORK # Example for another trusted network: # access-control: 10.8.0.0/16 allow # Example for a specific trusted external IP: # access-control: 203.0.113.55/32 allow
- Crucially, ensure that any IP range you plan to use to query Unbound (e.g., your LAN clients) is explicitly listed with
allow
. - Remove or comment out any overly permissive rules like
access-control: 0.0.0.0/0 allow
.
- Crucially, ensure that any IP range you plan to use to query Unbound (e.g., your LAN clients) is explicitly listed with
-
Save the file and exit the editor.
-
Check the configuration syntax:
Fix any reported errors. -
Reload Unbound to apply the changes:
Step 3: Configuring the Host Firewall
Choose the subsection relevant to your server's OS.
Option A: Using ufw
(for Debian/Ubuntu)
-
Install
ufw
if not present: -
Set default policies:
-
Allow SSH: It's safest to allow SSH only from your specific admin IP or network. If your admin IP is dynamic or you're unsure, you can temporarily use
sudo ufw allow ssh
which allows from anywhere, then refine it later. Let's say your admin IP is203.0.113.42
:If you just ransudo ufw allow from 203.0.113.42/32 to any port 22 proto tcp # If your admin IP is part of a /24 network you trust: # sudo ufw allow from YOUR_ADMIN_NETWORK_CIDR to any port 22 proto tcp
sudo ufw allow ssh
, that's okay for now, but consider restricting it. -
Allow DNS traffic only from trusted sources (matching your
access-control
rules): Assuming your local network is192.168.1.0/24
and you want to allow localhost.# Allow from localhost sudo ufw allow from 127.0.0.1 to any port 53 # sudo ufw allow from ::1 to any port 53 # If using IPv6 for localhost queries # Allow from your specific local network (MODIFY THIS TO YOUR NETWORK) sudo ufw allow from 192.168.1.0/24 to any port 53 proto udp sudo ufw allow from 192.168.1.0/24 to any port 53 proto tcp # If you have other trusted IPs or networks in access-control, add them here too. # Example: sudo ufw allow from 10.8.0.0/16 to any port 53
-
Enable
Confirm withufw
:y
. Your SSH session should remain active if you allowed SSH correctly. -
Check status:
Verify that your rules for SSH and DNS (port 53) from trusted sources are listed withALLOW
, and the default incoming policy isDENY
.
Option B: Using firewalld
(for CentOS/RHEL/AlmaLinux/Rocky Linux)
-
Ensure
firewalld
is active: -
Add rich rules for DNS traffic from trusted sources: Replace
192.168.1.0/24
with your actual local network.# Allow DNS from your specific local network (MODIFY THIS TO YOUR NETWORK) sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="dns" accept' # If you use IPv6 on your local network and have IPv6 clients: # sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" source address="YOUR_IPV6_LAN_PREFIX" service name="dns" accept' # Allow DNS from localhost (often implicitly allowed if 'lo' interface is in 'trusted' zone, but explicit is fine) sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" service name="dns" accept' # sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv6" source address="::1" service name="dns" accept' # Ensure SSH is allowed. It's often enabled by default in the 'public' zone. # If not, or if you use a different zone, add it: # sudo firewall-cmd --permanent --add-service=ssh # Or, for more restriction (e.g., allow SSH only from a specific admin network): # sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="YOUR_ADMIN_NETWORK_CIDR" service name="ssh" accept'
-
Reload
firewalld
to apply changes: -
Check configuration:
Ensure your DNS rich rules are listed and that the SSH service is allowed so you don't lock yourself out.
Step 4: Testing Access from Allowed and Denied IPs
-
Test from an allowed client: From a machine on your local network (e.g.,
This should succeed.192.168.1.x
) that you configured client-side to use your Unbound server, perform a DNS query: -
Test from a denied IP (more complex to simulate unless you have an external machine):
- If your Unbound server has a public IP and you try to query it from an external machine NOT listed in
access-control
or firewall rules, the query should fail (timeout orREFUSED
). - Locally (Simulating a denied client on the same server for
access-control
testing): You could temporarily add a deny rule for a specific unused IP on your local network inunbound.conf
, restart Unbound, then try to usedig
's-b
option to source the query from that IP (requires the IP to be configured on an interface locally, can be tricky). Example: Inunbound.conf
:access-control: 192.168.1.200/32 refuse
(assuming .200 is unused) Reload Unbound. If you can assign192.168.1.200
as a secondary IP to your test client, then trydig @YOUR_UNBOUND_SERVER_IP -b 192.168.1.200 www.example.com A
. This should be refused. - Firewall Test: If you try to query from an IP that is not allowed by the firewall rules (even if it might be allowed by Unbound's
access-control
), the firewall should drop the packet, anddig
will time out.
- If your Unbound server has a public IP and you try to query it from an external machine NOT listed in
The primary goal is to ensure that only legitimate clients defined in both your Unbound access-control
list and your firewall rules can successfully query your DNS server.
By completing this workshop, you've significantly enhanced the security posture of your Unbound installation by verifying user privileges, tightening access controls, and implementing a host-based firewall. These are essential steps for any self-hosted service.
Intermediate Unbound Features
Having mastered the basic setup and security of your Unbound resolver, we now move into intermediate territory. This section will explore features that enhance privacy, improve performance, enable robust DNSSEC validation, and allow for local DNS customization. These capabilities transform your Unbound instance from a simple resolver into a more powerful and tailored DNS solution.
5. Enhancing Privacy with Unbound
Privacy is a key motivator for self-hosting a DNS resolver. Unbound offers several features that go beyond simply keeping your query logs local. These features minimize the data exposed to upstream DNS servers during the resolution process, further protecting your browsing habits from widespread observation.
QNAME Minimisation (qname-minimisation: yes
)
QNAME (Query Name) minimisation is a privacy-enhancing feature standardized in RFC 7816. It changes how Unbound queries authoritative DNS servers.
What it is and how it works
Without QNAME minimisation, when a recursive resolver like Unbound needs to resolve www.sub.example.com
, it would traditionally send the full query name www.sub.example.com
to each server in the resolution chain:
- To Root Server: "What are the NS records for
www.sub.example.com
?" (Root doesn't need to know "www.sub", only ".com") - To
.com
TLD Server: "What are the NS records forwww.sub.example.com
?" (TLD server doesn't need "www", only "sub.example.com") - To
example.com
Authoritative Server: "What are the NS records forwww.sub.example.com
?" (This server might only managesub.example.com
and delegatewww.sub.example.com
) - To
sub.example.com
Authoritative Server: "What is the A record forwww.sub.example.com
?"
This exposes the full domain you're trying to reach to every upstream server, even if they don't need that level of detail to provide their part of the answer.
With QNAME minimisation enabled, Unbound only sends the minimum necessary part of the query name to each server:
- To Root Server: "What are the NS records for
.com
?" - To
.com
TLD Server: "What are the NS records forexample.com
?" - To
example.com
Authoritative Server: "What are the NS records forsub.example.com
?" (Ifsub.example.com
is a separate zone delegated fromexample.com
) - To
sub.example.com
Authoritative Server: "What is the A record forwww.sub.example.com
?"
If example.com
's server is authoritative for sub.example.com
as well, it would be asked: "What are the NS records for sub.example.com
?". If it directly handles it, it might return the A record for www.sub.example.com
if asked, or indicate it handles sub.example.com
. Unbound intelligently figures out how much to ask.
Benefits for privacy
- Reduced Data Exposure: Each authoritative server in the resolution path only sees the portion of the domain name relevant to its level of the hierarchy. The root servers only see TLD queries, TLD servers only see second-level domain queries, and so on.
- Less Information for Profiling: This makes it harder for operators of upstream DNS servers to build detailed profiles of your browsing activity based on the full query names they receive.
Configuration in unbound.conf
:
QNAME minimisation is often enabled by default in recent Unbound versions, but it's good to ensure it's explicitly set.
server:
# ... other settings ...
qname-minimisation: yes
# Optional: For some misbehaving authoritative servers, you might need:
# qname-minimisation-strict: no # Default is no. If yes, enforces strict RFC adherence,
# which can break resolution for some non-compliant domains.
# Usually 'no' is fine and more compatible.
Aggressive NSEC (aggressive-nsec: yes
)
Aggressive NSEC is a feature (inspired by RFC 8198, "Aggressive Use of DNSSEC-Validated Cache") that leverages DNSSEC NSEC and NSEC3 records to synthesize "NXDOMAIN" (Non-Existent Domain) responses from the cache without querying authoritative servers. This can improve performance and reduce query load.
Understanding NSEC and NSEC3
NSEC (Next Secure) and NSEC3 (Next Secure version 3) records are part of DNSSEC. They provide authenticated denial of existence.
- NSEC: For a DNSSEC-signed zone, NSEC records create a chain of all existing domain names in the zone. An NSEC record for
alpha.example.com
would point tocharlie.example.com
ifbravo.example.com
doesn't exist, and list the record types that exist foralpha.example.com
. This proves thatbravo.example.com
doesn't exist. A downside is "zone walking," where one can enumerate all names in a zone by following the NSEC chain. - NSEC3: To mitigate zone walking, NSEC3 uses hashed domain names. An NSEC3 record for a hash of
alpha.example.com
would point to the hash ofcharlie.example.com
. This makes it computationally harder to enumerate all names, though not impossible.
How Aggressive NSEC leverages NSEC/NSEC3
When Unbound receives an NSEC or NSEC3 record during a DNSSEC-validated resolution (e.g., proving that nonexistent.example.com
does not exist between name1.example.com
and name2.example.com
), it caches this information.
With aggressive-nsec: yes
:
If Unbound later receives a query for another domain that, based on the cached NSEC/NSEC3 information, also would not exist within that same range (e.g., nonexistent-too.example.com
), Unbound can confidently synthesize an NXDOMAIN response directly from its cache without needing to query the authoritative servers again.
Benefits:
- Faster NXDOMAIN Responses: For domains that don't exist within DNSSEC-signed zones for which Unbound has cached NSEC/NSEC3 data, NXDOMAIN responses are served instantly from the cache.
- Reduced Query Load: Fewer queries are sent to authoritative servers for non-existent domains.
- Privacy (Minor): Slightly reduces the number of queries for non-existent names sent upstream, though the primary benefit is performance.
Configuration in unbound.conf
:
DNS-over-TLS (DoT) and DNS-over-HTTPS (DoH) - Unbound as a Client
While running your own resolver significantly enhances privacy by keeping your direct queries local, your Unbound server still needs to talk to other DNS servers (root, TLD, authoritative) using traditional UDP/53 or TCP/53. This traffic, though qname-minimized, is typically unencrypted and can be observed by entities on the network path (e.g., your ISP).
To encrypt this "last mile" of DNS resolution from your resolver to upstream servers, Unbound can be configured to use DNS-over-TLS (DoT) or DNS-over-HTTPS (DoH) when forwarding queries. This is useful if, instead of performing full recursion itself for all queries, you want Unbound to act as a local caching resolver that forwards to a trusted upstream DoT/DoH provider (like Cloudflare, Quad9, Google Public DNS).
This is configuring Unbound as a DoT/DoH client. (Unbound can also act as a DoT/DoH server, which is an advanced topic.)
Forwarding queries over DoT/DoH to upstream resolvers
You use a forward-zone:
block for this.
-
DNS-over-TLS (DoT): Uses TLS to encrypt DNS queries, typically on port 853.
forward-zone: name: "." # Forward all queries # List of DoT upstream servers. Unbound will pick one. # Format: IP_ADDRESS@PORT#TLS_SERVER_NAME # TLS_SERVER_NAME is the domain name that must be in the server's SSL certificate for validation. # Example: Cloudflare DoT forward-addr: 1.1.1.1@853#cloudflare-dns.com forward-addr: 1.0.0.1@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com # IPv6 forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com # IPv6 # Example: Quad9 DoT (secure, blocks malicious domains, DNSSEC validated) # forward-addr: 9.9.9.9@853#dns.quad9.net # forward-addr: 149.112.112.112@853#dns.quad9.net # forward-addr: 2620:fe::fe@853#dns.quad9.net # IPv6 # Tell Unbound to use TLS for these forwarders forward-tls-upstream: yes
-
DNS-over-HTTPS (DoH): Encapsulates DNS queries in HTTPS, typically on port 443. This can be more resilient to network blocking as it looks like regular web traffic.
forward-zone: name: "." # Forward all queries # List of DoH upstream servers. # Format: URL_OF_DOH_ENDPOINT # Note: Unbound's native DoH client support is for well-known providers or # requires careful setup. Paths to CA certs might be needed if not using system CAs. # Example: Cloudflare DoH forward-addr: https://cloudflare-dns.com/dns-query # forward-addr: https://1.1.1.1/dns-query # Using IP might be problematic for cert validation # Example: Google DoH # forward-addr: https://dns.google/dns-query # Tell Unbound to use HTTPS for these forwarders (implicitly if URL starts with https://) # For DoH, 'forward-tls-upstream: yes' is also effectively what happens, # but the URL scheme is the primary indicator. # You may need to specify SSL CA certificates for DoH validation if system ones are not enough: # tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt" # Path to CA bundle
Important Considerations for DoT/DoH Forwarding:
- Trust: You are shifting your trust from various authoritative servers (in full recursion mode) to a single (or few) upstream DoT/DoH provider(s). Choose providers whose privacy policies you trust.
- Centralization: Relying on a few large DoT/DoH providers contributes to DNS centralization, which some argue has its own downsides.
- Performance: Forwarding might add latency compared to direct recursion if the forwarder is geographically distant. However, a well-connected forwarder can also be very fast.
- DNSSEC: If you forward, Unbound itself might not be doing DNSSEC validation on the forwarded queries if the forwarder already does it (and you trust it). Some DoT/DoH providers offer DNSSEC-validating endpoints. If Unbound is configured for DNSSEC (
auto-trust-anchor-file
etc.) andforward-tls-upstream: yes
is set, Unbound should still validate DNSSEC responses from the forwarder unless specifically configured otherwise for the forward zone. Check theforward-secure:
vsforward-first:
options (default isforward-first
which means if forwarders fail, it may try recursion). - Certificate Validation: For DoT/DoH, Unbound needs to validate the TLS certificate of the upstream server. Ensure your system's CA certificate bundle is up to date, or specify
tls-cert-bundle
in theserver:
block if needed. The#SERVER_NAME
inforward-addr
for DoT is crucial for this.
When to use forwarding vs. full recursion:
- Full Recursion (no
forward-zone
for.
):- Pros: Maximum privacy from intermediate resolvers (only root/TLD/auth servers see parts of your queries), no reliance on third-party resolver policies.
- Cons: Outgoing queries from Unbound to authoritative servers are usually unencrypted (unless those servers happen to support DoT/DoH and Unbound is configured to use it with them, which is rare for auth servers).
- Forwarding to DoT/DoH:
- Pros: Encrypts queries between your Unbound and the upstream forwarder, protecting them from local network snooping (e.g., by your ISP). Can bypass some forms of DNS blocking/censorship.
- Cons: You trust the DoT/DoH provider with your full query history. Contributes to centralization.
Many users opt for full recursion with QNAME minimisation for a good balance. If your primary threat model involves ISP snooping on DNS, DoT/DoH forwarding is a strong countermeasure.
Workshop Configuring QNAME Minimisation and Forwarding to a DoT Resolver
This workshop will guide you through enabling QNAME minimisation and Aggressive NSEC in your Unbound configuration. Then, as an alternative setup, you'll configure Unbound to forward all its queries to a public DNS-over-TLS (DoT) resolver, encrypting the traffic between your Unbound instance and the upstream provider.
Prerequisites
- Your Unbound server set up and running.
- SSH access to your Unbound server with sudo privileges.
- The
unbound.conf
file from previous configurations. dig
utility available on the server or a client machine.- Optional:
tcpdump
ortshark
(Wireshark's command-line utility) on the Unbound server to observe DNS traffic (for advanced verification).
Part 1: Enabling QNAME Minimisation and Aggressive NSEC
These features enhance privacy and performance when Unbound is performing full recursion.
-
Edit
unbound.conf
: -
Add/Ensure QNAME Minimisation and Aggressive NSEC directives in the
server:
block: If they are not already present or are commented out, add or uncomment them and set them toyes
. -
Save the file and exit the editor.
-
Check the configuration syntax:
Fix any errors. -
Reload Unbound:
-
Verification (Conceptual):
- QNAME Minimisation: Verifying QNAME minimisation directly requires observing the outgoing DNS queries from Unbound using tools like
tcpdump
ortshark
. You would look for queries to root/TLD servers that only contain the relevant part of the domain name. For example, forwww.sub.example.com
, you'd see Unbound ask a root server for.com
NS records, not forwww.sub.example.com
NS records. This is advanced and optional for this workshop. For now, trust that Unbound implements it when enabled. - Aggressive NSEC: This primarily improves performance for NXDOMAIN responses in DNSSEC-signed zones. You might notice slightly faster responses for non-existent subdomains of DNSSEC-signed domains after the first few queries.
- QNAME Minimisation: Verifying QNAME minimisation directly requires observing the outgoing DNS queries from Unbound using tools like
Part 2: Configuring Unbound to Forward All Queries to a DoT Resolver
This part changes Unbound's behavior from a full recursive resolver to a caching forwarder that uses an encrypted DoT upstream. This will override the full recursion behavior.
-
Choose a Public DoT Resolver: We'll use Cloudflare DNS as an example. Their DoT servers are:
- IPv4:
1.1.1.1
,1.0.0.1
- IPv6:
2606:4700:4700::1111
,2606:4700:4700::1001
- TLS Server Name (for certificate validation):
cloudflare-dns.com
- DoT Port:
853
Other options include Quad9 (
dns.quad9.net
), Google Public DNS, etc. Each will have its own IPs and TLS server name. - IPv4:
-
Edit
unbound.conf
: -
Add a
forward-zone:
block. If you already haveqname-minimisation
andaggressive-nsec
from Part 1, you can leave them; they won't have much effect if all queries are forwarded, but they don't hurt. Place this block outside theserver:
block, typically at the end of the file or alongside other zone configurations if you have them.# ... server: block ends ... forward-zone: name: "." # Apply to all queries # Cloudflare DoT servers forward-addr: 1.1.1.1@853#cloudflare-dns.com forward-addr: 1.0.0.1@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com forward-tls-upstream: yes # Optional: Ensure your system's CA certificates are up-to-date, or specify a bundle # This usually goes inside the server: block if needed globally # server: # tls-cert-bundle: "/etc/ssl/certs/ca-certificates.crt" # Example for Debian/Ubuntu
- Important: If your server does not have IPv6 connectivity, remove or comment out the IPv6
forward-addr
lines, or ensure Unbound'sdo-ip6: no
is set in theserver:
block to prevent attempts to reach IPv6 forwarders.
- Important: If your server does not have IPv6 connectivity, remove or comment out the IPv6
-
Save the file and exit the editor.
-
Check the configuration syntax:
Fix any errors. -
Restart Unbound (Recommended for forwarding changes): While
Check its status:reload
might work, arestart
is often safer for significant changes like adding global forwarding.sudo systemctl status unbound
. -
Verification:
-
Basic Query Test: From a client configured to use your Unbound server (or from the server itself using
These should still resolve successfully. The source of the answer will now be your Unbound server, which got its answer from Cloudflare over DoT.dig @127.0.0.1 ...
), perform some DNS lookups: -
(Advanced) Observing Encrypted Traffic with
Now, from a client, perform a DNS query for a domain that is unlikely to be in Unbound's cache (e.g., a random subdomain or a domain you haven't visited recently). You should see encrypted traffic on port 853 between your Unbound server and one of Cloudflare's IPs (e.g., 1.1.1.1). You should not see cleartext DNS queries on port 53 going out from your Unbound server to other authoritative servers (except perhaps for root priming if that still happens before forwarding kicks in for some reason, but typical queries for domains will go via DoT).tcpdump
ortshark
: This is the definitive way to confirm DoT is working. On your Unbound server, listen for traffic on port 853 (DoT's standard port). -
Check Unbound logs: Increase verbosity temporarily if needed (
Look for messages indicating connections to the forwarders. You might see lines about TLS handshakes or forwarding actions. For example, withverbosity: 2
or3
inunbound.conf
, thensudo systemctl restart unbound
).verbosity: 2
, you might see:(Log messages can vary by Unbound version and specific query flow).... unbound[pid:tid]: info: resolving www.example.com. A IN ... unbound[pid:tid]: info: priming . IN NS ... unbound[pid:tid]: info: response for . IN NS cache_miss_no_ad_referral ... unbound[pid:tid]: info: forwarding query . NS IN ... unbound[pid:tid]: info: SSL handshake with 1.1.1.1 port 853 ... unbound[pid:tid]: info: Connected via SSL to 1.1.1.1
-
To revert to full recursive mode (disable DoT forwarding):
- Edit
/etc/unbound/unbound.conf
. - Comment out or delete the entire
forward-zone:
block you added. - Save the file.
- Check syntax:
sudo unbound-checkconf /etc/unbound/unbound.conf
. - Restart Unbound:
sudo systemctl restart unbound
.
This workshop demonstrated how to enable QNAME minimisation and aggressive NSEC for enhanced privacy and performance in recursive mode. It also showed how to configure Unbound to forward queries over DoT, encrypting its upstream communications. Choose the mode of operation (full recursion or DoT/DoH forwarding) that best suits your privacy needs and trust model.
6. Performance Tuning and Caching
Unbound is designed for high performance, but its default settings are often conservative to suit a wide range of hardware. By understanding and tuning Unbound's caching mechanisms, threading, and network buffers, you can optimize its performance for your specific workload and server resources, leading to faster query responses and a more efficient resolver.
Understanding Unbound's Cache
Unbound maintains several types of caches to store DNS information and speed up responses:
-
RRset Cache (Resource Record Set Cache):
- Stores individual DNS Resource Record Sets (RRsets). An RRset is a collection of all records of a given type for a specific name (e.g., all A records for
www.example.com
). - When Unbound resolves a query, it stores the RRsets it fetches from authoritative servers in this cache.
- Directive:
rrset-cache-size: <bytes>
- Example:
rrset-cache-size: 256m
(256 megabytes) - A larger RRset cache can hold more unique DNS records, increasing the cache hit rate, especially if you resolve many different domains. The memory specified is a hard limit.
- Stores individual DNS Resource Record Sets (RRsets). An RRset is a collection of all records of a given type for a specific name (e.g., all A records for
-
Message Cache:
- Stores entire DNS response messages for queries Unbound has recently processed.
- When an identical query comes in, Unbound can serve the full response directly from this cache, which is faster than reassembling it from the RRset cache.
- Directive:
msg-cache-size: <bytes>
- Example:
msg-cache-size: 128m
(128 megabytes) - A larger message cache is beneficial if you receive many identical queries.
-
Infrastructure Cache:
- Stores information about authoritative name servers, such as their round-trip times (RTT), EDNS support, and whether they are lame (misconfigured or unresponsive).
- This helps Unbound choose the best available authoritative servers for future queries.
- Directives like
infra-cache-slabs
,infra-cache-numhosts
,infra-cache-min-rtt
control its behavior. Defaults are usually fine for most setups.
-
Key Cache (for DNSSEC):
- Stores DNSKEY and DS records used for DNSSEC validation. This speeds up the validation process for frequently queried signed zones.
- Directive:
key-cache-size: <bytes>
(often part ofrrset-cache-size
in newer Unbound versions or managed implicitly) - Modern Unbound versions often manage key cache sizing more dynamically as part of the RRset cache. The explicit
key-cache-size
might be less prominent or deprecated in favor of letting the RRset cache handle DNSKEYs.
Cache Sizing Strategy:
- The optimal cache sizes depend on available RAM and the query load.
rrset-cache-size
is generally more critical and should be larger thanmsg-cache-size
. A common ratio is 2:1 or 4:1 for RRset to Message cache.- Monitor Unbound's memory usage and cache hit rates (
unbound-control stats_noreset
) to fine-tune these values. If memory usage is too high, reduce cache sizes. If cache hit rates are low and you have RAM to spare, consider increasing them. - The memory values are for the data itself; Unbound also uses memory for data structures, threads, etc. So, total memory usage will be higher than just the sum of these cache sizes.
- Example for a server with 2GB RAM dedicated to Unbound (this is generous for many setups):
For a Raspberry Pi or small VM with 512MB-1GB total RAM, more modest values like
rrset-cache-size: 64m
andmsg-cache-size: 32m
(or even32m
/16m
) would be more appropriate. Start with defaults or small values and increase gradually if needed.
Cache TTLs (cache-min-ttl
, cache-max-ttl
)
These directives control the Time-To-Live (TTL) values Unbound respects for cached records:
-
cache-min-ttl: <seconds>
- The minimum TTL Unbound will use for a cached record, even if the authoritative server provided a shorter TTL.
- Default:
0
(respects the authoritative TTL). - Setting this to a higher value (e.g.,
300
for 5 minutes, or3600
for 1 hour) can increase cache longevity and hit rates, especially for records with very short authoritative TTLs. - Caution: This can cause Unbound to serve stale data for longer than intended if the authoritative record changes and had a legitimately short TTL for rapid updates (e.g., for dynamic DNS or load balancing). Use with care.
cache-min-ttl: 60
(A modest override)
-
cache-max-ttl: <seconds>
- The maximum TTL Unbound will use for a cached record, even if the authoritative server provided a longer TTL.
- Default:
86400
(1 day). - Reducing this can force Unbound to re-validate records more frequently, ensuring fresher data but potentially lowering cache hit rates and increasing load. Increasing it can improve cache hits but increases the risk of serving stale data.
- The default is generally a good balance.
cache-max-ttl: 604800
(1 week - use if you prioritize cache hits over absolute freshness for very long TTL records)
Prefetching (prefetch: yes
)
Prefetching is a feature where Unbound attempts to refresh popular cache entries before they expire. When a query arrives for an item in the cache that is about to expire, Unbound serves the stale (but still valid) data to the client immediately and simultaneously initiates a background query to the authoritative server to refresh the item.
-
prefetch: <yes/no>
- Default:
no
in some older versions, oftenyes
in newer ones. Explicitly setting it is good. - Enabling prefetching can significantly improve perceived performance for frequently accessed domains because clients are less likely to experience the latency of a full recursive lookup if the item was prefetched.
prefetch: yes
- Default:
-
prefetch-key: <yes/no>
- Default:
yes
(ifprefetch: yes
). - Specifically enables prefetching for DNSKEY RRsets. This is beneficial for maintaining DNSSEC validation capabilities smoothly, as it ensures DNSKEYs are refreshed before they expire.
prefetch-key: yes
- Default:
Serve Expired (serve-expired: yes
)
This feature allows Unbound to serve records from its cache even after their TTL has expired, under certain conditions. This is primarily a resilience feature to improve availability if authoritative servers for a domain become unreachable.
-
serve-expired: <yes/no>
- Default:
no
. - When
yes
, if Unbound has an expired record in cache and fails to contact the authoritative servers to refresh it (e.g., due to network issues or server downtime), it can serve the expired data to the client. - This is better than returning a
SERVFAIL
if the data, though stale, is likely still usable. serve-expired: yes
- Default:
-
serve-expired-ttl: <seconds>
- Default:
0
. This means when serving expired data, the TTL in the response is 0, telling clients not to cache it further. - You can set a small positive TTL here (e.g.,
30
or60
seconds) to allow clients to cache the stale data for a short period, reducing repeated queries for the same expired data if the authoritative servers remain down. serve-expired-ttl: 30
- Default:
-
serve-expired-reply-ttl: <seconds>
- Default:
30
. This is the TTL that Unbound itself uses internally for the served expired entry if the client did not ask for DNSSEC. - The
serve-expired-ttl
is what is sent to the client,serve-expired-reply-ttl
is how long Unbound considers it 'serve-expired-able'.
- Default:
-
serve-expired-client-timeout: <milliseconds>
- Default:
0
. If greater than 0, Unbound will first try to resolve and if that takes longer than this timeout, it will serve an expired answer if available. This provides a quick (though possibly stale) answer if resolution is slow.
- Default:
Using serve-expired
judiciously: While it improves availability, remember that served data is stale. It's a trade-off between availability and data freshness/accuracy.
Optimizing Threading (num-threads
)
Unbound is a multi-threaded application. The num-threads
directive controls how many worker threads Unbound creates to handle incoming queries and perform recursive lookups.
num-threads: <number>
- Default: Often
1
. - A good general recommendation is to set this to the number of CPU cores available on your server.
- You can find the number of cores with
nproc
orlscpu
. num-threads: 4
(for a 4-core CPU)- Each thread handles queries independently. More threads can improve concurrency and throughput, especially on multi-core systems under heavy load.
- However, too many threads can also lead to increased context switching and contention for resources (like the cache), potentially degrading performance. Experimentation might be needed for very high-load scenarios. For most self-hosted resolvers, matching the core count is a good starting point.
- Default: Often
Tuning Network Buffers (so-rcvbuf
, so-sndbuf
)
These directives control the size of the socket send and receive buffers used by Unbound for its network communications. Larger buffers can sometimes help with performance under high load or on networks with high latency, especially for TCP traffic, by allowing more data to be "in flight."
-
so-rcvbuf: <bytes>
- Socket receive buffer size.
- Default: System default (often small).
- Can be increased, e.g.,
so-rcvbuf: 4m
(4 megabytes).
-
so-sndbuf: <bytes>
- Socket send buffer size.
- Default: System default.
- Can be increased, e.g.,
so-sndbuf: 4m
.
Considerations:
- The operating system might have limits on maximum buffer sizes. Unbound will try to set the requested size but might get a smaller value if it exceeds system limits. You might need to adjust kernel parameters (e.g.,
sysctl net.core.rmem_max
,net.core.wmem_max
) to allow larger buffer sizes. - For most typical self-hosted Unbound instances (personal, small network), the default buffer sizes are usually adequate. Tuning these is more relevant for very high-performance, high-traffic resolvers.
- Setting these too high can consume more kernel memory per socket.
Other performance-related settings:
outgoing-range: <number>
:
Number of ports Unbound can use for outgoing queries. Default is 1024. Increasing this can help if Unbound is making a very large number of concurrent outgoing queries, to avoid port exhaustion. Max is 65535.outgoing-range: 8192
num-queries-per-thread: <number>
:
Number of queries each thread can handle concurrently. Default is 1024. Derived fromoutgoing-range
/num-threads
.rrset-roundrobin: yes
:
If multiple records exist in an RRset (e.g., multiple A records for a load-balanced service), this option makes Unbound rotate the order of records in the response, providing a simple form of client-side load balancing. Defaultno
.minimal-responses: no
:
Defaultno
. Ifyes
, Unbound minimizes the size of responses by omitting optional data (like authority and additional sections if not strictly needed). Can save a tiny bit of bandwidth but might break some poorly implemented clients or applications that expect fuller responses. Generally leave asno
.
Workshop Optimizing Unbound Cache and Performance Settings
This workshop will guide you through adjusting Unbound's cache sizes, enabling prefetching and serve-expired features, and setting the number of threads. We will also look at how to use unbound-control stats_noreset
to get some insight into cache performance.
Prerequisites
- Your Unbound server set up and running.
- SSH access to your Unbound server with sudo privileges.
unbound-control
configured and working.- Knowledge of your server's CPU core count and available RAM.
Step 1: Analyzing Current System Resources and Baseline Stats
-
Determine CPU Cores: On your Unbound server, run:
Note the number of cores. Let's say it's2
for this workshop. -
Estimate Available RAM for Unbound: Check your server's total and free memory:
Decide how much RAM you can reasonably allocate to Unbound. For a dedicated resolver on a VM with 1GB RAM, you might allocate 128-256MB for Unbound's caches initially. If it's a shared server, be more conservative. Let's assume we can allocate around 64MB for caches initially for a small setup. -
Get Baseline Cache Statistics (Optional but good): If Unbound has been running for a while and handling queries, get current stats:
Look for lines like:total.num.queries
total.num.cachehits
total.num.cachemiss
mem.cache.rrset
(memory used by RRset cache)mem.cache.message
(memory used by message cache) Calculate your current cache hit rate:(cachehits / queries) * 100%
. Note these values down. If it's a fresh setup, these numbers will be small.
Step 2: Adjusting Cache Sizes Based on System Resources and Load
-
Edit
unbound.conf
: -
Modify
rrset-cache-size
andmsg-cache-size
in theserver:
block. Based on our example (2-core CPU, ~64MB for caches for a small setup): A 2:1 or 4:1 ratio for RRset to Message cache is common. Let's tryrrset-cache-size: 40m
andmsg-cache-size: 20m
. (Total ~60MB). If you have more RAM (e.g., a server with 4GB RAM, and you can dedicate 512MB to Unbound caches):rrset-cache-size: 340m
msg-cache-size: 170m
Add or modify these lines:
Choose values appropriate for your system. It's better to start smaller and increase if needed and if memory allows.
Step 3: Enabling Prefetching and Serve Expired
These features generally improve user experience.
- Still in
unbound.conf
, add/ensure these directives in theserver:
block:
Step 4: Experimenting with num-threads
-
Still in
If you had 4 cores, you'd useunbound.conf
, setnum-threads
according to your CPU core count in theserver:
block: Using our example of a 2-core CPU:num-threads: 4
. -
Save the
unbound.conf
file and exit the editor. -
Check configuration syntax:
Fix any errors. -
Restart Unbound to apply these changes: Cache size and thread changes typically require a restart.
Verify it started correctly:sudo systemctl status unbound
.
Step 5: Monitoring Performance Changes
-
Allow Unbound to run and serve queries for a while (e.g., a few hours or a day, depending on your query volume) to build up its cache and for stats to become meaningful.
-
Check statistics again:
- Compare
total.num.queries
,total.num.cachehits
,total.num.cachemiss
to your baseline (if you had one). Has the cache hit rate improved?new_hit_rate = (total.num.cachehits / total.num.queries) * 100%
- Check
mem.cache.rrset
andmem.cache.message
. Are they close to the*-cache-size
limits you set? If they are consistently much lower, your configured sizes might be too large for your current workload (or the workload hasn't been high enough yet to fill them). If they are at the limit and cache misses are still high, you might benefit from larger caches if RAM permits. - Look for
num.query.prefetch
. If it's greater than zero, prefetching is working. - If
serve-expired: yes
is active and authoritative servers were down for some domains you queried, you might seenum.expired
or similar counters increment.
- Compare
-
Observe Memory Usage: Use
top
orhtop
(and filter forunbound
) orps aux | grep unbound
to see how much memory Unbound is actually consuming. This will be more than justrrset-cache-size + msg-cache-size
due to overhead, thread stacks, buffers, etc. Ensure it's within acceptable limits for your server. -
Iterate (If Necessary):
- If memory usage is too high, reduce cache sizes.
- If cache hit rate is still low and you have RAM, consider cautiously increasing cache sizes.
- If your server's CPU load is consistently high while Unbound is busy, ensure
num-threads
matches your core count. If it's a very high-end server with many cores (e.g., 16+), you might experiment withnum-threads
being slightly higher or lower than core count, but for most, matching core count is optimal.
By following this workshop, you've tuned some key performance parameters for Unbound. Regular monitoring of statistics and system resources will help you find the sweet spot for your specific environment and workload. Remember that performance tuning is often an iterative process.
7. DNSSEC Validation
DNS Security Extensions (DNSSEC) are a suite of specifications for securing certain kinds of information provided by the Domain Name System (DNS) as used on Internet Protocol (IP) networks. DNSSEC provides origin authentication of DNS data, data integrity, and authenticated denial of existence. In simpler terms, it allows your resolver to cryptographically verify that the DNS responses it receives are authentic and have not been tampered with. Unbound is a DNSSEC-validating resolver by default.
What is DNSSEC?
DNS was originally designed without strong security features. This left it vulnerable to various attacks, most notably:
- DNS Cache Poisoning (DNS Spoofing): An attacker tricks a resolver into caching a fraudulent DNS record (e.g., mapping
www.bank.com
to a malicious IP address). Clients using this resolver are then misdirected. - DNS Forgery: An attacker intercepts a DNS query and sends back a forged response.
DNSSEC addresses these threats by adding digital signatures to DNS data.
Key Concepts (RRSIG, DNSKEY, DS, NSEC, NSEC3)
DNSSEC introduces several new DNS record types:
-
RRSIG (Resource Record Signature): Contains a digital signature for an RRset (e.g., all A records for a name). This signature is created by the zone owner using their private key. Your resolver can verify this signature using the zone's public key.
- Example:
www.example.com. A 1.2.3.4
would have a correspondingwww.example.com. RRSIG A ... (signature data) ...
- Example:
-
DNSKEY (DNS Public Key): Contains the public key(s) for a DNS zone. This public key is used to verify RRSIG records within that zone. There are two main types of DNSKEYs:
- ZSK (Zone Signing Key): Used to sign RRsets within the zone.
- KSK (Key Signing Key): Used to sign the DNSKEY RRset itself (i.e., sign all keys in the zone, including ZSKs and the KSK itself). The KSK is what is used to link to the parent zone in the chain of trust.
-
DS (Delegation Signer): This record is placed in the parent zone (e.g., the
.com
TLD zone would have a DS record forexample.com
ifexample.com
is DNSSEC-signed). The DS record contains a hash (digest) of a KSK from the child zone (example.com
). This creates a secure link, or chain of trust, from the parent zone to the child zone. The DS record itself is signed by the parent zone's key. -
NSEC (Next Secure): As discussed in "Aggressive NSEC," this record provides authenticated denial of existence by creating a sorted chain of all names in a zone. If you query for
nonexistent.example.com
, andalpha.example.com
andcharlie.example.com
exist, an NSEC record foralpha.example.com
might state that the next name ischarlie.example.com
, provingnonexistent
(which falls between them alphabetically) does not exist. The NSEC record itself is signed. -
NSEC3 (Next Secure version 3): An alternative to NSEC that uses hashed names to prevent "zone walking" (enumerating all names in a zone). It also provides authenticated denial of existence.
- NSEC3PARAM: A record in a zone that specifies the parameters for NSEC3 usage (hash algorithm, iterations, salt).
The Chain of Trust
DNSSEC validation relies on a hierarchical "chain of trust" that mirrors the DNS hierarchy itself. This chain starts from a pre-configured trust anchor, which is typically the public KSK for the DNS root zone (.
).
- Root Trust Anchor: Your resolver (Unbound) is configured with the root zone's public KSK. This is the ultimate point of trust.
- Root Zone Validation: Unbound fetches the DNSKEY RRset for the root zone and its RRSIG. It verifies the RRSIG using its configured root trust anchor. This authenticates the root's ZSKs.
- TLD Validation: When resolving
www.example.com
, Unbound queries the root for.com
NS records. The root also provides DS records for.com
(if.com
is signed). These DS records are signed by the root's ZSK. Unbound verifies this signature. The DS record for.com
contains a hash of.com
's KSK. - Unbound then queries a
.com
TLD server forexample.com
's NS records. It also fetches.com
's DNSKEY RRset (containing its KSKs and ZSKs) and verifies it using the DS record obtained from the root. This authenticates.com
's keys. - Domain Validation: The
.com
TLD server provides DS records forexample.com
(ifexample.com
is signed). These are signed by.com
's ZSK. Unbound verifies this. The DS record forexample.com
contains a hash ofexample.com
's KSK. - Unbound then queries
example.com
's authoritative server forwww.example.com
's A record and its RRSIG. It also fetchesexample.com
's DNSKEY RRset and verifies it using the DS record obtained from the.com
TLD. This authenticatesexample.com
's ZSK. - Finally, Unbound uses
example.com
's authenticated ZSK to verify the RRSIG for thewww.example.com
A record.
If every signature in this chain verifies correctly, the A record for www.example.com
is considered "Secure" or "Authentic." If any part of the chain fails validation (e.g., a signature is invalid, a DS record doesn't match a KSK, a record is missing its RRSIG), Unbound considers the data "Bogus" and will typically return a SERVFAIL
error to the client, preventing the use of potentially compromised data.
If a zone is not DNSSEC-signed (e.g., it has no DS record in the parent, or no DNSKEY/RRSIG records), Unbound considers the data "Insecure" but will still return it if the parent zone up to that point was secure. The ad
(Authentic Data) bit in the DNS response header is set by Unbound only if all data in the answer is secure and all CNAMEs/DNAMEs in the path are secure.
Enabling DNSSEC Validation in Unbound (auto-trust-anchor-file:
)
Unbound is a DNSSEC-validating resolver by default. The key directive for managing the DNSSEC root trust anchor is auto-trust-anchor-file:
.
server:
# ... other settings ...
# Path to the file where Unbound stores and updates the root trust anchor(s).
# This file needs to be writable by the 'unbound' user if Unbound is to manage it automatically.
# Common paths:
# Debian/Ubuntu: /var/lib/unbound/root.key
# CentOS/RHEL: /etc/unbound/root.key or /var/lib/unbound/root.key
auto-trust-anchor-file: "/var/lib/unbound/root.key"
# Optional: Specify initial trust anchors if the auto-trust-anchor-file is empty
# or for other specific anchors. The root anchor is usually managed automatically.
# trust-anchor: ". DNSKEY 257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQ..." (example, do not use)
# DNSSEC validation hardness levels (defaults are usually good):
# harden-dnssec-stripped: yes # (Default) Protects against attackers removing DNSSEC records.
# val-log-level: 0 # (Default) Set to 1 or 2 for very verbose DNSSEC validation logging.
# Use for debugging only as it's extremely verbose.
# val-permissive-mode: no # (Default) If yes, validates but returns data even if bogus. For testing.
How auto-trust-anchor-file
works (RFC 5011 - Automated Updates of DNS Security (DNSSEC) Trust Anchors):
-
Initialization: If the
auto-trust-anchor-file
is empty or does not exist, Unbound needs an initial root key. It might come with built-in "bootstrap" keys or you might need to initialize it manually usingunbound-anchor
.Packaged Unbound installations often run# To initialize or update the root anchor manually (run as root or user with write perms to the file): sudo unbound-anchor -a /var/lib/unbound/root.key -v # The -v is for verbosity. This tool fetches root hints and the root DNSKEYs, # validates them, and writes the trusted KSK to the file.
unbound-anchor
during installation or as part of the service startup to ensureroot.key
is populated. -
Validation and Updates: When Unbound starts, it loads the trust anchors from this file. During operation, it monitors for changes to the root KSKs according to RFC 5011. If a new KSK is introduced and properly signed by the old KSK (during a key rollover period), Unbound will validate it and eventually update the
auto-trust-anchor-file
with the new key, making it a "managed" trust anchor. This ensures Unbound stays current with root key rollovers without manual intervention.
Permissions: The unbound
user (or whatever user Unbound runs as) needs read/write access to the auto-trust-anchor-file
for automatic updates to work. If it only has read access, Unbound can validate using the existing anchor but cannot update it automatically; unbound-anchor
would need to be run periodically as root.
Managing Trust Anchors
- Root Anchor: The primary trust anchor is the root KSK, managed via
auto-trust-anchor-file
. -
Other Trust Anchors (
These are static and not automatically updated via RFC 5011 unless also managed bytrust-anchor:
): You can manually specify other trust anchors for specific domains using thetrust-anchor:
directive. This is less common and usually only needed for private DNSSEC-signed zones that are not part of the global public DNS chain of trust, or for testing.unbound-anchor
with specific configurations. -
Negative Trust Anchors (
Unbound will then not attempt DNSSEC validation for this domain or any subdomains, treating all data from it as insecure. Use this with extreme caution as it bypasses DNSSEC protection for that domain.domain-insecure:
): If you know a domain's DNSSEC is broken but you still need to resolve it (and are willing to accept the risk of insecure data from it), you can declare it "insecure":
Troubleshooting DNSSEC Validation Issues
If DNSSEC validation fails for a domain that should be secure, Unbound will typically return SERVFAIL
.
-
Check Unbound Logs: Increase
verbosity
(e.g., to2
) andval-log-level
(e.g., to1
or2
- very verbose!) inunbound.conf
temporarily. Restart Unbound and try the query again. The logs will provide detailed information about which part of the validation process failed (e.g., "signature expired," "DS not found," "keyset not secure"). Remember to revert verbosity after debugging. -
Verify Root Trust Anchor: Ensure
auto-trust-anchor-file
exists and is up-to-date. Try runningsudo unbound-anchor -a /path/to/root.key -v
. If it reports errors fetching or validating root keys, there might be network issues or a problem with the root zone itself (rare). -
Check System Time: DNSSEC signatures are time-sensitive. If your server's clock is significantly off, DNSSEC validation will fail (signatures will appear expired or not yet valid). Ensure your server's time is synchronized using NTP (Network Time Protocol).
-
Use Online DNSSEC Debuggers: For a problematic domain, tools like:
- DNSViz (dnsviz.net): Provides a visual representation of the DNSSEC chain of trust and highlights errors.
- Verisign DNSSEC Debugger (dnssec-debugger.verisignlabs.com): Analyzes a domain and reports DNSSEC status.
- ISC DLV (delv):
delv
is a command-line tool similar todig
but with a focus on DNSSEC validation debugging.delv @your_resolver_ip example.com A +rtrace
can show the validation path.
-
Check for Path MTU Discovery (PMTUD) issues or firewalls blocking large DNS UDP packets: DNSSEC responses can be larger than standard DNS responses due to the inclusion of RRSIG and DNSKEY records. If UDP packets carrying these responses are fragmented or dropped by firewalls or routers that don't handle large UDP packets or ICMP "Packet Too Big" messages correctly, DNSSEC can fail. Unbound will typically retry over TCP, but if TCP is also blocked or problematic, resolution fails. Ensure your firewall allows related ICMP messages for PMTUD and doesn't overly restrict UDP packet sizes or block DNS over TCP (port 53).
-
Temporarily Use
val-permissive-mode: yes
(for diagnosis only): If you setval-permissive-mode: yes
, Unbound will log DNSSEC validation errors but still return the data. This can help identify if the problem is indeed DNSSEC-related or something else. Do not leave this on in production.
Workshop Enabling and Verifying DNSSEC Validation
This workshop will ensure your Unbound server is correctly configured for DNSSEC validation, that the root trust anchor is primed, and then test validation against known good, known bad, and unsigned domains.
Prerequisites
- Your Unbound server set up and running.
- SSH access to your Unbound server with sudo privileges.
dig
utility available on a client or the server itself.- System time on the Unbound server is reasonably accurate.
Step 1: Ensuring auto-trust-anchor-file
is Correctly Configured and Primed
-
Check
Make sure it points to a valid path (e.g.,unbound.conf
forauto-trust-anchor-file
:/var/lib/unbound/root.key
or/etc/unbound/root.key
). -
Prime/Update the Root Anchor Manually (if unsure or first time): Run
You should see output indicating it's fetching root hints and keys, and hopefully "success." If it fails, address any network or permission errors reported. Theunbound-anchor
to ensure the root key file is populated and up-to-date. Replace/var/lib/unbound/root.key
with your actual path if different.unbound
user needs read/write access to this file for Unbound to manage it automatically. If permissions are wrong,chown unbound:unbound /var/lib/unbound/root.key
(adjust user/group if different on your system). -
Restart Unbound to ensure it loads the anchor:
Check logs for any errors related to loading trust anchors:sudo journalctl -u unbound -e
.
Step 2: Verifying DNSSEC Validation is Active
We'll use dig
to query through your Unbound resolver.
Replace @127.0.0.1
with @YOUR_UNBOUND_SERVER_IP
if testing from a separate client.
-
Test a DNSSEC-signed domain known to be valid:
In thesigok.verteiltesysteme.net
is designed for this.dig
output, look for theflags:
line in the header section. You should see thead
(Authentic Data) flag present if DNSSEC validation was successful. Example:;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
Thead
flag is the key indicator from Unbound that it considers the answer cryptographically verified. -
Test a DNSSEC-signed domain known to have an invalid signature:
This query should result insigfail.verteiltesysteme.net
is designed for this.status: SERVFAIL
(Server Failure). Unbound detected the invalid signature and refused to return the bogus data. This is the correct behavior for a DNSSEC-validating resolver. If you get an IP address andstatus: NOERROR
, DNSSEC validation is NOT working correctly. Revisit Step 1 and check logs. -
Test a domain that is NOT DNSSEC-signed:
This query should result inexample.com
is (as of this writing) not DNSSEC signed.status: NOERROR
and return an IP address. However, thead
flag should NOT be present in theflags:
line because there was no DNSSEC information to validate. Unbound considers this data "Insecure."
Step 3: Using dig +dnssec
to Inspect DNSSEC Records
The +dnssec
option tells dig
to request DNSSEC records (like RRSIGs) along with the query. This allows you to see the signatures that Unbound is verifying.
-
Query a valid signed domain with
In the+dnssec
:ANSWER SECTION
, you will now see not only theA
record but also anRRSIG A
record, which is the signature for that A record. Thead
flag should still be present in the header. -
Observe the TTL of DNSSEC records: RRSIGs and DNSKEYs also have TTLs. Unbound caches these as well.
Step 4: Observing unbound-control stats
for DNSSEC-related Counters
Let Unbound run for a bit and serve some queries for DNSSEC-signed domains.
- View statistics:
-
Look for DNSSEC-related counters:
num.query.flags.AD
: Number of queries where upstream set AD bit (often from forwarders, or when Unbound itself validates).num.answer.secure
: Number of answers Unbound marked secure.num.answer.bogus
: Number of answers Unbound marked bogus (validation failure). This should be non-zero if you've queriedsigfail.verteiltesysteme.net
.num.rrset.bogus
: Number of RRsets marked bogus.mem.cache.validator
: Memory used by the validator module (for DNSKEYs, etc.).mem.cache.key
: Memory used by key cache.
An increase in
num.answer.secure
after querying valid signed domains, andnum.answer.bogus
after queryingsigfail
domains, further confirms DNSSEC validation is active.
If all these tests behave as described (especially the ad
flag for sigok
and SERVFAIL
for sigfail
), your Unbound resolver is correctly performing DNSSEC validation, significantly enhancing the security and trustworthiness of the DNS responses it provides.
8. Local DNS Records and Overrides
One of the powerful advantages of self-hosting a DNS resolver like Unbound is the ability to define custom DNS records for your local network or override public DNS records. This feature, often referred to as "split-horizon DNS" in a broader sense, allows you to:
- Resolve custom hostnames (e.g.,
my-nas.lan
,router.local
) to internal IP addresses. - Block access to unwanted domains (e.g., ad servers, trackers, malicious sites) by redirecting them.
- Override public IP addresses for certain domains with internal IPs, useful for accessing local services that might also have public DNS entries (e.g., a web server hosted internally).
Unbound provides several directives for this, primarily local-zone:
and local-data:
.
Defining Local Zones (local-zone:
)
The local-zone:
directive defines a zone for which Unbound is considered "authoritative" locally. Queries for names within this zone will be answered by Unbound directly using local-data:
entries or predefined actions, rather than being recursively resolved.
Syntax: local-zone: "<zone_name>" <type>
Common <type>
values for local-zone:
:
static
:
(Default if not specified) Unbound provides answers fromlocal-data
for this zone. If nolocal-data
matches, it returnsNXDOMAIN
(Non-Existent Domain). It does not recurse for names in this zone.refuse
:
All queries for this zone and subdomains are refused (RCODE REFUSED). Useful for blocking entire TLDs or domains you never want to resolve.deny
:
Silently drops all queries for this zone. Client will time out.transparent
:
Unbound resolves queries for this zone as usual (i.e., recursively). However,local-data
entries within this zone can still override specific records. This is useful if you want to resolve most ofexample.com
normally but overrideinternal.example.com
.redirect
:
All queries for this zone are answered with an A or AAAA record specified in alocal-data
entry that is the zone itself. E.g.,local-zone: "ads.example.com." redirect
andlocal-data: "ads.example.com. A 127.0.0.1"
. All subdomains ofads.example.com
will also resolve to127.0.0.1
.nodefault
:
If a query for a name in this zone does not have a matchinglocal-data
entry, Unbound does not automatically return NXDOMAIN. Instead, it continues to process other local-zone types or local-data if they exist. Useful for complex layering.typetransparent
:
Liketransparent
, but Unbound will also look up NS records for the zone from the cache or via iteration. Useful for serving a local zone but still being able to find the authoritative servers if needed for specific record types not covered by local-data.inform
:
Unbound resolves normally but also logs queries for this zone.inform_deny
:
Likeinform
but also denies the query.always_transparent
:
Liketransparent
, but even if the name is a CNAME alias that points outside the zone, the original name is still treated as transparent.always_refuse
:
Likerefuse
, but CNAMEs are not followed before refusing.always_nxdomain
:
Likestatic
but returns NXDOMAIN even if CNAMEs are involved.
Serving custom records for internal hostnames
Let's say you want to define a local domain like home.arpa
(using .arpa
for local zones is a good practice as per RFC 6761, though .lan
, .internal
, .local
are also commonly used but .local
is officially for mDNS/Bonjour).
# In unbound.conf, within the server: block
server:
# ... other settings ...
# Define a local zone for home.arpa
local-zone: "home.arpa." static
# Define records within this zone
local-data: "router.home.arpa. IN A 192.168.1.1"
local-data: "nas.home.arpa. IN A 192.168.1.10"
local-data: "nas.home.arpa. IN AAAA 2001:db8:cafe::10" # IPv6 for NAS
local-data: "printer.home.arpa. IN A 192.168.1.15"
# You can also create aliases (CNAMEs)
local-data: "fileserver.home.arpa. IN CNAME nas.home.arpa."
- Querying
router.home.arpa
will return192.168.1.1
. - Querying
nas.home.arpa
for A will return192.168.1.10
, for AAAA will return2001:db8:cafe::10
. - Querying
fileserver.home.arpa
will resolve to the IP(s) ofnas.home.arpa
. - Querying
nonexistent.home.arpa
will returnNXDOMAIN
because the zone type isstatic
.
Blocking domains (Ad-blocking, malware protection)
You can use local-zone
to block domains by redirecting them to a non-routable IP address (like 0.0.0.0
or ::1
for IPv6) or by refusing queries.
Method 1: Redirecting specific domains using local-data
This is good for a small list of domains.
server:
# ...
# To block exampleadserver.com:
# For this to work without a specific local-zone for "exampleadserver.com.",
# Unbound needs to allow local-data to override public DNS.
# This is often default behavior, or can be influenced by `domain-insecure`
# if the domain is DNSSEC signed (as local-data isn't signed by the public key).
# A common pattern for ad-blocking is to use a "transparent" zone for the TLDs
# if you are overriding many domains, or just add local-data directly.
# For maximum effect, treat these as insecure locally to ensure override.
domain-insecure: "exampleadserver.com." # Ensures your local-data takes precedence over DNSSEC
local-zone: "exampleadserver.com." static # Make it a static zone so only local-data applies
local-data: "exampleadserver.com. IN A 0.0.0.0"
local-data: "exampleadserver.com. IN AAAA ::1"
# And for its subdomains, you'd need explicit entries or use the redirect type:
local-data: "www.exampleadserver.com. IN A 0.0.0.0"
local-data: "www.exampleadserver.com. IN AAAA ::1"
Method 2: Using local-zone
with type redirect
for broader blocking
This is more efficient for blocking an entire domain and all its subdomains.
server:
# ...
# Block doubleclick.net and all its subdomains
# Define the redirect target for the zone itself
local-data: "doubleclick.net. IN A 0.0.0.0"
local-data: "doubleclick.net. IN AAAA ::1"
# Then define the zone with type redirect
local-zone: "doubleclick.net." redirect
doubleclick.net
, ads.doubleclick.net
, any.subdomain.doubleclick.net
will all return 0.0.0.0
(or ::1
for AAAA queries).
Method 3: Using local-zone
with type refuse
or deny
This completely blocks resolution without redirection.
Using Blocklists:
Manually adding thousands of domains for ad-blocking is impractical. You can use scripts to generate a file containing many local-zone: "domain." redirect
and local-data: "domain. A 0.0.0.0"
entries, and then include this file in your main unbound.conf
:
ad_blocklist.conf
file would contain lines like:
local-zone: "adserver1.com." redirect
local-data: "adserver1.com. A 0.0.0.0"
local-zone: "tracker2.net." redirect
local-data: "tracker2.net. A 0.0.0.0"
...
Using local-data:
for Specific Records
local-data:
is the workhorse for defining actual DNS records for your local zones or for overriding public records.
Syntax: local-data: "<FQDN> [TTL] [CLASS] <TYPE> <RDATA>"
<FQDN>
:
The fully qualified domain name, ending with a dot (e.g.,myhost.home.arpa.
).[TTL]
:
Optional Time-To-Live in seconds. If omitted, Unbound uses a default (e.g., 3600).[CLASS]
:
Optional class, almost alwaysIN
(Internet).<TYPE>
:
DNS record type (A, AAAA, MX, CNAME, TXT, PTR, SRV, etc.).<RDATA>
:
The record data (e.g., IP address for A/AAAA, hostname for MX/CNAME).
Examples:
server:
# ...
# A record (IPv4 Address)
local-data: "webserver.lan. 3600 IN A 192.168.1.50"
# AAAA record (IPv6 Address)
local-data: "webserver.lan. 3600 IN AAAA 2001:db8:cafe::50"
# CNAME record (Alias)
local-data: "www.lan. IN CNAME webserver.lan."
# MX record (Mail Exchange)
# Points to mail.lan. with preference 10
local-data: "lan. IN MX 10 mail.lan."
local-data: "mail.lan. IN A 192.168.1.60" # mail.lan needs an A/AAAA record
# TXT record (Text)
local-data: "info.lan. IN TXT \"This is my local network\""
# SRV record (Service)
# For a service _myservice._tcp on port 1234 at host.lan.
local-data: "_myservice._tcp.lan. IN SRV 0 5 1234 host.lan."
local-data: "host.lan. IN A 192.168.1.70"
Overriding Public DNS:
If you want service.example.com
(a public domain) to resolve to an internal IP for clients using your Unbound resolver:
server:
# ...
# Ensure Unbound will override public DNSSEC for this if it's signed.
# This tells Unbound to not expect DNSSEC for this domain from public internet,
# allowing your local-data (which isn't signed by example.com's keys) to take precedence.
domain-insecure: "service.example.com."
# Define a local zone for it, or just use local-data if the zone type allows overrides.
# If example.com is a public domain, you might want a transparent zone if you only override one subdomain.
local-zone: "example.com." transparent # Allows general resolution but local-data can override
local-data: "service.example.com. IN A 192.168.1.80"
domain-insecure: "service.example.com."
, if service.example.com
is DNSSEC-signed, Unbound might return SERVFAIL
or ignore your local-data
because your local entry isn't part of the valid DNSSEC chain for example.com
.
local-data-ptr:
for Reverse DNS
For reverse DNS lookups (IP address to hostname), you use local-data-ptr:
. Unbound automatically creates the necessary PTR record in the appropriate in-addr.arpa.
(IPv4) or ip6.arpa.
(IPv6) zone.
Syntax: local-data-ptr: "<IP_address> <hostname>"
server:
# ...
# Matching the A records defined earlier:
local-data: "router.home.arpa. IN A 192.168.1.1"
local-data-ptr: "192.168.1.1 router.home.arpa."
local-data: "nas.home.arpa. IN A 192.168.1.10"
local-data-ptr: "192.168.1.10 nas.home.arpa."
local-data: "nas.home.arpa. IN AAAA 2001:db8:cafe::10"
local-data-ptr: "2001:db8:cafe::10 nas.home.arpa."
192.168.1.10
(i.e., dig -x 192.168.1.10
) through your Unbound server will return nas.home.arpa.
.
Redirecting Domains
As shown in the ad-blocking example, the redirect
type for local-zone:
is very powerful for making an entire domain and all its subdomains resolve to a single IP address (or set of addresses if you provide multiple A/AAAA records for the zone name).
server:
# Redirect all of example.com and its subdomains to 10.0.0.1
local-data: "example.com. IN A 10.0.0.1"
local-data: "example.com. IN AAAA ::ffff:10.0.0.1" # Example IPv6
local-zone: "example.com." redirect
foo.bar.example.com
would also result in 10.0.0.1
.
Order of Processing:
Unbound processes local-zone
and local-data
in a specific order. More specific local-data
entries usually take precedence over broader local-zone
actions like redirect
. Multiple local-zone
declarations for the same zone but with different types can lead to complex interactions; typically, you define a zone once.
Workshop Setting Up Local DNS for a Home Network and Basic Ad-Blocking
This workshop will guide you through:
- Defining a local domain (e.g.,
mynet.lan
) for your home network devices. - Adding A, AAAA, and CNAME records for these devices.
- Setting up corresponding PTR records for reverse lookups.
- Implementing a simple ad-blocking mechanism by redirecting a few common ad-serving domains.
Prerequisites
- Your Unbound server set up and running.
- SSH access to your Unbound server with sudo privileges.
- Knowledge of the IP addresses of some devices on your local network (e.g., your router, a NAS, a printer).
- A list of a few ad-serving domains to block (e.g.,
doubleclick.net
,pagead2.googlesyndication.com
).
Step 1: Defining a Local Zone for Your Home Network
- Choose a local domain name. For this workshop, we'll use
mynet.lan
. - Edit
unbound.conf
: - Add a
local-zone
directive formynet.lan
inside theserver:
block: We'll usestatic
type, as we want Unbound to be authoritative and only serve records we explicitly define.
Step 2: Adding local-data
Entries for Internal Services
Let's assume you have the following devices on your network:
- Router:
192.168.1.1
(IPv4),fd00:cafe::1
(IPv6 ULA) - Network Attached Storage (NAS):
192.168.1.100
(IPv4),fd00:cafe::100
(IPv6 ULA) -
Printer:
192.168.1.200
(IPv4 only) -
Still in
unbound.conf
(insideserver:
block), addlocal-data
records for these devices within yourmynet.lan
zone:Make sure FQDNs end with a dot.server: # ... local-zone: "mynet.lan." static # Records for mynet.lan local-data: "router.mynet.lan. IN A 192.168.1.1" local-data: "router.mynet.lan. IN AAAA fd00:cafe::1" local-data: "nas.mynet.lan. IN A 192.168.1.100" local-data: "nas.mynet.lan. IN AAAA fd00:cafe::100" # Create an alias for the NAS local-data: "storage.mynet.lan. IN CNAME nas.mynet.lan." local-data: "printer.mynet.lan. IN A 192.168.1.200"
Step 3: Adding local-data-ptr
for Reverse DNS
To allow reverse lookups (IP to hostname) for these local devices:
- Still in
unbound.conf
(insideserver:
block), addlocal-data-ptr
records:server: # ... previous local-data entries ... # PTR records for reverse lookup local-data-ptr: "192.168.1.1 router.mynet.lan." local-data-ptr: "fd00:cafe::1 router.mynet.lan." local-data-ptr: "192.168.1.100 nas.mynet.lan." local-data-ptr: "fd00:cafe::100 nas.mynet.lan." # No PTR for storage.mynet.lan as it's a CNAME. PTRs point from IP to canonical name. local-data-ptr: "192.168.1.200 printer.mynet.lan."
Step 4: Implementing a Simple Ad-Blocking List
We'll block doubleclick.net
and pagead2.googlesyndication.com
by redirecting them to 0.0.0.0
(IPv4) and ::1
(IPv6 null address).
-
Still in
unbound.conf
(insideserver:
block), addlocal-zone
andlocal-data
for the ad domains: Using theredirect
type forlocal-zone
is efficient here.server: # ... previous local zone settings ... # Ad-blocking entries # For doubleclick.net local-data: "doubleclick.net. IN A 0.0.0.0" local-data: "doubleclick.net. IN AAAA ::1" # Can also use :: local-zone: "doubleclick.net." redirect # For pagead2.googlesyndication.com local-data: "pagead2.googlesyndication.com. IN A 0.0.0.0" local-data: "pagead2.googlesyndication.com. IN AAAA ::1" local-zone: "pagead2.googlesyndication.com." redirect # If these ad domains are DNSSEC signed, you might need to add: # domain-insecure: "doubleclick.net." # domain-insecure: "pagead2.googlesyndication.com." # to ensure your local override isn't rejected due to DNSSEC validation failure. # However, for blocking to 0.0.0.0, this is often not strictly necessary as the goal # is to prevent connection, not necessarily to serve 'valid' local data for it. # The redirect type itself often implies local authority.
-
Save the
unbound.conf
file and exit the editor. -
Check configuration syntax:
Fix any errors. -
Restart Unbound:
Check its status:sudo systemctl status unbound
.
Step 5: Testing Local Name Resolution and Ad-Blocking
Use dig
from a client configured to use your Unbound server (or from the server itself, targeting @127.0.0.1
or @YOUR_UNBOUND_SERVER_IP
).
-
Test local hostnames:
Verify they return the correct internal IP addresses. -
Test reverse lookups:
Verify they return the correct hostnames (e.g.,router.mynet.lan.
). -
Test ad-blocking:
These should returndig @127.0.0.1 doubleclick.net A dig @127.0.0.1 ads.doubleclick.net A # Test a subdomain dig @127.0.0.1 pagead2.googlesyndication.com AAAA
0.0.0.0
for A record queries and::1
(or::
) for AAAA record queries. -
Test a normal internet domain to ensure general resolution still works:
This should resolve correctly to its public IP.
If these tests are successful, you have configured Unbound to serve custom DNS records for your local network and implement basic ad-blocking. For more extensive ad-blocking, you would typically use a pre-generated blocklist file and include it as shown in the theory section.
Advanced Unbound Configurations
With a solid understanding of Unbound's intermediate features, we now venture into advanced configurations. These topics cover more complex scenarios such as providing differentiated DNS responses based on client IP (views), setting up Unbound as a secure DNS-over-TLS (DoT) or DNS-over-HTTPS (DoH) server, extending its functionality with Python scripting, advanced monitoring, high availability setups, and troubleshooting complex issues. These capabilities allow for highly customized and robust DNS resolver deployments.
9. Advanced Access Control and Views
While basic access-control
rules determine if a client can query Unbound, advanced access control using views allows Unbound to provide different DNS responses for the same query based on the client's IP address or other criteria. This is a cornerstone of implementing "split-horizon DNS" (also known as split-view DNS). In a split-horizon setup, internal clients might receive DNS information tailored to the internal network (e.g., private IP addresses for services), while external clients receive standard public DNS information for the same hostnames.
Views in Unbound enable you to define distinct sets of DNS data and behaviors (like local zones, local data, and forwarding rules) and apply them selectively to different groups of clients. This provides a granular level of control over DNS resolution that is essential in many corporate, campus, or complex home network environments.
The Concept of Split-Horizon DNS
Imagine you have a service, say files.example.com
.
- When accessed from within your internal network, you want
files.example.com
to resolve to a private IP address like192.168.1.50
for direct, fast access. - When accessed from outside your network (the internet), you want
files.example.com
to resolve to its public IP address, say203.0.113.80
, which might be a NAT address on your firewall or a load balancer.
This scenario, where the same DNS name yields different results based on the querier's location or identity, is precisely what split-horizon DNS achieves. Unbound's views are the mechanism to implement this.
Defining Views with view:
Blocks
A view is a named container for DNS configuration directives. You define each view within a view:
block.
Syntax for a view:
block:
view:
name: "<view_name>" # A unique name for this view
# View-specific local zones and data
# These apply ONLY to clients mapped to this view.
local-zone: "internal.example.com." static
local-data: "server.internal.example.com. IN A 192.168.1.100"
local-zone: "example.com." transparent # Allow normal resolution for example.com...
local-data: "files.example.com. IN A 192.168.1.50" # ...but override files.example.com
# View-specific forwarding rules
# forward-zone:
# name: "partner.network."
# forward-addr: 10.10.10.1 # Forward queries for partner.network to an internal resolver
# You can also specify view-specific DNS64 or RPZ settings here.
# view-first: yes # If 'yes', this view is tried first for matching clients. Default is 'no'.
# For most split-horizon, you assign clients explicitly.
# Other settings specific to this view can include:
# - view-val-permissive-mode: <yes/no>
# - view-harden-dnssec-stripped: <yes/no>
Key characteristics of view:
blocks:
name:
:
Each view must have a unique name. This name is used to associate clients with the view.- View-Specific Configuration: Directives like
local-zone
,local-data
,local-data-ptr
,forward-zone
,stub-zone
, and Response Policy Zones (RPZ) can be defined inside aview:
block. These settings will only apply to clients using this view. - Isolation (Partial): View-specific
local-zone
andlocal-data
entries create a distinct DNS namespace for clients of that view. - Cache Sharing: Importantly, Unbound typically uses a shared global cache for RRsets and messages across all views. This means if
external.example.com
is resolved by one view, its RRsets are cached globally and might be used by another view if the resolution path is the same and no view-specific data overrides it. However, the interpretation of this cached data and the final answer constructed can differ per view due to view-specificlocal-data
orlocal-zone
types. - Global Settings Inheritance: Settings defined in the main
server:
block (likeverbosity
,num-threads
,root-hints
, globalaccess-control
for default access) generally apply globally unless a view-specific equivalent exists (e.g.,view-val-permissive-mode
).
Assigning Clients to Views with access-control-view:
Once views are defined, you need to map clients to them. This is done using the access-control-view:
directive in the main server:
block.
Syntax: access-control-view: <IP netblock> <view_name>
<IP netblock>
:
An IP address (e.g.,192.168.1.10/32
) or a network CIDR (e.g.,192.168.1.0/24
).<view_name>
:
The name of aview:
block defined elsewhere in the configuration.
Example:
server:
# ... global settings ...
# Assign clients from the internal LAN to the 'internal_lan_view'
access-control-view: 192.168.1.0/24 internal_lan_view
# Assign clients from the VPN subnet to the 'vpn_clients_view'
access-control-view: 10.8.0.0/16 vpn_clients_view
# Default access control for clients not matching any access-control-view
# These clients will not use any specific view and will get Unbound's default behavior.
access-control: 0.0.0.0/0 refuse
access-control: ::0/0 refuse
access-control: 127.0.0.1/32 allow # Localhost can still query, gets default view
Processing Order:
access-control-view:
rules are processed in the order they appear in the configuration file. The first match determines the client's view.- If a client's IP does not match any
access-control-view:
rule, it does not use any named view. It will then be subject to the globalaccess-control:
rules. If allowed by globalaccess-control
, these clients receive Unbound's "default" DNS resolution behavior (i.e., using anylocal-zone
/local-data
defined directly in theserver:
block or performing standard recursion).
Example Scenario Internal vs. External DNS Resolution
Let's implement the files.example.com
split-horizon scenario.
- Assume
example.com
is a real public domain. files.example.com
publicly resolves to203.0.113.80
.- Internal clients (
192.168.1.0/24
) should resolvefiles.example.com
to192.168.1.50
. - All other clients (external) should get the public IP
203.0.113.80
.
Configuration:
# In unbound.conf
server:
# ... (verbosity, interfaces, port, root-hints, auto-trust-anchor-file, etc.) ...
# Define who can use which view
access-control-view: 192.168.1.0/24 internal_view # Internal LAN clients
# access-control-view: 10.8.0.0/16 internal_view # Another internal network (e.g., Wi-Fi)
# Default access for clients not in a view (e.g., external clients if server is public)
# These clients will get the "default view" or global behavior
access-control: 0.0.0.0/0 allow # Allow all other IPv4 to query (adjust if needed)
access-control: ::0/0 allow # Allow all other IPv6 to query (adjust if needed)
# More securely, you might refuse external access if it's not intended to be a public resolver:
# access-control: 0.0.0.0/0 refuse
# access-control: ::0/0 refuse
access-control: 127.0.0.1/32 allow # Allow localhost
# You might have global local-data for the "default/external" view,
# or let Unbound resolve public names normally.
# To ensure the public IP for files.example.com is served to non-internal clients:
# Option 1: Let Unbound resolve it normally if it's in public DNS.
# Option 2: Define it explicitly for the "default" view (if not in a named view).
# This local-data applies if NO access-control-view matches.
local-zone: "example.com." transparent # For the default view
local-data: "files.example.com. IN A 203.0.113.80" # Public IP for default view
# If example.com is DNSSEC-signed, and you are providing local data for it,
# you need to tell Unbound not to expect public DNSSEC validation for the
# parts you override. This applies to all views where it's overridden.
domain-insecure: "files.example.com."
# Define the 'internal_view'
view:
name: "internal_view"
# This view is for clients in 192.168.1.0/24
# For example.com, allow normal resolution but override files.example.com
local-zone: "example.com." transparent
local-data: "files.example.com. IN A 192.168.1.50" # Internal IP
# Other internal-only DNS records
local-zone: "dev.lan." static
local-data: "testserver.dev.lan. IN A 192.168.1.200"
# No 'external_view' block is explicitly needed if the default behavior +
# global local-data provides the "external" view.
# If you wanted more complex external behavior, you'd define an 'external_view'
# and assign clients to it using access-control-view for 0.0.0.0/0 and ::0/0
# (making sure those rules come AFTER more specific internal view assignments).
Explanation:
access-control-view: 192.168.1.0/24 internal_view
:
Clients from this subnet will use theinternal_view
.-
view: name: "internal_view"
:local-zone: "example.com." transparent
: Forexample.com
within this view, Unbound will generally try to resolve names recursively.local-data: "files.example.com. IN A 192.168.1.50"
: However, forfiles.example.com
, it provides the internal IP192.168.1.50
. This overrides any public record for clients in this view.- It also defines a purely internal zone
dev.lan
.
-
Default/External Clients:
- Clients not matching
192.168.1.0/24
(e.g., an external client, or localhost if not explicitly put in a view) will fall through. - They are then checked against global
access-control
rules. If allowed, they get the default Unbound behavior. - The global
local-data: "files.example.com. IN A 203.0.113.80"
(if used) provides the public IP for these non-internal_view
clients. If this globallocal-data
wasn't present, Unbound would recursively resolvefiles.example.com
from public DNS servers.
- Clients not matching
-
domain-insecure: "files.example.com."
:
This is important. Ifexample.com
is DNSSEC-signed, and you are providinglocal-data
forfiles.example.com
that differs from the public, signed record, Unbound would normally treat your local data as "bogus" because it can't be validated byexample.com
's public keys.domain-insecure
tells Unbound to not attempt DNSSEC validation for this specific name, allowing your local override to work smoothly for all views where it's defined.
Considerations and Best Practices for Views
- Clarity and Naming: Use descriptive names for your views (e.g.,
internal_lan
,guest_wifi
,dmz_servers
). - Order of
access-control-view
: Rules are matched top-down. Place more specific network blocks before broader ones if they overlap and need different views. - Default Behavior: Understand what clients get if they don't match any
access-control-view
rule. This "default view" is configured bylocal-zone
/local-data
directly in theserver:
block or just standard recursion. - Testing: Thoroughly test resolution from clients in each defined view and from clients in the "default view" to ensure they receive the correct DNS responses.
- Complexity: Views add complexity to your configuration. Use them when necessary, but avoid over-complicating if simpler methods suffice.
- Cache Implications: While the cache is shared, the path to an answer can differ. If
view A
resolvesx.com
to an internal IP andview B
resolvesx.com
to a public IP, the respectivelocal-data
entries are what differentiate the final answer. Common upstream RRsets fetched (e.g., for TLDs) would be shared. - No Client Tagging Beyond IP: Unbound views are based on client IP addresses. You cannot easily use other client attributes (like MAC address or authenticated user) directly with Unbound views without external systems or scripting.
Views are a sophisticated tool in Unbound, allowing for flexible and powerful DNS policy enforcement tailored to different client groups.
Workshop Implementing Split-Horizon DNS with Internal and External Views
This workshop will guide you through setting up a split-horizon DNS configuration using Unbound views. We will create an "internal" view for local clients and simulate an "external" view (or default behavior) for others.
Scenario:
- A service
portal.mycompany.local
needs to be accessible. - Internal clients (from
192.168.50.0/24
) should resolveportal.mycompany.local
to192.168.50.10
(a private IP). - External clients (everyone else) should resolve
portal.mycompany.local
to203.0.113.55
(a public IP). - Internal clients should also be able to resolve
adminpc.internal.lan
to192.168.50.5
. External clients should get NXDOMAIN for this.
Prerequisites
- Your Unbound server set up and running.
- SSH access to your Unbound server with sudo privileges.
dig
utility on the server and ideally on a (simulated) client in the192.168.50.0/24
network and one outside it.- For simulation, you can use
dig -b <source_ip>
if you can add a source IP to an interface on the machine runningdig
. Otherwise, testing directly from the Unbound server with@127.0.0.1
will reflect the "default" view unless 127.0.0.1 is explicitly put in a view.
- For simulation, you can use
Step 1: Planning the Views and IP Addresses
- Internal View Name:
v_internal
- Internal Client Subnet:
192.168.50.0/24
portal.mycompany.local
(Internal IP):192.168.50.10
portal.mycompany.local
(External IP):203.0.113.55
adminpc.internal.lan
(Internal IP):192.168.50.5
Step 2: Configuring Unbound with Views
-
Edit
unbound.conf
: -
Add
access-control-view
and globallocal-data
(for the "external" view) in theserver:
block:server: # ... (your existing verbosity, interface, port, root-hints, etc.) ... # Allow all queries for this workshop for simplicity, adjust for production access-control: 0.0.0.0/0 allow access-control: ::0/0 allow # Map internal clients to the 'v_internal' view access-control-view: 192.168.50.0/24 v_internal # --- Configuration for the "default" (external) view --- # Clients not in 192.168.50.0/24 will use these settings. # Define mycompany.local as transparent for default view to allow specific override local-zone: "mycompany.local." transparent local-data: "portal.mycompany.local. IN A 203.0.113.55" # External IP # For internal.lan, ensure external clients get NXDOMAIN # This local-zone definition in the server block makes it authoritative for the default view. # Since no local-data for adminpc.internal.lan is here, it will be NXDOMAIN. local-zone: "internal.lan." static # If mycompany.local was a real DNSSEC signed domain, we'd add: # domain-insecure: "portal.mycompany.local." # For this workshop, mycompany.local is fictional, so DNSSEC is not an issue.
-
Define the
v_internal
view block (outside theserver:
block):# ... end of server: block ... # --- Definition for the Internal View --- view: name: "v_internal" # Must match the name in access-control-view # For clients in 192.168.50.0/24 # Override for portal.mycompany.local local-zone: "mycompany.local." transparent local-data: "portal.mycompany.local. IN A 192.168.50.10" # Internal IP # Define internal.lan zone and records local-zone: "internal.lan." static local-data: "adminpc.internal.lan. IN A 192.168.50.5" local-data-ptr: "192.168.50.5 adminpc.internal.lan."
-
Save the
unbound.conf
file and exit. -
Check configuration syntax:
Fix any errors. -
Restart Unbound:
Check status:sudo systemctl status unbound
.
Step 3: Testing the Split-Horizon Configuration
Test Scenario 1: Simulating an Internal Client (from 192.168.50.x
)
If you have a machine with an IP in 192.168.50.0/24
configured to use your Unbound server:
# On the internal client
dig @YOUR_UNBOUND_SERVER_IP portal.mycompany.local A
dig @YOUR_UNBOUND_SERVER_IP adminpc.internal.lan A
dig @YOUR_UNBOUND_SERVER_IP -x 192.168.50.5 # Test PTR
portal.mycompany.local
should resolve to192.168.50.10
.adminpc.internal.lan
should resolve to192.168.50.5
.- The reverse lookup for
192.168.50.5
should returnadminpc.internal.lan.
.
If you need to simulate this from the Unbound server itself (or another machine) using dig -b
:
First, you'd need to add an IP from the 192.168.50.0/24
range to one of your server's network interfaces (e.g., as a secondary IP). Let's say you add 192.168.50.2
:
# Example: sudo ip addr add 192.168.50.2/24 dev eth0
# Then test:
dig @YOUR_UNBOUND_SERVER_IP -b 192.168.50.2 portal.mycompany.local A
dig @YOUR_UNBOUND_SERVER_IP -b 192.168.50.2 adminpc.internal.lan A
# Remember to remove the temporary IP: sudo ip addr del 192.168.50.2/24 dev eth0
Test Scenario 2: Simulating an External Client (or using localhost on the Unbound server)
Queries from any IP not in 192.168.50.0/24
should get the "external" view.
Using localhost
(127.0.0.1
) on the Unbound server itself will test this, as 127.0.0.1
is not covered by our access-control-view
rule for v_internal
.
# On the Unbound server (or an external client)
dig @127.0.0.1 portal.mycompany.local A
dig @127.0.0.1 adminpc.internal.lan A
portal.mycompany.local
should resolve to203.0.113.55
.adminpc.internal.lan
should returnNXDOMAIN
(Non-Existent Domain) because it's defined asstatic
in the global scope with nolocal-data
for this specific record in that scope.
If the results match the expected outcomes for both internal and external scenarios, your split-horizon DNS configuration with Unbound views is working correctly. This workshop demonstrates the power of views to serve different DNS realities to different client sets.