Blog>>Networks>>Practical implementation of DNSSEC

Practical implementation of DNSSEC

DNSSEC (Domain Name System Security Extension) is a security feature that introduces data integrity and origin verification to the legacy DNS protocol. By enabling DNSSEC for a domain, we are able to tell if DNS communication related to that domain hasn't been corrupted or altered.

In another article, we cover the general principles of DNSSEC. However, this article focuses on a real-life implementation of this solution using BIND as a DNS server installed on a machine running Ubuntu. We are going to provide a step-by-step guide on how to set up a DNS server with enabled DNSSEC functionalities, starting from nothing more than a fresh, “vanilla” Ubuntu installation and a newly purchased domain. 

BIND (Berkeley Internet Name Domain) is the oldest, but still the most commonly deployed DNS system in the world. Up to this day it is the gold standard when it comes to feature set and conformability to norms. It’s also released under an open-source license. All that makes BIND a perfect candidate for showing a real-life DNS and DNSSEC configuration.

Architecture

In this article, we are going to cover two main scenarios:

  • Authoritative DNS server with a DNSSEC-enabled domain,
  • DNS recursive server (resolver) performing DNSSEC validation on behalf of clients.

In all discussed cases, we will use BIND 9.18 as the DNS service, running on the Ubuntu 22.04.4 operating system.

Authoritative DNS

In this scenario, we are going to configure two DNS servers to host the ns-testing.com domain (figure 1). This will provide redundancy in case any of those servers fail. The first server is going to be configured as the primary DNS server. The second server will be configured as the secondary DNS device. Only the primary server will contain a Read-Write copy of the DNS configuration. The secondary server will maintain a  Read-Only version of the configuration and synchronize with the primary DNS at regular intervals, using a zone transfer mechanism.

Fig.1: DNS architecture with authoritative servers for the ns-testing.com domain
Figure 1: DNS architecture with authoritative servers for the ns-testing.com domain

After completing this task, we will sign the zone and establish trust with the parent domain so that the domain can be correctly resolved by servers with DNSSEC validation enabled.

Recursive DNS

In this part, we will configure a separate recursive DNS server allowing queries only from a specific network (192.168.38.0/24), and then we are going to enable DNSSEC validation on that server:

Fig.2: Recursive DNS setup
Figure 2: Recursive DNS setup

DNS service implementation

Installing BIND

The first step for both scenarios is to install BIND on an Ubuntu server. To do that, you can use the apt tool:

$ sudo apt update
$ sudo apt install bind9 bind9-doc

Besides BIND itself (and all its required dependencies), we are also going to install additional documentation. And that’s it. After running the commands presented above, BIND is installed and ready to be configured. 

By default, BIND works for both IPv4 and IPv6. In our case, we are going to use only IPv4, so optionally, we can disable IPv6 by editing the following file and adding the “-4” parameter in the OPTIONS section:

$ sudo vim /etc/default/named
#
# run resolvconf?
RESOLVCONF=no

# startup options for the server
OPTIONS="-u bind -4"

To apply these changes, restart the BIND service:

$ sudo systemctl restart bind9

Depending on the Linux distribution, a BIND configuration may be organized in different ways. On Ubuntu/Debian, for instance, configuration is stored in the /etc/bind/ folder and consists of the following main components:

  • named.conf – Main configuration file with references to more specific files. Usually, no changes are made here.
  • named.conf.default-zones – Configuration file with definitions of standard default zones like “localhost” or broadcast ("255.in-addr.arpa"), as per recommendations described in RFC1912      link-icon.
  • named.conf.options – Configuration file containing settings of the DNS server itself (e.g., enable/disable recursion, configure forwarders, etc.).
  • named.conf.local – The file where we declare zones associated with the DNS server (in other words - authoritative zones).

Configuring authoritative DNS servers

The Primary DNS server

Let’s start with some general settings and add two statements within the options section located in the named.conf.options file:

$ sudo vim /etc/bind/named.conf.options
options {
        listen-on-v6 { none; };
        allow-transfer { none; };
};

The first option disables listening on any IPv6 address. The second option globally disables zone transfers (otherwise, they are enabled by default). Generally, it is a bad idea to expose the content of our zones to anyone on the Internet. We will allow zone transfers only for the specific IP address of the secondary member.

As mentioned in the previous section, zone declarations are located in the named.conf.local file. Content of zones (specific records), on the other hand, are stored in separate zone files, typically using the following naming convention: db..

According to the Ubuntu documentation      link-icon, static zone files (updated only by administrators) should normally be placed directly within the /etc/bind/ folder. However, in our case, the created zones will be automatically signed and updated with DNSSEC records. The right place to store such dynamic zones is the /var/lib/bind/ folder. If you want to use the default location, you will have to modify the AppArmor settings      link-icon (the Linux kernel security module used on Ubuntu).

In our case, we need to declare the ns-testing.com zone in the named.conf.local file and create a db.ns-testing.com file with the zone’s content. Let’s start with the zone definition:

$ sudo vim /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 "ns-testing.com" {
        // Zone type
        type primary;
        // Zone file
        file "/var/lib/bind/db.ns-testing.com";
        // Secondary server allowed to transfer the zone
        allow-transfer { 98.71.216.161; };
        // Notify secondary servers about changes (default)
        notify yes;
};

In the example above, we define the type of our zone (“primary”), a file with zone records (db.ns-testing.com), and a host that is allowed to receive zone transfers (98.71.216.161 – the secondary server). By default, the primary DNS server is also configured to send a NOTIFY message (RFC1996      link-icon) whenever there is a change in the configuration to speed up the synchronization process. This behavior is controlled by the notify statement. Without the notify feature enabled, the secondary server will only perform a zone transfer at a regular interval defined in the SOA record.

In the next step, we are going to create a file with all zone records:

$ sudo vim /var/lib/bind/db.ns-testing.com
$TTL    3600
@       IN      SOA     ns1.ns-testing.com. admin.ns-testing.com. (
                                2         ; Serial
                             3600         ; Refresh
                              300         ; Retry
                            86400         ; Expire
                             3600 )       ; Negative Cache TTL

        IN      NS      ns1.ns-testing.com.
        IN      NS      ns2.ns-testing.com.

ns1     IN      A       51.137.105.102
ns2     IN      A       98.71.216.161
www     IN      A       51.137.105.102
        IN      A       98.71.216.161

The Resource Record (RR) format used in a zone file is as follows:

  • Name (optional) – The domain name where the Resource Record is found. If not provided, the name is taken from the previous record.
  • TTL (optional) – A 32-bit integer defining the time-to-live of the RR. It informs resolvers how long they can cache the record (in seconds) before it should be discarded. If not provided, the value from the $TTL directive is used.
  • Class (mandatory) – Value identifying a protocol family. In practical implementations, it is always set to “IN” value (The Internet).
  • RR Type (mandatory) – Record type (e.g., NS, A, SOA).
  • RR Data (mandatory) – Record data. Each record type has its own structure in this field.

Besides Resource Records, a zone file can also contain so-called directives - special keywords starting with the “$” sign. For example, you can see at the beginning of the zone file the $TTL directive that specifies the default TTL for Resource Records. Another important directive is $ORIGIN, which sets the domain name that is appended to any unqualified/not-absolute name (without a dot at the end). Usually, we don’t see this directive in the configuration file; however, it is always implicitly set to the zone name (in our case, that would be: $ORIGIN ns-testing.com.). You can use multiple $ORIGIN directives in the same zone file, affecting Resource Records defined below these directives.

In the example above, we can also see the “@” sign. When used in the name field of a specific Resource Record, it represents the current origin.

Having all that in mind, we can now dissect each item from the zone file presented above:

$TTL    3600

This directive, located at the top of the file, defines the default time-to-live (in seconds) for the whole zone, which is then used for records that don’t have their own TTL defined. Other time formats (and their combinations) are supported as well:

  • #s – seconds
  • #m – minutes
  • #h – hours
  • #d – day
  • #w – week

We can have the TTL set to, e.g., 2d12h (2 days and 12 hours).

@       IN      SOA     ns1.ns-testing.com. admin.ns-testing.com. (
                                2         ; Serial
                             3600         ; Refresh
                              300         ; Retry
                            86400         ; Expire
                             3600 )       ; Negative Cache TTL

The next entry is the SOA (Start of Authority) Resource Record, the most important and complex record in the zone file. The “@” sign indicates that the record refers to the whole ns-testing.com domain. The SOA record defines the authoritative name server and global parameters of the zone. Only one SOA record can exist in the zone file, and it must be the first record that is defined. The table below describes all components of the SOA record:

FieldValueDescription
Name@ (ns-testing.com)Domain name.
TTLNone (3600s taken from the $TTL directive)TTL of the SOA record.
ClassINProtocol family.
RR TypeSOARecord type.
Name Serverns1.ns-testing.com.A name server that will respond authoritatively for the domain.
Emailadmin.ns-testing.com.An email address of the person/group responsible for the domain. Because the “@” sign has another meaning in the zone file, it should be replaced by a dot here.
Serial Number2A 32-bit number identifying the current revision of the zone configuration. It should be increased after every change in the zone file (not necessarily by 1). This is very important when secondary servers are used. A common way to format the SN is by adding information about the date of the change: YYYYMMDDSS where YYYY = year, MM = month, DD = day, and SS = a sequence number (in case of multiple changes in one day).
Refresh3600A 32-bit value in seconds indicating zone refreshment frequency by secondary servers.
Retry300A 32-bit value (in seconds) defining the time between consecutive attempts to perform a zone transfer by secondary servers after the initial refresh timer expires.
Expire86400A 32-bit value (in seconds) indicating when the zone data on a secondary server is no longer authoritative after unsuccessful attempts to perform the zone transfer. Secondary servers stop responding authoritatively after this timer expires.
Negative Cache TTL3600A 32-bit value (in seconds) defining the time NXDOMAIN responses should be cached on a resolver.

Table 1: SOA record fields

Note that timers defined in the SOA record can also use the same time formats as the $TTL directive.

IN      NS      ns1.ns-testing.com.
IN      NS      ns2.ns-testing.com.

The two NS (Name Server) records presented above define authoritative DNS servers for the domain. Note that they don’t have a name field. In this case, the name is taken from the previous record (SOA), which refers to the ns-testing.com domain.

ns1     IN      A       51.137.105.102
ns2     IN      A       98.71.216.161

Next, we have two A records defining IPv4 addresses for both name servers. These names don’t have a dot at the end, which means that the name defined by the $ORIGIN directive should be appended (ns-testing.com. in our case).

www     IN      A       51.137.105.102
        IN      A       98.71.216.161

Another two A records define IPv4 addresses for a host (www.ns-testing.com      link-icon). Also in this case the record without its own name refers to the name defined in the previous record.

In order to make your configuration active on a DNS server, you can simply restart the BIND service:

$ sudo systemctl restart bind9

Note that this operation causes a short outage (usually a couple of seconds). However, there is a tool that provides far more control over the configuration reloading process. It’s called rndc (Remote Name Daemon Control) and, in general, is a command line tool used to manage the operations of the BIND server. It provides quite a lot of options (see the man page      link-icon). In our case, we will be using two options: reconfig and reload.

$ sudo rndc reconfig

This command reloads the named.config file and loads any new zones. Existing zones, even if they contain pending changes, are not reloaded.

$ sudo rndc reload ns-testing.com

To reload the configuration of an already existing zone (ns-testing.com in this case), use the command above. If no zone is specified, all zones are reloaded. 

The Secondary DNS server

Configuring the secondary DNS server is much easier since we don’t have to provide a zone file. Instead, all Resource Records are being synchronized with the primary server. Besides adding the same options in the named.conf.options file like for the primary server, the only thing that needs to be done is to define the zone in the named.conf.local file:

$ sudo vim /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 "ns-testing.com" {
        type secondary;
        file "db.ns-testing.com";
        masters { 51.137.105.102; };
};

As with the primary server, we define the ns-testing.com zone, but this time as type “secondary”. We also specify a file name, however, on the secondary server, it only stores cached zone data transferred from the primary server. This allows the secondary server to quickly load zone data after a restart without waiting for the zone transfer to happen. The file is stored in a binary format in the /var/cache/bind/ folder. The masters statement defines the IP address of the primary DNS server.

After reloading the configuration, the secondary server should immediately start responding to queries:

> nslookup www.ns-testing.com 98.71.216.161
Server:  UnKnown
Address:  98.71.216.161

Name:    www.ns-testing.com
Addresses:  98.71.216.161
            51.137.105.102

Zone transfer process between the primary and secondary server

After configuring both the primary and secondary DNS servers, whenever there is a change in the zone file on the primary server, it will be automatically replicated to the secondary device. Figure 3 shows the zone transfer process in detail:

  1. If there is any change in the zone file and the configuration is reloaded, the primary server sends a NOTIFY message to the secondary server. The message refers to the SOA record in the “Query” section. In the “Answer” section, we can find the SOA record data.
  2. The secondary server acknowledges the notification by sending a response NOTIFY message. At this stage, the secondary server decides whether to proceed with the zone transfer or ignore the notification. It checks the serial number from the received message and compares it with the current serial. If the received number is larger, the secondary server continues with the next step.
  3. The secondary server sends a standard query for the SOA record. This is done as a result of a received NOTIFY message. In addition to that, the secondary server also periodically sends SOA queries with intervals defined by the Refresh timer in the SOA record.
  4. A response with the SOA record is sent by the primary server. The actual zone transfer is performed only if the serial number in the received SOA record is greater than the one currently held on the secondary server.
  5. The secondary server switches to TCP and queries either for the IXFR (incremental zone transfer) or AXFR (full zone transfer). The incremental zone transfer is preferred as it only contains actual changes in the configuration. If this is not possible, a full zone transfer is performed, and all records are read.

Updating the serial number in the SOA record after every change is crucial for the zone transfer process to work correctly. If a change is made within the zone file but the serial number is not incremented, the primary server will send the NOTIFY message, but there won’t be any reaction from the secondary server. The secondary server will also not trigger the zone transfer after periodically querying the SOA record (see points 3 and 4).

Fig.3: Zone transfer process
Figure 3: Zone transfer process

Configuring zone delegation

At this stage, both the primary and the secondary servers are fully functional. We can directly query these servers for any record configured for the ns-testing.com domain. However, if we try to resolve any records from that domain using a public resolver, we will still receive a NXDOMAIN (non-existent domain) response:

> nslookup www.ns-testing.com
Server:  dns.mhlan.local
Address:  192.168.1.201

*** dns.mhlan.local can't find www.ns-testing.com: Non-existent domain

This is because there is no relationship between the com domain and ns-testing.com. To create such a relationship, we must create a zone delegation. This is done by a domain registrar (an organization that handles the reservation of domain names). Usually it is possible to perform such an operation using a convenient web interface to manage registered domains.

In the figure below, you can see both previously configured servers being added to the list of name servers hosting the ns-testinig.com domain.

Fig.4: Creating zone delegation.
Figure 4: Creating zone delegation.

This creates two NS records at the com level, indicating that both servers mentioned above are authoritative for the ns-testing.com domain.

At this point we are almost done, but there is still one important step to be made. NS records always contain FQDNs of DNS servers and not their IP addresses. But how will a resolver know what are the IP addresses of authoritative name servers for a specific domain if the names of those servers are also within that domain? In order to resolve this “chicken and egg” problem we must create so-called “glue” records –  defined at the parent domain level, specifying IP addresses of a child domain’s name servers.

Fig.5: “Glue” records for the ns-testing.com domain.
Figure 5: “Glue” records for the ns-testing.com domain.

After completing these tasks (and waiting a little bit for this information to be propagated on the Internet), we will be able to resolve names within our domain using any resolver with Internet access:

> nslookup www.ns-testing.com
Server:  dns.mhlan.local
Address:  192.168.1.201

Non-authoritative answer:
Name:    www.ns-testing.com
Addresses:  98.71.216.161
            51.137.105.102

This is possible because the parent domain’s DNS servers send IP addresses of authoritative servers for the ns-testing.com domain within every response related to that domain (“Additional” section). In the example below, you can see a response sent by one of the authoritative .com servers. Note that there is no “Answer” section (the server is not authoritative for ns-testing.com and doesn’t allow recursive queries), only the “Authority” and “Additional” sections:

$ dig @a.gtld-servers.net. www.ns-testing.com.

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @a.gtld-servers.net. www.ns-testing.com.
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50175
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.ns-testing.com.            IN      A

;; AUTHORITY SECTION:
ns-testing.com.         172800  IN      NS      ns1.ns-testing.com.
ns-testing.com.         172800  IN      NS      ns2.ns-testing.com.

;; ADDITIONAL SECTION:
ns1.ns-testing.com.     172800  IN      A       51.137.105.102
ns2.ns-testing.com.     172800  IN      A       98.71.216.161

;; Query time: 53 msec
;; SERVER: 192.5.6.30#53(192.5.6.30)
;; WHEN: Tue Jun 25 19:22:44 CEST 2024
;; MSG SIZE  rcvd: 115

Setting up a recursive DNS server

Recursion settings can be changed in the named.conf.options file. By default, BIND doesn’t resolve any names on behalf of clients. We have to explicitly set it up:

acl "trusted" {
        192.168.38.0/24;
};

options {
        listen-on-v6 { none; };
        recursion yes;
        allow-recursion { trusted; };
};

All changes that need to be introduced to the configuration file are in the code above.

The first section defines an access list called “trusted” with IP addresses of clients that will be allowed to recursively resolve DNS queries. In the options section, there are three statements defined:

  • listen-on-v6 – here, we declare that we don’t want to listen on any IPv6 address,
  • recursion yes – this option enables recursion on the server,
  • allow-recursion – in this option, we define which clients are allowed to perform recursive queries (in our case, from the “trusted” acl).

After reloading the configuration using the rndc reconfig command, we can verify that the newly configured DNS resolver works correctly:

> nslookup codilime.com. 192.168.38.128
Server:  UnKnown
Address:  192.168.38.128

Non-authoritative answer:
Name:    codilime.com
Addresses:  2606:4700:20::681a:bb0
            2606:4700:20::681a:ab0
            2606:4700:20::ac43:4a2b
            104.26.10.176
            172.67.74.43
            104.26.11.176

Note: It’s never a good idea to mix recursion with publicly available authoritative zones on the same server. Even if we limit recursion queries only to specific internal hosts, it’s always possible to spoof source IP addresses. This opens up the possibility for performing different attacks using DNS protocol, e.g. amplification attack      link-icon.

DNSSEC configuration

DNSSEC on authoritative servers

Maintaining DNSSEC signed zones is a complicated task when done manually, and requires many different steps to be performed at specific times. Any missed or misconfigured step may make the whole domain unavailable. Over the years, there have been many different ways to simplify DNSSEC configuration and management on BIND servers. Each method presented below provides one step forward toward making the DNSSEC maintenance process less troublesome.

Let’s examine those methods in the order they were introduced:

  • Manual – This was the first method to be introduced into BIND, and as the name suggests, all tasks related to DNSSEC maintenance must be done manually. This includes:

    • generating keys,
    • signing zones (both during the initial setup and then after every change in the zone file),
    • rolling keys and making sure that they don’t expire.

A good example of a fully manual DNSSEC configuration can be found here      link-icon.

  • Semi-Automatic – First introduced in BIND 9.7. Allows automatic signing and re-signing of resource records while still leaving keys generation to a DNS administrator. Although fully automatic methods are available, you may still require semi-automatic to cover some more complex scenarios, e.g. with DNSSEC keys stored in an HSM module (Hardware Security Module).
  • Fully Automatic – In this case, all tasks are done automatically, including creating and rolling DNSSEC keys. The first version, introduced in BIND 9.11, used a separate utility written in Python called dnssec-keymgr, which was expected to be run on a regular basis (e.g. via cron). Only an initial key was required. All other operations were performed based on a policy file and timing information from DNSSEC key files. In BIND 9.17, all those features have been integrated with the main DNS service, and now they can be configured using dnssec-policy. The latest method also handles the initial creation of keys. 

BIND setup

In our example, we will use the latest method (dnssec-policy). In order to quickly sign a zone using predefined settings, we need to add these two statements to the zone declaration from the named.conf.local file and reload the configuration:

zone "test.com" {
        // Enable DNSSEC and use a default policy
        dnssec-policy default;
        // Enable inline signing
        inline-signing yes;
};

The dnssec-policy statement causes the zone to be signed and enables automatic DNSSEC maintenance for that zone according to the built-in “default” policy. The second statement is mandatory with the DNSSEC policy. The inline-signing option allows BIND to automatically create and maintain a signed version of the zone file (with the .signed extension) in addition to the original, unsigned zone. When the file with the unsigned zone is changed, and the configuration is reloaded, BIND updates the signed copy as well.

The default DNSSEC policy provides only a basic set of options: a combined key (CSK) with an unlimited lifetime instead of a separate pair of ZSK/KSK keys and NSEC for denial of existence. More information about the default policy can be found here      link-icon. If we want to have something more than that, or we want to be sure that the policy won’t change on upgrade, we have to create our own policy. This way, we can have full control over all aspects of DNSSEC management, including:

  • types of DNSSEC keys,
  • cryptographic algorithms used when creating keys,
  • key rollover frequency,
  • RRSIG (Resource Record Signature) refreshment frequency.

Let’s create a new custom DNSSEC policy that we are going to use for our test domain ns-testing.com. The policy should be placed in the named.conf.options file.

dnssec-policy custom {
        dnskey-ttl 600;
        keys {
                ksk lifetime 365d algorithm ecdsap256sha256;
                zsk lifetime 60d algorithm ecdsap256sha256;
        };
        max-zone-ttl 3600;
        parent-ds-ttl 6h;
        parent-propagation-delay 2h;
        publish-safety 2d;
        retire-safety 2d;
        signatures-refresh 5d;
        signatures-validity 15d;
        signatures-validity-dnskey 15d;
        zone-propagation-delay 2h;
        nsec3param;
};

The dnssec-policy section starts with a name (“custom” in our case) followed by a set of specific parameters enclosed within braces. A brief description of those parameters can be found in the table below:

ParameterValue(s)Description
dnskey-ttl600TTL for the DNSKEY records (in seconds).
keysksk lifetime 365d algorithm ecdsap256sha256 zsk lifetime 60d algorithm ecdsap256sha256Defines key types, lifetimes (365 days for KSK and 60 days for ZSK), and cryptographic algorithms.
max-zone-ttl3600Specifies the maximum permissible TTL (in seconds) for a zone that uses the policy. Any encountered record with a higher TTL will cause the zone to fail to be loaded.
parent-ds-ttl6hTTL of the DS record, defined within the parent zone.
parent-propagation delay2dExpected propagation delay from the time when the parent zone is updated with a new DS record to the time when the new version of the record is served by all of the zone’s name servers.
publish-safety2dAdditional time between publishing keys and making them active. This is an extra margin to cover any unforeseen events.
retire-safety2dAdditional amount of time a key remains published after it’s no longer active, to cover any unforeseen events.
signatures-refresh5dSpecifies a time interval between RRSIG records expiration and signature renewal. In other words, the signature is renewed when the time until the expiration is less than the specified interval.
signatures-validity15dSpecifies the validity period of RRSIG records.
singatures-validity-dnskey15dSpecifies the validity of signatures attached to DNSKEY records
zone-propagation-delay2hExpected propagation delay from the time when a zone is updated to the time when the new version of the zone is published on all secondary servers.
nsec3paramNone (iterations 0, optout no, salt-length 0 by default)Enable NSEC3 for denial of existence instead of default NSEC.

Table 2: DNSSEC policy

It is important to remember that the lifetime of a key should be longer than the time needed for a rollover. In particular, it needs to be longer than the publication interval (dnskey-ttl + publish-safety + zone-propagation-delay) and the retire interval (max-zone-ttl + retire-safety + zone-propagation-delay for ZSK and parent-ds-ttl + retire-safety + parent-propagation-delay for KSKs and CSKs).

A full description of DNSSEC policy syntax can be found here      link-icon.

To configure DNSSEC for our authoritative domain, first place the DNSSEC policy within the named.conf.options file. Next, in the named.conf.local file, add options to enable this policy for the ns-testing.com zone:

//
// 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 "ns-testing.com" {
        type primary;
        file "/var/lib/bind/db.ns-testing.com";
        allow-transfer { 98.71.216.161; };
        dnssec-policy custom;
        inline-signing yes;
};

Reload the configuration using the following command:

$ sudo rndc reconfig

After reloading the configuration, BIND should generate a new KSK/ZSK key pair for that zone and sign all resource records within that zone. Note that when DNSSEC policies are used, new keys are created separately for every signed zone. We can verify if everything went fine by viewing the logs (/var/log/syslog by default):

named[579]: scheduled loading new zones
named[579]: zone ns-testing.com/IN (unsigned): loaded serial 5
named[579]: zone ns-testing.com/IN (signed): loaded serial 5
named[579]: zone ns-testing.com/IN (signed): could not get zone keys for secure dynamic update
named[579]: zone ns-testing.com/IN (signed): receive_secure_serial: unchanged
named[579]: zone ns-testing.com/IN (signed): sending notifies (serial 6)
named[579]: zone ns-testing.com/IN (signed): reconfiguring zone keys
named[579]: keymgr: DNSKEY ns-testing.com/ECDSAP256SHA256/54249 (KSK) created for policy custom
named[579]: keymgr: DNSKEY ns-testing.com/ECDSAP256SHA256/11836 (ZSK) created for policy custom
named[579]: Fetching ns-testing.com/ECDSAP256SHA256/54249 (KSK) from key repository.
named[579]: DNSKEY ns-testing.com/ECDSAP256SHA256/54249 (KSK) is now published
named[579]: DNSKEY ns-testing.com/ECDSAP256SHA256/54249 (KSK) is now active
named[579]: Fetching ns-testing.com/ECDSAP256SHA256/11836 (ZSK) from key repository.
named[579]: DNSKEY ns-testing.com/ECDSAP256SHA256/11836 (ZSK) is now published
named[579]: DNSKEY ns-testing.com/ECDSAP256SHA256/11836 (ZSK) is now active
named[579]: zone ns-testing.com/IN (signed): zone_addnsec3chain(1,INITIAL|CREATE,0,-)
named[579]: zone ns-testing.com/IN (signed): next key event: 23-Jun-2024 13:47:55.844
named[579]: any newly configured zones are now loaded
named[579]: running
named[579]: client @0x77e818163398 98.71.216.161#44137 (ns-testing.com): transfer of 'ns-testing.com/IN': IXFR version not in journal, falling back to AXFR
named[579]: client @0x77e818163398 98.71.216.161#44137 (ns-testing.com): transfer of 'ns-testing.com/IN': AXFR-style IXFR started (serial 11)
named[579]: client @0x77e818163398 98.71.216.161#44137 (ns-testing.com): transfer of 'ns-testing.com/IN': AXFR-style IXFR ended: 1 messages, 29 records, 2096 bytes, 0.001 secs (2096000 bytes/sec) (serial 11)
named[579]: managed-keys-zone: Key 20326 for zone . is now trusted (acceptance timer complete)
named[579]: zone ns-testing.com/IN (signed): sending notifies (serial 11)

An important thing to take note of is that the SOA serial number for the signed version of the zone has been automatically incremented (from 5 to 11), which allows for automatic replication to the secondary server. We can actually see in the logs above, an entry confirming a successful zone transfer. In fact, nothing needs to be done on the secondary server. 

We can also check the status of the signed zone by using the following command:

$ sudo rndc dnssec -status ns-testing.com
dnssec-policy: custom
current time:  Fri Jun 21 11:40:17 2024

key: 11836 (ECDSAP256SHA256), ZSK
  published:      yes - since Fri Jun 21 11:37:55 2024
  zone signing:   yes - since Fri Jun 21 11:37:55 2024

  Next rollover scheduled on Sun Aug 18 09:27:55 2024
  - goal:           omnipresent
  - dnskey:         rumoured
  - zone rrsig:     rumoured

key: 54249 (ECDSAP256SHA256), KSK
  published:      yes - since Fri Jun 21 11:37:55 2024
  key signing:    yes - since Fri Jun 21 11:37:55 2024

  Next rollover scheduled on Thu Jun 19 09:27:55 2025
  - goal:           omnipresent
  - dnskey:         rumoured
  - ds:             hidden
  - key rrsig:      rumoured

When you run this command just after signing the zone, the key states should be set to “rumoured”. This means that the keys are fresh, and not all resolvers on the Internet may have heard about them. Eventually, they should change their state to “omnipresent”, which means that they are now used by all resolvers.

Let’s see what other changes have been made after signing the zone. First of all, a signed version of the zone file (.signed) has been created, accompanied by a transient file used by BIND (.jbk) and a journal file (.jnl):

$ ls -al /var/lib/bind/
total 28
drwxrwxr-x  2 root bind 4096 Jun 21 11:37 .
drwxr-xr-x 41 root root 4096 May 20 20:19 ..
-rw-r--r--  1 root root  363 Jun 21 10:56 db.ns-testing.com
-rw-r--r--  1 bind bind  512 Jun 21 11:37 db.ns-testing.com.jbk
-rw-r--r--  1 bind bind 1299 Jun 21 11:37 db.ns-testing.com.signed
-rw-r--r--  1 bind bind 5913 Jun 21 11:37 db.ns-testing.com.signed.jnl

In /var/cache/bind/, you can also find KSK and ZSK keys created for the recently signed zone. Public parts are stored in .key files, whereas private keys can be found in the .private files. The last number in the name represents the key ID.

$ ls -al /var/cache/bind/
total 40
drwxrwxr-x  2 root bind 4096 Jun 21 11:38 .
drwxr-xr-x 12 root root 4096 May 20 20:19 ..
-rw-r--r--  1 bind bind  458 Jun 21 11:37 Kns-testing.com.+013+11836.key
-rw-------  1 bind bind  235 Jun 21 11:37 Kns-testing.com.+013+11836.private
-rw-r--r--  1 bind bind  552 Jun 21 11:37 Kns-testing.com.+013+11836.state
-rw-r--r--  1 bind bind  514 Jun 21 11:37 Kns-testing.com.+013+54249.key
-rw-------  1 bind bind  263 Jun 21 11:37 Kns-testing.com.+013+54249.private
-rw-r--r--  1 bind bind  675 Jun 21 11:37 Kns-testing.com.+013+54249.state
-rw-r--r--  1 bind bind  821 Jun 21 11:38 managed-keys.bind
-rw-r--r--  1 bind bind 2528 Jun 21 11:37 managed-keys.bind.jnl

Configuring trust with the parent domain

At this point, both the primary and the secondary servers should be able to provide DNSSEC records. However, the chain of trust is not yet established. In order to do that, we need to add to the parent domain (.com) additional information about the KSK being used by the zone. Let’s generate a DS record that can be added to the parent domain based on the public KSK. We can use the dnssec-dsfromkey tool for that purpose:

$ dnssec-dsfromkey /var/cache/bind/Kns-testing.com.+013+54249.key
ns-testing.com. IN DS 54249 13 2 6BEC2190EA809EF19FC39EE9FEF05FAAB32E6403847F7D295B3B5F6A4C2E6112

Each DS record stores the following information:

  • Key Tag – identifier of the public KSK key (54249 in our case),
  • Algorithm – identifies an algorithm associated with the key (13 – ECDSA Curve P-256 with SHA-256),
  • Digest Type – the algorithm used to construct the digest (2 – SHA-256),
  • Digest – cryptographic hash of the KSK public key (6BEC2190E…6A4C2E6112).

Next, we have to log in to the registrar’s management portal and add the proper information that would allow us to create the DS record:

Fig.6: Creating DS records at the parent domain level.
Figure 6: Creating DS records at the parent domain level.

After waiting for the time needed to propagate the newly created record, our DNSSEC-signed domain should be fully functional.

Verification of the signed zone

We can start the verification by just checking if the signed zone contains the expected DNSSEC records. Both the primary and the secondary servers should respond with DNSKEY records containing both KSK (id = 54249) and ZSK (id = 11836):

$ dig @ns1.ns-testing.com ns-testing.com. DNSKEY +multiline

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @ns1.ns-testing.com ns-testing.com. DNSKEY +multiline
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44726
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: 1d14a390423970ac010000006675c55095b85438b564ac25 (good)
;; QUESTION SECTION:
;ns-testing.com.                IN DNSKEY

;; ANSWER SECTION:
ns-testing.com.         600 IN DNSKEY 256 3 13 (
                                KTUPpPEyYbKzWAaL8+4dxuisGm0gkkghwwizM6zZa1aN
                                gLi3XdTDl3rIQMwU/Qr3W8K/kJlIAkzhK+AwkM+MXA==
                                ) ; ZSK; alg = ECDSAP256SHA256 ; key id = 11836
ns-testing.com.         600 IN DNSKEY 257 3 13 (
                                xStQGvplWIM7roZyXLyKfkMWOSgVs5qcbwCoP0B0tlLW
                                dG+/pai0CPNu6Bs4UJaJtA1Q+YEpglSo/txjOGtzHg==
                                ) ; KSK; alg = ECDSAP256SHA256 ; key id = 54249

;; Query time: 34 msec
;; SERVER: 51.137.105.102#53(51.137.105.102)
;; WHEN: Fri Jun 21 20:24:16 CEST 2024
;; MSG SIZE  rcvd: 231

All resource records should also have associated signatures in the form of an RRSIG record:

$ dig @ns1.ns-testing.com www.ns-testing.com. +dnssec +multiline

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @ns1.ns-testing.com www.ns-testing.com. +dnssec +multiline
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 60047
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; COOKIE: a86157b909914d78010000006675c8c7ea2cd0b4eda65603 (good)
;; QUESTION SECTION:
;www.ns-testing.com.    IN A

;; ANSWER SECTION:
www.ns-testing.com.     3600 IN A 51.137.105.102
www.ns-testing.com.     3600 IN A 98.71.216.161
www.ns-testing.com.     3600 IN RRSIG A 13 3 3600 (
                                20240627202038 20240621103755 11836 ns-testing.com.
                                UgtgrggT4vkoPtk0CVPG3MD8WXk95Vapsc5hXCgFGZ8S
                                9ksVR1e6oyd2M4umtsFUKF++T1tLqyeJ1L8cVztUmg== )

;; Query time: 31 msec
;; SERVER: 51.137.105.102#53(51.137.105.102)
;; WHEN: Fri Jun 21 20:39:03 CEST 2024
;; MSG SIZE  rcvd: 217

We should also be able to query for the DS record installed at the parent domain level:

$ dig ns-testing.com. DS +multiline

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> ns-testing.com. DS +multiline
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24047
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 6fe41bccb325ab71c38f2db66675ca2ed4ade77723f463c6 (good)
;; QUESTION SECTION:
;ns-testing.com.                IN DS

;; ANSWER SECTION:
ns-testing.com.         550 IN DS 54249 13 2 (
                                6BEC2190EA809EF19FC39EE9FEF05FAAB32E6403847F
                                7D295B3B5F6A4C2E6112 )

A good method of checking if all DNSSEC components are correctly configured and the chain of trust is established for the tested domain is to use dedicated online tools. Let’s check out both tools mentioned in the previous article:

Fig.7: DNSSEC Analyze output for the ns-testing.com domain
Figure 7: DNSSEC Analyze output for the ns-testing.com domain
Fig.8: DNSViz output for the ns-testing.com domain
Figure 8: DNSViz output for the ns-testing.com domain

In the first diagram (figure 7), all green “ticks” indicate that the domain passed the test and all components within the DNSSEC chain are correctly configured. In the second diagram (figure 8), we can see a graph showing all the relationships between different DNSSEC records at all levels of the DNS tree. Solid blue arrows between DNSKEY records and other records indicate that RRSIG signatures for those records are valid. Solid blue arrows between DS and DNSKEY records indicate that hashes of particular keys from the DNSKEY records match with information found in their respective DS records. Solid bold blue arrows between rectangles representing particular domains show that delegations are secure from a DNSSEC standpoint. Detailed instructions on how to read the diagram can be found here      link-icon.

Finally, we can just use a resolver that performs DNSSEC validation and check if we can get a proposer response. One such server is Google Resolver at 8.8.8.8:

$ dig @8.8.8.8 www.ns-testing.com.

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @8.8.8.8 www.ns-testing.com.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17595
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;www.ns-testing.com.            IN      A

;; ANSWER SECTION:
www.ns-testing.com.     3600    IN      A       51.137.105.102
www.ns-testing.com.     3600    IN      A       98.71.216.161

;; Query time: 79 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Jun 21 21:26:46 CEST 2024
;; MSG SIZE  rcvd: 79

As we can see in the output above, we are able to receive a valid response, which means that the DNSSEC validation must have been successful. Otherwise, we would get a response with the “SERVFAIL” error code.

DNSSEC validation on a recursive server

If we want to do the standard validation of publicly available domains using the root DNSSEC key, there is actually nothing to be done on the server. BIND 9.18 has DNSSEC validation enabled by default, with the root key as the default trust anchor. The respective statement can be found in the named.conf.options file:

options {
        dnssec-validation auto;
};

There are three available options:

  • auto (the default since BIND 9.13) – Perform validation using a built-in root DNSSEC key (can be found in the predefined /etc/bind/bind.keys file).

  • yes – Perform validation but using a custom trust anchor, defined by the trust-anchors statement stored in the named.conf.options file. Example:

    trust-anchors {
            # This key (20326) was published in the root zone in 2017.
            . static-key 257 3 8 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3
                    +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv
                    ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF
                    0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e
                    oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd
                    RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN
                    R1AkUTV74bU=";
    };
    
  • no – Disable validation.

To learn more about trust anchors and the concept of the DNSSEC chain of trust, please check out our previous article.

Let’s check if the validation really works, first by querying for a domain that has a correct DNSSEC signature:

$ dig @192.168.38.128 www.dnssec-tools.org.

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @192.168.38.128 www.dnssec-tools.org.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34288
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: d7bb995104596d4d0100000066731ebfa26e72bfeed85372 (good)
;; QUESTION SECTION:
;www.dnssec-tools.org.          IN      A

;; ANSWER SECTION:
www.dnssec-tools.org.   300     IN      A       107.220.113.177

;; Query time: 455 msec
;; SERVER: 192.168.38.128#53(192.168.38.128)
;; WHEN: Wed Jun 19 20:09:03 CEST 2024
;; MSG SIZE  rcvd: 93

We get a proper response with an IP address associated with a queried name. Now let’s try doing the same thing, but this time for a domain that has a broken DNSSEC trust chain:

$ dig @192.168.38.128 www.dnssec-failed.org.

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @192.168.38.128 www.dnssec-failed.org.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 62482
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: e48697e957a8592e0100000066731e98bb0eed9e630aae18 (good)
;; QUESTION SECTION:
;www.dnssec-failed.org.         IN      A

;; Query time: 1464 msec
;; SERVER: 192.168.38.128#53(192.168.38.128)
;; WHEN: Wed Jun 19 20:08:24 CEST 2024
;; MSG SIZE  rcvd: 78

Now we get a response with a “SERVFAIL” error code. To confirm that this error is strictly related to the DNSSEC validation, we can send another request with the CD flag set, which disables validation (see our other article on DNSSEC for details):

$ dig @192.168.38.128 www.dnssec-failed.org. +cd

; <<>> DiG 9.11.5-P4-5.1-Debian <<>> @192.168.38.128 www.dnssec-failed.org. +cd
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 14305
;; flags: qr rd ra cd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: fb06837cacb8b32a0100000066732293297b452fea733131 (good)
;; QUESTION SECTION:
;www.dnssec-failed.org.         IN      A

;; ANSWER SECTION:
www.dnssec-failed.org.  6181    IN      A       68.87.109.242
www.dnssec-failed.org.  6181    IN      A       69.252.193.191

;; Query time: 1 msec
;; SERVER: 192.168.38.128#53(192.168.38.128)
;; WHEN: Wed Jun 19 20:25:23 CEST 2024
;; MSG SIZE  rcvd: 110

Key takeaways

In this article, we covered the following topics:

  • Installation of BIND 9.18 on an Ubuntu 22.04.4 LTS server.
  • BIND configuration as an authoritative name server.
  • Zone transfer configuration between the primary and secondary DNS servers.
  • Setting up BIND as a recursive server.
  • DNSSEC configuration of an authoritative name server using a custom DNSSEC policy.
  • Enabling DNSSEC validation on a recursive DNS server.
  • Verification of deployed DNS setup, including DNSSEC configuration.

At the end of this guide, you should now have a solid understanding of how to secure your DNS infrastructure using DNSSEC. You’ve gained the knowledge to install and configure both primary and secondary DNS servers, along with setting up zone transfers, delegating zones, and implementing DNSSEC to ensure data integrity and authenticity. 

By following these step-by-step instructions, you can now confidently manage DNSSEC configuration and avoid some of the common issues that lead to potential service disruptions. This guide has also introduced practical tools available to help you to build a secure DNS infrastructure.

Hryszko  Mariusz

Mariusz Hryszko

Network Engineer

Mariusz Hryszko is a network engineer and author on CodiLime's blog. Check out the author's articles on the blog.Read about author >

Read also

Get your project estimate

For businesses that need support in their software or network engineering projects, please fill in the form and we'll get back to you within one business day.