Skip to content
Author Nejat Hakan
eMail 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 for www.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.

  1. 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).
  2. 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.
  3. Second-Level Domains (SLDs):
    These are the names directly to the left of the TLD, like example in example.com. Organizations or individuals typically register these.
  4. Subdomains:
    Further subdivisions of a domain, like www in www.example.com or api.service.example.com. The owner of an SLD can create and manage subdomains within their zone.
  5. 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 for www.example.com, it will eventually query one of the authoritative name servers for example.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
  • 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
  • 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.
  • 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.
  • NS (Name Server) Record:
    Delegates a DNS zone to use the given authoritative name servers.
    • Example: example.com. IN NS ns1.auth-server.com.
  • PTR (Pointer) Record:
    Used for reverse DNS lookups, mapping an IP address back to a hostname. These are primarily found in reverse lookup zones like in-addr.arpa (for IPv4) and ip6.arpa (for IPv6).
    • Example: 34.216.184.93.in-addr.arpa. IN PTR www.example.com.
  • 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)

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):

  1. 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.
  2. 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.
  3. Caching the Result:
    The recursive resolver caches the received answer (respecting its TTL) for future queries.
  4. 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:

  1. It checks its zone data for the requested record.
  2. If the record exists, it returns the record.
  3. If the record does not exist for a domain it is authoritative for, it returns an NXDOMAIN (Non-Existent Domain) response.
  4. 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:

  1. Client Query:
    Your computer (the DNS client) sends a DNS query for www.example.com to its configured recursive DNS resolver (e.g., your Unbound server, your ISP's resolver, or 8.8.8.8).
  2. Recursive Resolver to Root Servers:
    The recursive resolver, not knowing www.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).
  3. 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 where www.example.com is, but it knows who manages .com.
  4. Recursive Resolver to TLD Servers:
    The recursive resolver then queries one of the .com TLD name servers. "Where can I find information about example.com?"
  5. TLD Server Response:
    The .com TLD server responds with a list of IP addresses for the authoritative name servers for the example.com domain. It doesn't know www.example.com, but it knows who manages example.com.
  6. Recursive Resolver to Authoritative Servers:
    The recursive resolver now queries one of the authoritative name servers for example.com. "What is the A record (IP address) for www.example.com?"
  7. Authoritative Server Response:
    The authoritative name server for example.com checks its zone files, finds the A record for www.example.com, and returns the IP address (e.g., 93.184.216.34) to the recursive resolver.
  8. 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.
  9. 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 about example.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:

    sudo apt update
    sudo apt install dnsutils
    

  • On CentOS/RHEL/Fedora:

    sudo yum install bind-utils
    # or
    sudo dnf install bind-utils
    

  • On macOS: dig and nslookup 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 extracting dig.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:

    dig www.google.com A
    

    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 for www.google.com).
    • ANSWER SECTION: Provides the answer. www.google.com. has an A record with the IP 142.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:

    nslookup www.google.com
    

    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):

    dig www.google.com AAAA
    
    Look for an AAAA record in the ANSWER SECTION.

  • MX Record (Mail Exchange):

    dig google.com MX
    

    Expected Output (simplified):

    ;; 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.
    ...
    
    This shows mail servers for 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 dig:

    dig @1.1.1.1 www.cloudflare.com A
    
    The @1.1.1.1 part tells dig to send the query to that server.

  • Using nslookup:

    nslookup www.cloudflare.com 1.1.1.1
    

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.

dig +trace www.wikipedia.org

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
Observe how 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):
    dig www.example.com A +short
    
    Output:
    93.184.216.34
    

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.

dig -x 8.8.8.8

Expected Output (simplified):

;; ANSWER SECTION:
8.8.8.8.in-addr.arpa.   21599   IN      PTR     dns.google.
This shows that 8.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
    sudo apt upgrade -y
    

    • 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
    sudo dnf upgrade -y
    

    • sudo dnf check-update: Checks for available updates.
    • sudo dnf upgrade -y: Upgrades all packages with available updates.
  • On older CentOS/RHEL (using YUM):

    sudo yum check-update
    sudo yum update -y
    

It's also a good idea to reboot your server if the upgrade included a kernel update or other critical system components:

sudo reboot
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.

    sudo apt install unbound -y
    
    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 epel-release). For CentOS/RHEL 8+, AlmaLinux, Rocky Linux (using DNF):

    # If EPEL is not enabled, you might need:
    # sudo dnf install epel-release -y
    sudo dnf install unbound -y
    
    For older CentOS/RHEL 7 (using YUM):
    # If EPEL is not enabled, you might need:
    # sudo yum install epel-release -y
    sudo yum install unbound -y
    

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):

  1. 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" then sudo yum install openssl-devel expat-devel
  2. Download the source code: Get the latest tarball from the NLnet Labs Unbound website.
    wget https://nlnetlabs.nl/downloads/unbound/unbound-latest.tar.gz
    tar -xzvf unbound-latest.tar.gz
    cd unbound-<version>
    
  3. 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:
      make
      
    • Install:
      sudo make 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:

    sudo systemctl status unbound
    

    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 (Active: inactive (dead) or Active: failed), you can try to start it:

    sudo systemctl start unbound
    
    And enable it to start on boot:
    sudo systemctl enable unbound
    

  • Using older init systems (e.g., SysVinit on very old systems):

    sudo service unbound status
    sudo service unbound start
    sudo chkconfig unbound on  # For RHEL/CentOS 6
    sudo update-rc.d unbound defaults # For older Debian/Ubuntu
    

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)

  1. 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).
  2. 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.
  3. 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.
  4. 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).
  5. Get IP Address: Once installed and rebooted, log in to the VM's console. Find its IP address:
    ip addr show
    
    Look for the IP address associated with an interface like eth0, ens33, or enp0s3.

Option B: Cloud Virtual Machine (using a generic cloud provider example)

  1. Sign up/Log in: Access your cloud provider's console.
  2. 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.
  3. 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:

    ssh your_username@server_ip_address
    
    (e.g., ssh myuser@192.168.1.105 or ssh myuser@203.0.113.50) If you used SSH keys with a cloud VM, it might be ssh -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:

    sudo apt update
    sudo apt upgrade -y
    # Optional: sudo reboot if a kernel update occurred
    
    If you rebooted, reconnect via SSH.

  • For CentOS/RHEL/AlmaLinux/Rocky Linux:

    sudo dnf check-update
    sudo dnf upgrade -y
    # Optional: sudo reboot if a kernel update occurred
    
    If you rebooted, reconnect via SSH.

Step 4: Installing Unbound

Now, install Unbound using the package manager.

  • For Debian/Ubuntu:

    sudo apt install unbound -y
    

  • For CentOS/RHEL/AlmaLinux/Rocky Linux: If epel-release is needed and not installed (common on minimal RHEL-derivatives):

    sudo dnf install epel-release -y # Or yum for older systems
    
    Then install Unbound:
    sudo dnf install unbound -y # Or yum for older systems
    

Step 5: Verifying the Installation and Service Status

Check if Unbound installed correctly and if the service is running and enabled.

sudo systemctl status unbound
You should see Active: 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:

unbound -V
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:

sudo cp /etc/unbound/unbound.conf /etc/unbound/unbound.conf.default

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:

sudo unbound-checkconf /etc/unbound/unbound.conf
If the output is unbound-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"
To set up 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
You can have multiple 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 multiple interface: 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 the port: directive.
  • port: <port number> The UDP and TCP port number Unbound listens on for DNS queries. Default is 53.

    • 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 usually yes.

    • 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 be allow, 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 will chroot 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 the directory: (see below) is relative to the chroot path, or an absolute path within the chroot.

    • chroot: "/etc/unbound" (Common, but ensure write permissions for unbound user on necessary files/subdirs if directory 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 an unbound user and set this automatically.

    • username: "unbound"
  • directory: "<path>" The working directory for Unbound. Relative paths for other files (like pidfile, 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>" and use-syslog: <yes/no> Configures logging.

    • logfile: "/var/log/unbound.log" (Log to a specific file. Ensure the unbound 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). If yes, 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 with 1 or 2.

    • 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 named root.hints or named.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) or root-hints: "/etc/unbound/root.hints" It's good practice to periodically update this file. Unbound can do this automatically if auto-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:
      wget -O root.hints https://www.internic.net/domain/named.root
      sudo mv root.hints /etc/unbound/ # Or your configured path
      
  • harden-glue: <yes/no> Default yes. 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> Default yes. 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> Default no. If yes, Unbound will attempt DNSSEC validation but will not fail a query if validation fails. This is primarily for debugging and should generally be no 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. Default 0.

    • 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. Default 86400 (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 with nproc or lscpu.

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:

sudo cp /etc/unbound/unbound.conf /etc/unbound/unbound.conf.orig

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:

sudo nano /etc/unbound/unbound.conf

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 to root.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:
      wget https://www.internic.net/domain/named.root -O root.hints.tmp
      sudo mv root.hints.tmp /etc/unbound/root.hints # Or another suitable location
      # Then update the path in unbound.conf
      
  • auto-trust-anchor-file::
    This file stores the DNSSEC root trust anchor. Unbound needs to be able to update it. The unbound 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 with sudo 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 out interface: ::0 and set do-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 and chroot:
      These are often set by the systemd service file provided by the package. If you set them in unbound.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. If chroot is active, paths like logfile, 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 on nproc output. For a simple server, 1 or 2 is fine.
    • Cache Sizes:
      msg-cache-size and rrset-cache-size can be increased for better performance on busier resolvers or systems with more RAM. 4m and 8m are modest starting points. (e.g., 16m and 32m).

unbound-control allows you to manage Unbound without restarting it (e.g., reload configs, view stats).

  1. Generate the control keys:

    sudo unbound-control-setup -d /etc/unbound
    
    This will create unbound_server.key, unbound_server.pem, unbound_control.key, and unbound_control.pem in /etc/unbound/. It will also print the remote-control: block you need to add to unbound.conf.

  2. Edit /etc/unbound/unbound.conf again (sudo nano /etc/unbound/unbound.conf).

  3. Uncomment the remote-control: section at the end of your file and paste the output from unbound-control-setup. It should look similar to this:
    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"
    
    Make sure the paths to the key/cert files are correct.

Step 4: Testing the Configuration and Restarting Unbound

  1. Check the configuration file for syntax errors:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    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.

  2. Restart Unbound to apply the new configuration:

    sudo systemctl restart unbound
    

  3. Check the status of Unbound:

    sudo systemctl status unbound
    
    Look for Active: active (running). If it failed, check the logs for more details:
    sudo journalctl -u unbound -e
    
    (This shows the latest log entries for the unbound unit). Common failure reasons include incorrect file paths (especially for root-hints or auto-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.

  1. Find your server's IP address (if you don't know it):

    ip addr show
    
    Let's assume your server's IP is 192.168.1.100.

  2. 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):

    ;; ->>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
    
    Key things to look for:

    • 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.:

    sudo systemctl status unbound
    
    Look for Active: active (running).

  • Start Unbound: If the service is stopped and you want to start it:

    sudo systemctl start unbound
    

  • Stop Unbound: To stop the service:

    sudo systemctl stop unbound
    
    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):

    sudo systemctl restart unbound
    

  • Reload Unbound Configuration: To apply most configuration changes without dropping active connections or clearing the cache (if possible):

    sudo systemctl reload unbound
    
    Alternatively, if you have unbound-control set up, you can use:
    sudo unbound-control reload
    
    This is generally the preferred way to apply configuration changes as it's less disruptive. Some changes (like chroot or fundamental interface changes) might still require a full restart.

  • Enable Unbound on Boot: To ensure Unbound starts automatically when the server boots:

    sudo systemctl enable unbound
    

  • Disable Unbound on Boot: To prevent Unbound from starting automatically on boot:

    sudo systemctl disable unbound
    

On systems not using systemd (e.g., older systems with SysVinit, or FreeBSD with rc.d), the commands would be different:

  • Example for SysVinit:

    sudo service unbound status
    sudo service unbound start
    sudo service unbound stop
    sudo service unbound restart
    sudo service unbound reload
    

  • Example for FreeBSD:

    sudo service unbound status
    sudo service unbound start
    sudo service unbound stop
    # Reload often handled by sending SIGHUP or using unbound-control
    

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:

    sudo unbound-control status
    

    Expected Output:

    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
    
    This output gives you:

    • 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:

  1. Basic Resolution (A and AAAA records): Query for common domains. SERVER_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).

    dig @SERVER_IP www.google.com A
    dig @SERVER_IP www.cloudflare.com AAAA
    
    Check for status: NOERROR and valid IP addresses in the ANSWER SECTION.

  2. Resolution of Different Record Types (MX, TXT, NS):

    dig @SERVER_IP google.com MX
    dig @SERVER_IP example.com TXT
    dig @SERVER_IP nlnetlabs.nl NS
    
    Verify that you receive the correct record types and data.

  3. Cache Behavior: Query the same domain twice in quick succession.

    dig @SERVER_IP www.wikipedia.org A
    # Note the "Query time"
    dig @SERVER_IP www.wikipedia.org A
    # "Query time" for the second query should be significantly lower (e.g., 0 or 1 msec)
    # Also, the TTL in the answer might be lower for the cached response.
    
    This indicates the record was served from Unbound's cache.

  4. 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:

      dig @SERVER_IP sigok.verteiltesysteme.net A +dnssec
      
      Look for flags: ... ad; (Authentic Data) in the response header. This means Unbound successfully validated the DNSSEC signatures. The ANSWER SECTION should also contain RRSIG records.

    • Test an intentionally broken DNSSEC-signed domain:

      dig @SERVER_IP sigfail.verteiltesysteme.net A +dnssec
      
      This query should result in status: 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:

      dig @SERVER_IP example.com A +dnssec
      
      This should resolve successfully (status: NOERROR) but will not have the ad flag, because example.com (as of writing) is not DNSSEC signed. Unbound will simply return the data as insecure.

  5. Querying from Allowed vs. Denied Clients:
    If you've configured access-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 or refuse 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 allowed 0.0.0.0/0.
  6. Non-Existent Domains (NXDOMAIN):

    dig @SERVER_IP doesnotexist.example.com A
    
    This should result in status: 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 by systemd-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.

    sudo tail -f /var/log/unbound.log  # Or your configured path
    

Interpreting Log Entries: The content of the logs depends heavily on the verbosity level in unbound.conf:

  • verbosity: 0:
    Only errors are logged.
  • 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):

    Jun 20 12:00:00 unbound-server unbound[1234]: [1234:0] info: start of service (unbound 1.13.1).
    
    Example entry (DNSSEC trust anchor update):
    Jun 20 12:01:00 unbound-server unbound[1234]: [1234:0] info: successfully read root-hints. new serial 2023062000
    Jun 20 12:01:00 unbound-server unbound[1234]: [1234:0] info: validator: primemin_success root . DNSKEY
    

  • 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):

    Jun 20 12:05:00 unbound-server unbound[1234]: [1234:0] info: query response was CNAME NXDOMAIN NONEXISTENT example.com.ix
    Jun 20 12:05:01 unbound-server unbound[1234]: [1234:1] Pinfo: 192.168.1.10 queries for www.google.com A IN
    

  • 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 (or nslookup) 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:

  1. Open /etc/resolv.conf with sudo:
    sudo nano /etc/resolv.conf
    
  2. Comment out any existing nameserver lines by adding a # at the beginning.
  3. Add a new line for your Unbound server:
    nameserver 192.168.1.100
    
  4. 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)

  1. Open "Control Panel" -> "Network and Internet" -> "Network and Sharing Center".
  2. Click on "Change adapter settings" on the left.
  3. Right-click on your active network connection (e.g., "Ethernet" or "Wi-Fi") and select "Properties".
  4. Select "Internet Protocol Version 4 (TCP/IPv4)" and click "Properties".
  5. Select "Use the following DNS server addresses:".
  6. In "Preferred DNS server", enter 192.168.1.100.
  7. Click "OK" on both dialogs.
  8. You might need to clear your local DNS cache: open Command Prompt as Administrator and run ipconfig /flushdns.

Option C: macOS Client (Network Settings)

  1. Open "System Settings" (or "System Preferences" on older macOS).
  2. Go to "Network".
  3. Select your active network connection (e.g., "Wi-Fi" or "Ethernet") from the list.
  4. Click the "Details..." button (or "Advanced..." on older macOS).
  5. Go to the "DNS" tab.
  6. Click the + button under "DNS Servers" and add 192.168.1.100.
  7. 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.
  8. Click "OK" and then "Apply".

Step 2: Performing Basic Queries from the Client

On your configured client machine, open a terminal or command prompt.

  1. Test A record resolution:

    dig www.google.com A
    # On Windows: nslookup www.google.com
    
    The output should show SERVER: 192.168.1.100#53 (or your Unbound server's IP) and a successful resolution.

  2. Test AAAA record resolution:

    dig www.google.com AAAA
    # On Windows: nslookup -type=AAAA www.google.com
    

  3. Test MX record resolution:

    dig nlnetlabs.nl MX
    # On Windows: nslookup -type=MX nlnetlabs.nl
    

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 in unbound.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

  1. Query a domain for the first time:

    dig www.wikipedia.org A
    
    Note the "Query time" in the output.

  2. Immediately query the same domain again:

    dig www.wikipedia.org A
    
    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

  1. Test a known good DNSSEC-signed domain:

    dig sigok.verteiltesysteme.net A +dnssec
    
    Look for the ad (Authentic Data) flag in the flags: line of the header. Example: flags: qr rd ra ad;. If the ad flag is present, DNSSEC validation was successful.

  2. Test a known bad (intentionally misconfigured) DNSSEC-signed domain:

    dig sigfail.verteiltesysteme.net A +dnssec
    
    This command should result in status: 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 and status: NOERROR, DNSSEC validation is likely not working correctly on your Unbound server. Check the auto-trust-anchor-file configuration and Unbound's logs.

  3. Check Unbound server logs for DNSSEC messages: On the Unbound server, if verbosity is 1 or higher, you should see messages related to DNSSEC validation, especially if issues occur or when the trust anchor is updated.

    sudo journalctl -u unbound -e
    
    Look for lines containing "validator" or "DNSKEY" or "secure" or "bogus".

Step 5: Using unbound-control status on the Server

Log back into your Unbound server via SSH.

  1. Get status and stats:

    sudo unbound-control status
    
    Observe the queries: total, recursive counts. As you perform queries from your client, these numbers should increase. Look at the uptime and cache sizes.

  2. Dump detailed stats (optional):

    sudo unbound-control stats_noreset
    
    This provides many counters, including num.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.

  • username: "<user>" Directive: The username directive in the server: block of unbound.conf specifies the user account Unbound will switch to.

    server:
        username: "unbound"
    
    Most package installations of Unbound automatically:

    1. Create a dedicated system user (e.g., unbound or _unbound) with no login shell and a locked password.
    2. 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:

    ps aux | grep unbound
    
    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 unprivileged unbound 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 to root (which is highly discouraged), any vulnerability exploited in Unbound could grant an attacker root access to your server.

  • 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).
    • Package installations that enable chroot usually handle this setup. If you enable it manually, it requires careful planning. For example, if directory: "/etc/unbound" and chroot: "/etc/unbound", then pidfile: "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 to auto-trust-anchor-file and the pidfile 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.

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 the RCODE (Response Code) set to REFUSED. This informs the client that the query was intentionally rejected. This is generally preferred over deny 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 with cache-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:

  1. 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.
  2. Allow other necessary traffic (e.g., SSH on TCP/22 from your admin IP).
  3. Block all other incoming traffic by default (default deny policy).

Using ufw (Uncomplicated Firewall - common on Debian/Ubuntu):

  1. Install ufw (if not already installed):
    sudo apt update
    sudo apt install ufw
    
  2. Set default policies:

    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    
    This blocks all incoming connections by default and allows all outgoing connections (Unbound needs to make outgoing queries).

  3. Allow SSH (crucial if you're connected via SSH!): Replace your_admin_ip_range with your actual IP or network, or omit from ... to allow from anywhere (less secure for SSH).

    sudo ufw allow from your_admin_ip_range to any port 22 proto tcp
    # Or simply (allows SSH from anywhere):
    # sudo ufw allow ssh
    

  4. Allow DNS traffic from trusted sources: Let's say your local network is 192.168.1.0/24 and localhost.

    # Allow from localhost (Unbound itself or other local services)
    sudo ufw allow from 127.0.0.1 to any port 53
    sudo ufw allow from ::1 to any port 53
    
    # Allow from your local 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 multiple trusted networks or specific IPs, add rules for each.

  5. Enable ufw:

    sudo ufw enable
    
    It will warn you that this may disrupt existing SSH connections. If you've correctly allowed SSH, it should be fine. Type y to proceed.

  6. Check status:

    sudo ufw status verbose
    
    This will list all active rules.

Using firewalld (common on CentOS/RHEL/Fedora):

  1. Ensure firewalld is running and enabled:
    sudo systemctl status firewalld
    sudo systemctl start firewalld
    sudo systemctl enable firewalld
    
  2. 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.

    # 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
    
    The service name="dns" predefinition covers both TCP and UDP port 53.

  3. List active rules/services:

    sudo firewall-cmd --list-all
    sudo firewall-cmd --list-rich-rules
    

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)

  1. Check the username in unbound.conf:

    grep "username:" /etc/unbound/unbound.conf
    
    Ensure it's set to a non-root user like unbound. If it's commented out, the service default (usually unbound) is likely in effect.

  2. Check the running process (optional, for deeper understanding):

    ps aux | grep unbound
    
    Look for processes owned by the unbound user.

  3. Check for chroot in unbound.conf:

    grep "chroot:" /etc/unbound/unbound.conf
    
    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:
    sudo systemctl cat unbound
    
    Look for User=, Group=, and RootDirectory= (systemd's equivalent of chroot for services) or ExecStart= 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.

  1. Edit unbound.conf:

    sudo nano /etc/unbound/unbound.conf
    

  2. Locate the access-control directives. A secure configuration should look something like this (adjust 192.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.
  3. Save the file and exit the editor.

  4. Check the configuration syntax:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    Fix any reported errors.

  5. Reload Unbound to apply the changes:

    sudo systemctl reload unbound
    # Or sudo unbound-control reload
    

Step 3: Configuring the Host Firewall

Choose the subsection relevant to your server's OS.

Option A: Using ufw (for Debian/Ubuntu)

  1. Install ufw if not present:

    sudo apt update
    sudo apt install ufw -y
    

  2. Set default policies:

    sudo ufw default deny incoming
    sudo ufw default allow outgoing
    

  3. 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 is 203.0.113.42:

    sudo 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
    
    If you just ran sudo ufw allow ssh, that's okay for now, but consider restricting it.

  4. Allow DNS traffic only from trusted sources (matching your access-control rules): Assuming your local network is 192.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
    

  5. Enable ufw:

    sudo ufw enable
    
    Confirm with y. Your SSH session should remain active if you allowed SSH correctly.

  6. Check status:

    sudo ufw status verbose
    
    Verify that your rules for SSH and DNS (port 53) from trusted sources are listed with ALLOW, and the default incoming policy is DENY.

Option B: Using firewalld (for CentOS/RHEL/AlmaLinux/Rocky Linux)

  1. Ensure firewalld is active:

    sudo systemctl start firewalld
    sudo systemctl enable firewalld
    

  2. 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'
    

  3. Reload firewalld to apply changes:

    sudo firewall-cmd --reload
    

  4. Check configuration:

    sudo firewall-cmd --list-all  # Shows services and ports for the default zone
    sudo firewall-cmd --list-rich-rules # Shows your added rich rules
    
    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

  1. Test from an allowed client: From a machine on your local network (e.g., 192.168.1.x) that you configured client-side to use your Unbound server, perform a DNS query:

    dig @YOUR_UNBOUND_SERVER_IP www.example.com A
    
    This should succeed.

  2. 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 or REFUSED).
    • 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 in unbound.conf, restart Unbound, then try to use dig's -b option to source the query from that IP (requires the IP to be configured on an interface locally, can be tricky). Example: In unbound.conf: access-control: 192.168.1.200/32 refuse (assuming .200 is unused) Reload Unbound. If you can assign 192.168.1.200 as a secondary IP to your test client, then try dig @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, and dig will time out.

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:

  1. To Root Server: "What are the NS records for www.sub.example.com?" (Root doesn't need to know "www.sub", only ".com")
  2. To .com TLD Server: "What are the NS records for www.sub.example.com?" (TLD server doesn't need "www", only "sub.example.com")
  3. To example.com Authoritative Server: "What are the NS records for www.sub.example.com?" (This server might only manage sub.example.com and delegate www.sub.example.com)
  4. To sub.example.com Authoritative Server: "What is the A record for www.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:

  1. To Root Server: "What are the NS records for .com?"
  2. To .com TLD Server: "What are the NS records for example.com?"
  3. To example.com Authoritative Server: "What are the NS records for sub.example.com?" (If sub.example.com is a separate zone delegated from example.com)
  4. To sub.example.com Authoritative Server: "What is the A record for www.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 to charlie.example.com if bravo.example.com doesn't exist, and list the record types that exist for alpha.example.com. This proves that bravo.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 of charlie.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:

server:
    # ... other settings ...
    aggressive-nsec: yes
This feature relies on successful DNSSEC validation, so ensure DNSSEC is working correctly. It's generally safe and beneficial to enable.

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.) and forward-tls-upstream: yes is set, Unbound should still validate DNSSEC responses from the forwarder unless specifically configured otherwise for the forward zone. Check the forward-secure: vs forward-first: options (default is forward-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 the server: block if needed. The #SERVER_NAME in forward-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 or tshark (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.

  1. Edit unbound.conf:

    sudo nano /etc/unbound/unbound.conf
    

  2. 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 to yes.

    server:
        # ... other settings ...
    
        qname-minimisation: yes
        # qname-minimisation-strict: no # Usually keep at 'no' for compatibility
    
        aggressive-nsec: yes
    
        # ... other settings ...
    

  3. Save the file and exit the editor.

  4. Check the configuration syntax:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    Fix any errors.

  5. Reload Unbound:

    sudo systemctl reload unbound
    # Or sudo unbound-control reload
    

  6. Verification (Conceptual):

    • QNAME Minimisation: Verifying QNAME minimisation directly requires observing the outgoing DNS queries from Unbound using tools like tcpdump or tshark. You would look for queries to root/TLD servers that only contain the relevant part of the domain name. For example, for www.sub.example.com, you'd see Unbound ask a root server for .com NS records, not for www.sub.example.com NS records.
      # On the Unbound server, if you have tcpdump:
      # sudo tcpdump -i any -n -vvv port 53 and host <IP of a root server>
      # Then, from a client, query a new, multi-label domain through Unbound.
      
      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.

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.

  1. 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.

  2. Edit unbound.conf:

    sudo nano /etc/unbound/unbound.conf
    

  3. Add a forward-zone: block. If you already have qname-minimisation and aggressive-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 the server: 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's do-ip6: no is set in the server: block to prevent attempts to reach IPv6 forwarders.
  4. Save the file and exit the editor.

  5. Check the configuration syntax:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    Fix any errors.

  6. Restart Unbound (Recommended for forwarding changes): While reload might work, a restart is often safer for significant changes like adding global forwarding.

    sudo systemctl restart unbound
    
    Check its status: sudo systemctl status unbound.

  7. Verification:

    • Basic Query Test: From a client configured to use your Unbound server (or from the server itself using dig @127.0.0.1 ...), perform some DNS lookups:

      dig www.example.com A
      dig internic.net A
      
      These should still resolve successfully. The source of the answer will now be your Unbound server, which got its answer from Cloudflare over DoT.

    • (Advanced) Observing Encrypted Traffic with tcpdump or tshark: This is the definitive way to confirm DoT is working. On your Unbound server, listen for traffic on port 853 (DoT's standard port).

      # Ensure no other application is using port 853 for other purposes.
      sudo tcpdump -i any -n -X port 853
      # The -X shows packet content in hex and ASCII. You should see TLS handshake
      # messages and then encrypted application data, not plain DNS packets.
      
      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).

    • Check Unbound logs: Increase verbosity temporarily if needed (verbosity: 2 or 3 in unbound.conf, then sudo systemctl restart unbound).

      sudo journalctl -u unbound -f
      
      Look for messages indicating connections to the forwarders. You might see lines about TLS handshakes or forwarding actions. For example, with verbosity: 2, you might see:
      ... 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
      
      (Log messages can vary by Unbound version and specific query flow).

To revert to full recursive mode (disable DoT forwarding):

  1. Edit /etc/unbound/unbound.conf.
  2. Comment out or delete the entire forward-zone: block you added.
  3. Save the file.
  4. Check syntax: sudo unbound-checkconf /etc/unbound/unbound.conf.
  5. 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.
  • 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 of rrset-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 than msg-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):
    server:
        rrset-cache-size: 512m
        msg-cache-size: 256m
    
    For a Raspberry Pi or small VM with 512MB-1GB total RAM, more modest values like rrset-cache-size: 64m and msg-cache-size: 32m (or even 32m/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, or 3600 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, often yes 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
  • prefetch-key: <yes/no>

    • Default: yes (if prefetch: 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

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
  • 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 or 60 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
  • 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'.
  • 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.

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 or lscpu.
    • 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.

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 from outgoing-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. Default no.
  • minimal-responses: no:
    Default no. If yes, 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 as no.

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

  1. Determine CPU Cores: On your Unbound server, run:

    nproc
    # Or
    lscpu | grep "^CPU(s):"
    
    Note the number of cores. Let's say it's 2 for this workshop.

  2. Estimate Available RAM for Unbound: Check your server's total and free memory:

    free -h
    
    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.

  3. Get Baseline Cache Statistics (Optional but good): If Unbound has been running for a while and handling queries, get current stats:

    sudo unbound-control stats_noreset
    
    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

  1. Edit unbound.conf:

    sudo nano /etc/unbound/unbound.conf
    

  2. Modify rrset-cache-size and msg-cache-size in the server: 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 try rrset-cache-size: 40m and msg-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:

    server:
        # ...
        # For a small setup (e.g., Raspberry Pi, small VM with ~1GB RAM)
        rrset-cache-size: 40m
        msg-cache-size: 20m
    
        # For a larger setup (e.g., multi-core server with 4GB+ RAM for Unbound)
        # rrset-cache-size: 340m
        # msg-cache-size: 170m
        # ...
    
    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.

  1. Still in unbound.conf, add/ensure these directives in the server: block:
    server:
        # ... other settings ...
        prefetch: yes
        prefetch-key: yes # Usually good to keep with prefetch:yes
    
        serve-expired: yes
        serve-expired-ttl: 30   # Serve expired data with a 30s TTL for clients
        # serve-expired-client-timeout: 200 # Optional: serve expired if live query takes >200ms
        # ...
    

Step 4: Experimenting with num-threads

  1. Still in unbound.conf, set num-threads according to your CPU core count in the server: block: Using our example of a 2-core CPU:

    server:
        # ... other settings ...
        num-threads: 2
        # ...
    
    If you had 4 cores, you'd use num-threads: 4.

  2. Save the unbound.conf file and exit the editor.

  3. Check configuration syntax:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    Fix any errors.

  4. Restart Unbound to apply these changes: Cache size and thread changes typically require a restart.

    sudo systemctl restart unbound
    
    Verify it started correctly: sudo systemctl status unbound.

Step 5: Monitoring Performance Changes

  1. 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.

  2. Check statistics again:

    sudo unbound-control stats_noreset
    

    • 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 and mem.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 see num.expired or similar counters increment.
  3. Observe Memory Usage: Use top or htop (and filter for unbound) or ps aux | grep unbound to see how much memory Unbound is actually consuming. This will be more than just rrset-cache-size + msg-cache-size due to overhead, thread stacks, buffers, etc. Ensure it's within acceptable limits for your server.

  4. 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 with num-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 corresponding www.example.com. RRSIG A ... (signature data) ...
  • 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 for example.com if example.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, and alpha.example.com and charlie.example.com exist, an NSEC record for alpha.example.com might state that the next name is charlie.example.com, proving nonexistent (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 (.).

  1. Root Trust Anchor: Your resolver (Unbound) is configured with the root zone's public KSK. This is the ultimate point of trust.
  2. 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.
  3. 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.
  4. Unbound then queries a .com TLD server for example.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.
  5. Domain Validation: The .com TLD server provides DS records for example.com (if example.com is signed). These are signed by .com's ZSK. Unbound verifies this. The DS record for example.com contains a hash of example.com's KSK.
  6. Unbound then queries example.com's authoritative server for www.example.com's A record and its RRSIG. It also fetches example.com's DNSKEY RRset and verifies it using the DS record obtained from the .com TLD. This authenticates example.com's ZSK.
  7. Finally, Unbound uses example.com's authenticated ZSK to verify the RRSIG for the www.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):

  1. 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 using unbound-anchor.

    # 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.
    
    Packaged Unbound installations often run unbound-anchor during installation or as part of the service startup to ensure root.key is populated.

  2. 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 (trust-anchor:): You can manually specify other trust anchors for specific domains using the trust-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.

    trust-anchor: "my.private.zone. KSK_FLAGS KSK_PROTOCOL KSK_ALGORITHM KSK_PUBLIC_KEY_BASE64"
    
    These are static and not automatically updated via RFC 5011 unless also managed by unbound-anchor with specific configurations.

  • Negative Trust Anchors (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":

    server:
        domain-insecure: "broken-dnssec-domain.com"
    
    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.

Troubleshooting DNSSEC Validation Issues

If DNSSEC validation fails for a domain that should be secure, Unbound will typically return SERVFAIL.

  1. Check Unbound Logs: Increase verbosity (e.g., to 2) and val-log-level (e.g., to 1 or 2 - very verbose!) in unbound.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.

    sudo journalctl -u unbound -f -n 100 # Watch recent logs
    

  2. Verify Root Trust Anchor: Ensure auto-trust-anchor-file exists and is up-to-date. Try running sudo 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).

  3. 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).

    date # Check current time
    sudo timedatectl status # Check NTP synchronization status
    # sudo apt install ntp or sudo apt install chrony (if not already)
    

  4. 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 to dig but with a focus on DNSSEC validation debugging. delv @your_resolver_ip example.com A +rtrace can show the validation path.
  5. 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).

  6. Temporarily Use val-permissive-mode: yes (for diagnosis only): If you set val-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

  1. Check unbound.conf for auto-trust-anchor-file:

    grep "auto-trust-anchor-file:" /etc/unbound/unbound.conf
    
    Make sure it points to a valid path (e.g., /var/lib/unbound/root.key or /etc/unbound/root.key).

  2. Prime/Update the Root Anchor Manually (if unsure or first time): Run unbound-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.

    sudo unbound-anchor -a /var/lib/unbound/root.key -v
    
    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. The unbound user needs read/write access to this file for Unbound to manage it automatically.
    # Check permissions (owner should be 'unbound' or similar)
    ls -l /var/lib/unbound/root.key
    
    If permissions are wrong, chown unbound:unbound /var/lib/unbound/root.key (adjust user/group if different on your system).

  3. Restart Unbound to ensure it loads the anchor:

    sudo systemctl restart unbound
    sudo systemctl status unbound
    
    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.

  1. Test a DNSSEC-signed domain known to be valid: sigok.verteiltesysteme.net is designed for this.

    dig @127.0.0.1 sigok.verteiltesysteme.net A
    
    In the dig output, look for the flags: line in the header section. You should see the ad (Authentic Data) flag present if DNSSEC validation was successful. Example: ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 The ad flag is the key indicator from Unbound that it considers the answer cryptographically verified.

  2. Test a DNSSEC-signed domain known to have an invalid signature: sigfail.verteiltesysteme.net is designed for this.

    dig @127.0.0.1 sigfail.verteiltesysteme.net A
    
    This query should result in 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 and status: NOERROR, DNSSEC validation is NOT working correctly. Revisit Step 1 and check logs.

  3. Test a domain that is NOT DNSSEC-signed: example.com is (as of this writing) not DNSSEC signed.

    dig @127.0.0.1 example.com A
    
    This query should result in status: NOERROR and return an IP address. However, the ad flag should NOT be present in the flags: 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.

  1. Query a valid signed domain with +dnssec:

    dig @127.0.0.1 sigok.verteiltesysteme.net A +dnssec
    
    In the ANSWER SECTION, you will now see not only the A record but also an RRSIG A record, which is the signature for that A record. The ad flag should still be present in the header.

  2. Observe the TTL of DNSSEC records: RRSIGs and DNSKEYs also have TTLs. Unbound caches these as well.

Let Unbound run for a bit and serve some queries for DNSSEC-signed domains.

  1. View statistics:
    sudo unbound-control stats_noreset
    
  2. 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 queried sigfail.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, and num.answer.bogus after querying sigfail 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 from local-data for this zone. If no local-data matches, it returns NXDOMAIN (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 of example.com normally but override internal.example.com.
  • redirect:
    All queries for this zone are answered with an A or AAAA record specified in a local-data entry that is the zone itself. E.g., local-zone: "ads.example.com." redirect and local-data: "ads.example.com. A 127.0.0.1". All subdomains of ads.example.com will also resolve to 127.0.0.1.
  • nodefault:
    If a query for a name in this zone does not have a matching local-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:
    Like transparent, 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:
    Like inform but also denies the query.
  • always_transparent:
    Like transparent, but even if the name is a CNAME alias that points outside the zone, the original name is still treated as transparent.
  • always_refuse:
    Like refuse, but CNAMEs are not followed before refusing.
  • always_nxdomain:
    Like static 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."
With this configuration:

  • Querying router.home.arpa will return 192.168.1.1.
  • Querying nas.home.arpa for A will return 192.168.1.10, for AAAA will return 2001:db8:cafe::10.
  • Querying fileserver.home.arpa will resolve to the IP(s) of nas.home.arpa.
  • Querying nonexistent.home.arpa will return NXDOMAIN because the zone type is static.

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
Now, queries for 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.

server:
    # ...
    local-zone: "maliciousdomain.com." refuse # Or deny

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:

# In unbound.conf server: block
server:
    # ...
    include: "/etc/unbound/ad_blocklist.conf"
The 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"
...
Many popular ad-blocking list formats (like Pi-hole's lists) can be converted to this Unbound format.

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 always IN (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"
Without 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."
Now, a reverse lookup for 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
A query for 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:

  1. Defining a local domain (e.g., mynet.lan) for your home network devices.
  2. Adding A, AAAA, and CNAME records for these devices.
  3. Setting up corresponding PTR records for reverse lookups.
  4. 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

  1. Choose a local domain name. For this workshop, we'll use mynet.lan.
  2. Edit unbound.conf:
    sudo nano /etc/unbound/unbound.conf
    
  3. Add a local-zone directive for mynet.lan inside the server: block: We'll use static type, as we want Unbound to be authoritative and only serve records we explicitly define.
    server:
        # ... other existing settings ...
    
        # Local network zone definition
        local-zone: "mynet.lan." static
    

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 (inside server: block), add local-data records for these devices within your mynet.lan zone:

    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"
    
    Make sure FQDNs end with a dot.

Step 3: Adding local-data-ptr for Reverse DNS

To allow reverse lookups (IP to hostname) for these local devices:

  1. Still in unbound.conf (inside server: block), add local-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).

  1. Still in unbound.conf (inside server: block), add local-zone and local-data for the ad domains: Using the redirect type for local-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.
    

  2. Save the unbound.conf file and exit the editor.

  3. Check configuration syntax:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    Fix any errors.

  4. Restart Unbound:

    sudo systemctl 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).

  1. Test local hostnames:

    dig @127.0.0.1 router.mynet.lan A
    dig @127.0.0.1 router.mynet.lan AAAA
    dig @127.0.0.1 nas.mynet.lan A
    dig @127.0.0.1 storage.mynet.lan A # Should show CNAME to nas.mynet.lan and then nas's IP
    dig @127.0.0.1 printer.mynet.lan A
    
    Verify they return the correct internal IP addresses.

  2. Test reverse lookups:

    dig @127.0.0.1 -x 192.168.1.1
    dig @127.0.0.1 -x 192.168.1.100
    dig @127.0.0.1 -x fd00:cafe::100
    
    Verify they return the correct hostnames (e.g., router.mynet.lan.).

  3. Test ad-blocking:

    dig @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
    
    These should return 0.0.0.0 for A record queries and ::1 (or ::) for AAAA record queries.

  4. Test a normal internet domain to ensure general resolution still works:

    dig @127.0.0.1 www.nlnetlabs.nl A
    
    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 like 192.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, say 203.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 a view: block. These settings will only apply to clients using this view.
  • Isolation (Partial): View-specific local-zone and local-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-specific local-data or local-zone types.
  • Global Settings Inheritance: Settings defined in the main server: block (like verbosity, num-threads, root-hints, global access-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 a view: 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 global access-control: rules. If allowed by global access-control, these clients receive Unbound's "default" DNS resolution behavior (i.e., using any local-zone/local-data defined directly in the server: 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 to 203.0.113.80.
  • Internal clients (192.168.1.0/24) should resolve files.example.com to 192.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:

  1. access-control-view: 192.168.1.0/24 internal_view:
    Clients from this subnet will use the internal_view.
  2. view: name: "internal_view":

    • local-zone: "example.com." transparent: For example.com within this view, Unbound will generally try to resolve names recursively.
    • local-data: "files.example.com. IN A 192.168.1.50": However, for files.example.com, it provides the internal IP 192.168.1.50. This overrides any public record for clients in this view.
    • It also defines a purely internal zone dev.lan.
  3. 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 global local-data wasn't present, Unbound would recursively resolve files.example.com from public DNS servers.
  4. domain-insecure: "files.example.com.":
    This is important. If example.com is DNSSEC-signed, and you are providing local-data for files.example.com that differs from the public, signed record, Unbound would normally treat your local data as "bogus" because it can't be validated by example.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 by local-zone/local-data directly in the server: 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 resolves x.com to an internal IP and view B resolves x.com to a public IP, the respective local-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 resolve portal.mycompany.local to 192.168.50.10 (a private IP).
  • External clients (everyone else) should resolve portal.mycompany.local to 203.0.113.55 (a public IP).
  • Internal clients should also be able to resolve adminpc.internal.lan to 192.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 the 192.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 running dig. 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.

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

  1. Edit unbound.conf:

    sudo nano /etc/unbound/unbound.conf
    

  2. Add access-control-view and global local-data (for the "external" view) in the server: 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.
    

  3. Define the v_internal view block (outside the server: 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."
    

  4. Save the unbound.conf file and exit.

  5. Check configuration syntax:

    sudo unbound-checkconf /etc/unbound/unbound.conf
    
    Fix any errors.

  6. Restart Unbound:

    sudo systemctl 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 to 192.168.50.10.
  • adminpc.internal.lan should resolve to 192.168.50.5.
  • The reverse lookup for 192.168.50.5 should return adminpc.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
This is more advanced and depends on your OS and network setup.

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 to 203.0.113.55.
  • adminpc.internal.lan should return NXDOMAIN (Non-Existent Domain) because it's defined as static in the global scope with no local-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.