Skip to main content
Xsec

Create a DNS server on Debian

Published on 17 min read

Updated on

Part 3 of 6
In this series40 min read in total
  1. Installing Docker on Debian
  2. Install an OpenSSL self-signed certificate with nginx
  3. Create a DNS server on Debian
  4. Install Nginx + Webdav on debian 11
  5. Create an OpenVPN server on debian
  6. SSH connection with public key

A DNS server translates names into addresses. Rather than remembering that the NAS sits at 172.16.30.30, you type nas.it.fr and the server does the mapping. This lab builds that service end to end with Bind9 on Debian: a machine that answers for its own domain, in both directions, and relays to the outside whatever it does not know.

SummaryWhat you will be able to do by the end
  • Prepare a Debian machine so it can carry a DNS service.
  • Install Bind9 and know what each of its files is for.
  • Write a forward zone, translating names into addresses.
  • Write a reverse zone, translating addresses into names.
  • Declare those zones and forward outside anything beyond the domain.
  • Validate the configuration before restarting the service, and diagnose a failure.

What a DNS server does and what we are building

A resolution happens in three moves. The client queries the DNS server it was pointed at. If the requested name belongs to a zone this server holds the reference copy of, it answers directly and is then authoritative. Otherwise it passes the question to another server, called a forwarder, and relays its answer back.

Our server will play both roles: authoritative for it.fr, forwarder to the outside for everything else.

Here is this lab’s configuration. These values come back in every file, replace them with yours throughout.

SettingValueWhere it reappears
Server address172.16.10.10/etc/hosts, resolv.conf, zones
Subnet mask255.255.0.0, that is /16network interface, reverse zone name
Machine namedns/etc/hostname, /etc/hosts, zones
Domain nameit.freverywhere
Forward zoneit.fr in /etc/bind/db.it.frnamed.conf.local
Reverse zone16.172.in-addr.arpa in /etc/bind/db.it.fr.invnamed.conf.local

Three machines will be declared in the zone:

Full nameAddressRole
dns.it.fr172.16.10.10the DNS server itself
client.it.fr172.16.20.20a client workstation
nas.it.fr172.16.30.30a file server
An issue with sudo?
DangerPlease don't use the root account

If you configure your server directly as root, don’t forget to remove sudo from each command. If you set a password for the root account, the sudo command won’t be accepted. Connect directly as root to execute commands. You can also reinstall your system leaving the root password empty during installation. sudo will install and work properly.

A DNS server advertises its own address to clients. That address must therefore be known and stable before anything is installed.

Step 1: pin down the machine’s network identity

  1. Name the machine

    Terminal window
    sudo nano /etc/hostname
    /etc/hostname
    dns

    This file holds the short name only, without the domain. The full name dns.it.fr is assembled by /etc/hosts two steps further down.

  2. Check the interface address

    Terminal window
    ip a
    ip a
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
    valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
    valid_lft forever preferred_lft forever
    2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:cd:01:1a brd ff:ff:ff:ff:ff:ff
    altname enp11s0
    inet 172.16.10.10/16 brd 172.16.255.255 scope global ens192
    valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fecd:11a/64 scope link
    valid_lft forever preferred_lft forever

    Interface ens192 does carry 172.16.10.10/16. If you already fixed the address during the Debian installation, move on to the next step.

    WarningA dynamic address dooms the service

    The server writes its own address into its zone files, and clients remember it in their configuration. A DHCP lease that changes address makes the whole thing wrong at once, without a single error message. Static addressing is a condition, not a preference.

  3. Switch the interface to static if needed

    Edit /etc/network/interfaces
    Terminal window
    sudo nano /etc/network/interfaces
    /etc/network/interfaces
    # This file describes the network interfaces available on your system
    # and how to activate them. For more information, see interfaces(5).
    source /etc/network/interfaces.d/*
    # The loopback network interface
    auto lo
    iface lo inet loopback
    # The primary network interface
    allow-hotplug ens192
    iface ens192 inet static
    address 172.16.10.10
    netmask 255.255.0.0
    gateway 172.16.1.1
  4. Declare the full name in /etc/hosts

    Terminal window
    sudo nano /etc/hosts
    /etc/hosts
    172.16.10.10 dns.it.fr dns
    127.0.0.1 dns

    The syntax is DNS_SERVER_IP<tab>HOSTNAME.DOMAIN<tab>HOSTNAME.

    ExplanationWhy this file matters before installation

    /etc/hosts is consulted before DNS by the system resolver. It lets the machine know its own full name while Bind9 is not installed yet, and therefore lets services start without waiting on a DNS that does not exist. That is what avoids long boot-time stalls.

  5. Point at the resolver to use

    Terminal window
    sudo nano /etc/resolv.conf
    /etc/resolv.conf
    domain it.fr
    search it.fr
    nameserver 172.16.10.10

    The search it.fr line lets you type nas instead of nas.it.fr: the suffix is appended automatically. The nameserver line designates the server to query, here the machine itself.

    WarningThis file often gets rewritten

    Depending on the Debian setup, resolvconf, systemd-resolved or the DHCP client may regenerate /etc/resolv.conf at boot and wipe your lines. If the content disappears after a reboot, either remove the service responsible, or declare the same values through dns-nameservers and dns-search in /etc/network/interfaces.

    Danger

    The machine needs to be restarted:

    Terminal window
    sudo reboot

The machine has a fixed address, a full name, and knows who to query. The service can now be installed.

Step 2: install Bind9

Terminal window
sudo apt update && sudo apt install bind9 dnsutils

apt update refreshes the package list from sources.list. bind9 is the DNS server proper, dnsutils provides the dig and nslookup diagnostic tools, indispensable for the final verification.

The installation drops several files into /etc/bind. Knowing which does what saves a lot of guesswork:

FileRoleDo we touch it?
named.confsimply includes the three files belowno
named.conf.optionsglobal behaviour: cache, forwarders, DNSSECyes, step 4
named.conf.localdeclaration of the zones this server is responsible foryes, step 4
named.conf.default-zonestechnical zones (localhost, root)no
db.local, db.127shipped zone templates, meant to be copiedwe copy them
db.<domain>the actual content of a zone, created by youyes, step 3
Notenamed and bind9, two names for one service

The daemon is called named (name daemon), the package is called bind9. Configuration files therefore carry named, while service commands use bind9. That is not an inconsistency, it is a historical leftover.

The configuration files are there, but none of them describes it.fr yet.

Step 3: write the zone files

A zone is a file listing the records this server is authoritative for. Two are needed: one to translate names into addresses, one for the opposite.

The forward zone

  1. Copy the shipped template

    Terminal window
    sudo cp /etc/bind/db.local /etc/bind/db.it.fr

    db.local already holds a valid SOA and NS record: starting from that copy avoids rewriting a structure that is easy to break.

  2. Replace the example domain

    The template uses localhost throughout. A global replacement is enough to turn it into our domain.

    Terminal window
    sudo sed -i 's/localhost/it.fr/g' /etc/bind/db.it.fr
    TipThe sed options

    -i edits the file in place, s/old/new/ is the substitution command, and the trailing g applies it to every occurrence on a line rather than the first only. Without -i, sed merely prints the result without saving anything, which is incidentally a good way to check before applying.

  3. Complete the zone

    Terminal window
    sudo nano /etc/bind/db.it.fr
    /etc/bind/db.it.fr
    ;
    ; Forward zone for it.fr
    ;
    $TTL 604800
    @ IN SOA dns.it.fr. root.it.fr. (
    2021102001 ; Serial
    604800 ; Refresh
    86400 ; Retry
    2419200 ; Expire
    604800 ) ; Negative Cache TTL
    ;
    @ IN NS dns.it.fr.
    @ IN A 172.16.10.10
    dns IN A 172.16.10.10
    client IN A 172.16.20.20
    nas IN A 172.16.30.30

    The A record named client is what lets you reach 172.16.20.20 by typing client.it.fr.

    DangerThe trailing dot is not decorative

    In a zone file, a name that does not end with a dot gets the zone name appended. Writing @ IN NS dns.it.fr without the trailing dot produces dns.it.fr.it.fr., and the zone resolves nothing any more. Remember the reverse rule: a short name such as dns or client is written without a dot, a full name such as dns.it.fr. always carries one.

    WarningTwo traps inherited from the db.local template

    The copy of db.local holds two lines to correct rather than simply carry over. The NS points at localhost., which must be replaced by the server’s real name, otherwise the zone designates a machine that does not exist. And an @ IN AAAA ::1 publishes the IPv6 loopback address as the domain’s own: an IPv6-capable client will try to connect to itself. Delete that line as long as you have no real IPv6 addressing.

    DefinitionReading the SOA record

    The SOA (Start of Authority) opens every zone and appears only once. Its first two fields are the reference server and the contact address, where the first dot stands for the @: root.it.fr. means root@it.fr.

    FieldValueRole
    Serial2021102001version number of the zone
    Refresh604800delay before a secondary server rechecks
    Retry86400retry delay when the primary did not answer
    Expire2419200beyond this, a secondary stops answering for the zone
    Negative Cache TTL604800how long a “this name does not exist” answer is cached

    The Serial is the only field you will change often: it must be incremented on every zone change, otherwise secondary servers and caches will keep the old version. The most common convention is YYYYMMDDNN, the date followed by a two-digit counter, which yields an always-increasing and readable number.

Adding a record later on

Each new machine is declared on one line, in the name IN type value format:

NameClassTypeValue
printerINA172.16.40.40
/etc/bind/db.it.fr
dns IN A 172.16.10.10
client IN A 172.16.20.20
nas IN A 172.16.30.30
printer IN A 172.16.40.40

Increment the Serial, then reload the zone with sudo rndc reload it.fr, which avoids restarting the whole service.

Here are the main record types and what they associate:

TypeWhat it associatesExample
Aa name to an IPv4 addressnas IN A 172.16.30.30
AAAAa name to an IPv6 addressnas IN AAAA 2001:db8::30
CNAMEa name to another name, never to an addresswww IN CNAME dns.it.fr.
MXa domain to its mail server, with a priority@ IN MX 10 mail.it.fr.
TXTfree text, used by SPF, DKIM and domain validations@ IN TXT "v=spf1 -all"
NSa zone to the server authoritative for it@ IN NS dns.it.fr.
SOAthe administrative parameters of the zone, one per zonesee above
SRVa service to a host and a port_ldap._tcp IN SRV 0 5 389 dns.it.fr.
PTRan address to a name, the reverse of the A type10.10 IN PTR dns.it.fr.

Full list

The reverse zone

The forward zone answers “what is the address of nas.it.fr”. The symmetric question, “which name matches 172.16.30.30”, belongs to a separate zone. Mail servers, system logs and many administration tools rely on it to display names rather than addresses.

ExplanationWhy the zone name is backwards

A domain name reads from most specific to most general, left to right: nas inside it inside fr. An IP address does the opposite, going from most general to most specific: the 172.16 network then the machine. So that both can be handled by the same tree mechanism, the address is reversed and placed under the special in-addr.arpa domain.

NetworkMaskReverse zone name
172.16.0.0/1616.172.in-addr.arpa
192.168.1.0/241.168.192.in-addr.arpa
10.0.0.0/810.in-addr.arpa

Our network being a /16, the zone covers 16.172.in-addr.arpa and can therefore hold all three machines, which live in different subnets.

  1. Copy the reverse template

    Terminal window
    sudo cp /etc/bind/db.127 /etc/bind/db.it.fr.inv
  2. Write the PTR records

    Terminal window
    sudo nano /etc/bind/db.it.fr.inv
    /etc/bind/db.it.fr.inv
    ;
    ; Reverse zone for 172.16.0.0/16
    ;
    $TTL 604800
    @ IN SOA dns.it.fr. root.it.fr. (
    2021102001 ; Serial
    604800 ; Refresh
    86400 ; Retry
    2419200 ; Expire
    604800 ) ; Negative Cache TTL
    ;
    @ IN NS dns.it.fr.
    10.10 IN PTR dns.it.fr.
    20.20 IN PTR client.it.fr.
    30.30 IN PTR nas.it.fr.

    The left-hand part is what remains of the address once the zone name is removed, itself reversed. For 172.16.20.20 in the 16.172.in-addr.arpa zone, 20.20 is left. The value on the right is a full name: the trailing dot is mandatory.

    Tip

    On a /24 network only one octet would remain on the left. For 192.168.1.42 in 1.168.192.in-addr.arpa, the line would simply be 42 IN PTR name.domain..

Both zones exist on disk, but Bind9 does not know yet that they concern it.

Step 4: declare the zones and the forwarders

  1. Declare the zones

    Terminal window
    sudo nano /etc/bind/named.conf.local
    /etc/bind/named.conf.local
    //
    // Do any local configuration here
    //
    // Consider adding the 1918 zones here, if they are not used in your
    // organization
    //include "/etc/bind/zones.rfc1918";
    zone "it.fr" {
    type master;
    file "/etc/bind/db.it.fr";
    allow-query { any; };
    };
    zone "16.172.in-addr.arpa" {
    type master;
    file "/etc/bind/db.it.fr.inv";
    allow-query { any; };
    };

    Each block ties a zone name to its file. type master means this server holds the reference copy and has nobody to query for that zone: that is what makes it authoritative. allow-query { any; } lets any machine ask the question.

    WarningA declared zone with no file prevents startup

    If the referenced file is missing or holds a syntax error, Bind9 refuses to load the zone and the service may not start at all. That is exactly why the reverse zone was written in the previous step before being declared here, and not the other way round.

    TipRestrict allow-query in production

    any is fine for a lab. On an exposed network, an open resolver can be abused to amplify denial-of-service attacks. Limit the query to your internal networks instead, for example allow-query { 172.16.0.0/16; localhost; };.

  2. Configure the forwarders

    Terminal window
    sudo nano /etc/bind/named.conf.options
    /etc/bind/named.conf.options
    options {
    directory "/var/cache/bind";
    // If there is a firewall between you and nameservers you want
    // to talk to, you may need to fix the firewall to allow multiple
    // ports to talk. See http://www.kb.cert.org/vuls/id/800113
    forwarders {
    1.1.1.1;
    8.8.8.8;
    };
    //========================================================================
    // If BIND logs error messages about the root key being expired,
    // you will need to update your keys. See https://www.isc.org/bind-keys
    //========================================================================
    dnssec-validation auto;
    auth-nxdomain no; # conform to RFC1035
    version none;
    listen-on-v6 { any; };
    };

    forwarders lists the servers to hand over questions that do not concern it.fr. That is what lets machines on the network reach the Internet while having a single DNS server configured.

    DangerNever put the server itself in forwarders

    Listing 172.16.10.10, this machine’s own address, creates a loop: the server forwards the question to itself, indefinitely, until the timeout expires. Forwarders must contain external resolvers only, for example 1.1.1.1 at Cloudflare or 8.8.8.8 at Google. The server already knows how to answer for what it hosts, it has no need to ask itself.

    Explanationforward only or forward first

    With no explicit mention, Bind9 uses forward first: it tries the forwarders then, on failure, queries the root servers itself. Adding forward only; forbids that second attempt. It is useful behind a firewall blocking direct outbound queries, but it makes the service entirely dependent on the forwarders. Note as well that forward only combined with dnssec-validation auto fails when the forwarder does not pass signature records through.

    Noteversion none

    This line stops the server from advertising its version number in its answers. It is a basic hygiene measure: it avoids telling an attacker which known vulnerabilities to try first.

Everything is written. What remains is checking before going live, which is far quicker than diagnosing a service that refuses to start.

Verify and troubleshoot

Bind9 ships two check commands that parse the files without touching the service. Use them systematically before any restart.

  1. Check the syntax

    Terminal window
    sudo named-checkconf
    sudo named-checkzone it.fr /etc/bind/db.it.fr
    sudo named-checkzone 16.172.in-addr.arpa /etc/bind/db.it.fr.inv
    Expected output
    zone it.fr/IN: loaded serial 2021102001
    OK
    zone 16.172.in-addr.arpa/IN: loaded serial 2021102001
    OK

    named-checkconf prints nothing at all when the configuration is correct. Each named-checkzone must end with OK and show the loaded serial number.

  2. Restart the service

    Terminal window
    sudo systemctl restart bind9
    sudo systemctl status bind9
  3. Query the server

    Terminal window
    dig @127.0.0.1 client.it.fr
    Excerpt of the answer
    ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1
    ;; ANSWER SECTION:
    client.it.fr. 604800 IN A 172.16.20.20

    The aa flag (authoritative answer) confirms the answer really comes from our zone and not from a cache.

    Terminal window
    dig @127.0.0.1 -x 172.16.20.20
    Excerpt of the answer
    ;; ANSWER SECTION:
    20.20.16.172.in-addr.arpa. 604800 IN PTR client.it.fr.
    Terminal window
    dig @127.0.0.1 example.com

    An answer here, with no aa flag, proves the forwarders work.

If one of these tests fails, the table below covers the most frequent causes.

SymptomLikely causeCheck
named-checkzone reports not at top of zonea trailing dot missing or one too manyreread every full name in the zone
The service refuses to starta declared zone whose file is missing or invalidsudo journalctl -u bind9 -n 50
SERVFAIL on a domain namezone not loaded, or file unreadable by the bind usernamed-checkconf, then the permissions on /etc/bind
NXDOMAIN on a name that is presentSerial not incremented, old version still cachedincrement the Serial then sudo rndc reload
Internal names answer, the outside does notforwarders missing, or looping on the server’s addressthe forwarders block in named.conf.options
The client resolves nothing while dig @127.0.0.1 worksthe client is not querying this servernameserver in its /etc/resolv.conf
TipAlways name the server being queried during tests

dig @127.0.0.1 name forces the query onto our server. Without the @, dig uses the system resolver, which may be another server and give a misleading answer. That is the first thing to fix when a test “means nothing”.

The service answers in both directions for it.fr and relays the rest outside. It can now act as a resolver for the other machines on the network, starting with those in the following articles of the series.

Use with an AI

Actions