<p>
The domain name system (DNS) resolves hostnames (e.g. <code>www.example.net</code>) to IP addresses.<br />
BIND is the most used DNS server on the internet.
</p>
<p>
Originally, this tutorial was written to show how to configure a subdomain<br />
in BIND 9 on Debian 10 (buster). It was extended with DNS over TLS (DoT) and<br />
how to block domains (e.g. ad servers) with BIND 9 on Debian 13 (trixie).
</p>

<h3>Contents</h3>
<ul>
<li><a href="#install">Installation</a></li>
<li><a href="#domain">Domain</a></li>
<li><a href="#firewall">Firewall</a></li>
<li><a href="#config">Configuration</a>
<ul>
<li><a href="#config_zone">Zone</a></li>
<li><a href="#config_reload">Reload</a></li>
</ul>
</li>
<li><a href="#test">Test</a></li>
<li><a href="#dynamic">Dynamic Updates</a>
<ul>
<li><a href="#dynamic_config">Configuration</a></li>
<li><a href="#dynamic_client">Client</a></li>
</ul>
</li>
<li><a href="#tls">DNS over TLS (DoT)</a></li>
<li><a href="#block">Block Domains</a></li>
<li><a href="#links">External Links for further Information</a></li>
</ul>

<h3 id="install">Installation</h3>
<p>
The official Debian repository includes BIND 9:
</p>
<pre><code class="language-bash">apt install bind9</code></pre>

<h3 id="domain">Domain</h3>
<p>
To run a name server for a subdomain <code>sub.example.net</code>,<br />
the following records are relevant in the zone of the domain <code>example.net</code>:
</p>
<pre class="file"><code class="language-dns">sub      IN  NS    ns1.sub.example.net.  ; name server for subdomain sub.example.net
ns1.sub  IN  A     192.0.2.1             ; IPv4 address of ns1.sub.example.net
ns1.sub  IN  AAAA  2001:db8:10::1        ; IPv6 address of ns1.sub.example.net
</code></pre>

<h3 id="firewall">Firewall</h3>
<p>
The DNS server <code>ns1.sub.example.net</code> needs to accept incoming packages<br />
on the following ports:
</p>
<ul>
<li>TCP port 53</li>
<li>UDP port 53</li>
</ul>

<h3 id="config">Configuration</h3>
<p>
The zone file for <code>sub.example.net</code> needs to be specified in the configuration file:
</p>
<dl class="file">
<dt><code class="filename">/etc/bind/named.conf.local</code></dt>
<dd>
<pre class="file"><code class="language-plaintext">zone "sub.example.net" {
    type master;
    file "/var/lib/bind/db.sub.example.net";
};
</code></pre>
</dd>
</dl>

<h4 id="config_zone">Zone</h4>
<p>
The zone file defines records for a (sub)domain.<br />
Here is an exemplary zone file for the subdomain <code>sub.example.net</code>:
</p>
<dl class="file">
<dt><code class="filename">/var/lib/bind/db.sub.example.net</code></dt>
<dd>
<pre class="file"><code class="language-dns">$TTL 86400
@         IN  SOA   ns1.sub.example.net. root.example.net. (
                    2020042301    ; Serial  (YYYYMMDDxx)
                          3600    ; Refresh  (1 hour)
                           600    ; Retry  (10 mins)
                        604800    ; Expire  (7 days)
                          1800 )  ; Negative Cache TTL  (30 mins)

@         IN  NS    ns1.sub.example.net.
ns1       IN  A     192.0.2.1
ns1       IN  AAAA  2001:db8:10::1

; other hostnames
somehost  IN  A     192.0.2.10
another   IN  A     192.0.2.11
</code></pre>
</dd>
</dl>
<p>
The content of the exemplary zone file means the following:
</p>
<ul>
<li>The default time to live (TTL) for each record is set to 86400 seconds (1 day).</li>
<li>"<code>@</code>" is an alias for the zone itself (<code>sub.example.net</code>).</li>
<li>The start of authority (SOA) record contains administrative information<br />
about the zone: name server, mail address, serial number, ...</li>
<li>The name server <code>ns1.sub.example.net</code> for the subdomain <code>sub.example.net</code> is<br />
specified in the NS record.</li>
<li>IPv4 addresses are stored in A records.</li>
<li>AAAA records contain IPv6 addresses.</li>
</ul>

<h4 id="config_reload">Reload</h4>
<p>
The following command reloads the configuration:
</p>
<pre><code class="language-bash">rndc reload</code></pre>

<h3 id="test">Test</h3>
<p>
Install the <code>dnsutils</code> package:
</p>
<pre><code class="language-bash">apt install dnsutils</code></pre>
<p>
Verify that the DNS lookup returns the correct IP address:
</p>
<pre><code class="language-bash">nslookup somehost.sub.example.net</code></pre>

<h3 id="dynamic">Dynamic Updates</h3>
<p>
Updating the DNS records is useful when IP addresses are dynamically assigned.<br />
For instance, DHCP servers automatically assign IP addresses to hosts.
</p>

<h4 id="dynamic_config">Configuration</h4>
<p>
The following command generates a random rndc key and<br />
prints a configuration example to the terminal:
</p>
<pre><code class="language-bash">rndc-confgen</code></pre>
<p>
To allow a client to update the DNS records of <code>sub.example.net</code> with an rndc key,<br />
enable it in the configuration file:
</p>
<dl class="file">
<dt><code class="filename">/etc/bind/named.conf.local</code></dt>
<dd>
<pre class="file"><code class="language-plaintext">include "/etc/bind/rndc.key";

zone "sub.example.net" {
    type master;
    file "/var/lib/bind/db.sub.example.net";
    allow-update { key "rndc-key"; };
};
</code></pre>
</dd>
</dl>

<h4 id="dynamic_client">Client</h4>
<p>
For updating DNS records, the following command line tool is used:
</p>
<pre><code class="language-bash">nsupdate</code></pre>
<p>
These commands update the IPv4 address of <code>somehost.sub.example.net</code>:
</p>
<pre><code class="language-plaintext">key rndc-key C3...DQ==
update delete somehost.sub.example.net A
update add somehost.sub.example.net 86400 A 192.0.2.123
send
</code></pre>

<h3 id="tls">DNS over TLS (DoT)</h3>
<dl class="file">
<dt><code class="filename">/etc/bind/named.conf.options</code></dt>
<dd>
<pre class="file"><code class="language-plaintext">tls mycert {
    cert-file "/etc/letsencrypt/live/example.net/fullchain.pem";
    key-file "/etc/letsencrypt/live/example.net/privkey.pem";
};

tls google-tls {
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    remote-hostname "dns.google";
};

tls cloudflare-tls {
    ca-file "/etc/ssl/certs/ca-certificates.crt";
    remote-hostname "one.one.one.one";
};

options {
    directory "/var/cache/bind";

    listen-on port 53 { any; };
    listen-on-v6 port 53 { any; };
    listen-on port 853 tls mycert { any; };
    listen-on-v6 port 853 tls mycert { any; };

    allow-recursion { any; };
    forwarders port 853 {
        8.8.8.8 tls google-tls;
        8.8.4.4 tls google-tls;
        1.1.1.1 tls cloudflare-tls;
        1.0.0.1 tls cloudflare-tls;
    };
    forward only;
};
</code></pre>
</dd>
</dl>
<p>
Add the user <code>bind</code> to the group <code>ssl-cert</code>:
</p>
<pre><code class="language-bash">usermod -a -G ssl-cert bind</code></pre>
<p>
Configure AppArmor to allow reading the certificates:
</p>
<dl class="file">
<dt><code class="filename">/etc/apparmor.d/local/usr.sbin.named</code></dt>
<dd>
<pre class="file"><code class="language-plaintext"># Allow BIND to read Let's Encrypt certificates
/etc/letsencrypt/archive/example.net/* r,
/etc/letsencrypt/live/example.net/* r,
</code></pre>
</dd>
</dl>

<h3 id="block">Block Domains</h3>
<dl class="file">
<dt><code class="filename">/etc/bind/named.conf.local</code></dt>
<dd>
<pre class="file"><code class="language-plaintext">include "/etc/bind/adservers-blacklist.conf";
</code></pre>
</dd>
</dl>
<dl class="file">
<dt><code class="filename">/etc/bind/adservers-blacklist.conf</code></dt>
<dd>
<pre class="file"><code class="language-plaintext">zone "adserver.com" { type master; notify no; file "/etc/bind/blocked.zone"; };
</code></pre>
</dd>
</dl>
<dl class="file">
<dt><code class="filename">/etc/bind/blocked.zone</code></dt>
<dd>
<pre class="file"><code class="language-dns">$TTL 86400
@  IN  SOA  ns1.example.net. hostmaster.example.net. (
            2026102701    ; serial number YYMMDDNN
                 28800    ; refresh  8 hours
                  7200    ; retry    2 hours
                864000    ; expire  10 days
                 86400 )  ; min ttl  1 day
@  IN  NS   ns1.example.net.
@  IN  A    127.0.0.1
*  IN  A    127.0.0.1
</code></pre>
</dd>
</dl>
<pre><code class="language-bash">wget -q -O - 'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=bindconfig&showintro=0&mimetype=plaintext' \
  | sed 's#null.zone.file#/etc/bind/blocked.zone#' \
  > /etc/bind/adservers-blacklist.conf
</code></pre>

<h3 id="links">External Links for further Information</h3>
<ul>
<li><a href="https://www.isc.org/bind/" target="_blank">
https://www.isc.org/bind/</a></li>
<li><a href="https://tools.ietf.org/html/rfc1035" target="_blank">
https://tools.ietf.org/html/rfc1035</a></li>
<li><a href="https://en.wikipedia.org/wiki/Zone_file" target="_blank">
https://en.wikipedia.org/wiki/Zone_file</a></li>
<li><a href="https://wiki.debian.org/Bind9" target="_blank">
https://wiki.debian.org/Bind9</a></li>
<li><a href="https://bind9.readthedocs.io/en/stable/" target="_blank">
https://bind9.readthedocs.io/en/stable/</a></li>
</ul>