Introduction

This is an introductory howto to get DNSSEC running with BIND >=9.9 on Debian >=8 (jessie). We assume an "clean", freshly installed bind9 here.

If you're looking for more general information about DNSSEC, you may want to have a look at:
DNSSEC
Domain Name System Security Extensions (DNSSEC) - Wikipedia

Approach used here. We use inline-signing here, as it relieves the administrator of most of the hassle, hazards, and pitfalls of manually maintaining DNSSEC and associated Resource Records (RRs), at least once the initial configuration has been completed.

BIND version 9.9 (or later) is minimal requirement, and we presume a "pure" Debian >=8 (jessie) host system with a "clean" installation of the bind9 Debian package.
See also: Bind9.

In our examples here, for security, we'll generally be showing/using Principle of least privilege - Wikipedia. That is most important/critical here, for any and all private keys. If one wishes to be more permissive, for items other than private keys, one may wish to allow read access in the general case, however in all cases most all the files/data referenced here should be protected from general write access. That generally means group bind will need read access, and in some areas/locations/files, write access. Others should not have write access, and must not have read access to private keys.

By convention, we generally show
root / superuser / UID 0 prompt as a leading "# "
and other ID prompt(s) as a leading "$ ".

Initial setup

Make separate directory for keys and zones, let group bind write in zones. We also use SGID on directory, so by default items created in directory will have group ownership matching that of the directory.

Install bind9 if it's not already installed, e.g.:

# apt-get install bind9

Note that by between BIND 9.11.5 Debian buster 10 and BIND 9.16.48 Debian bullseye 11, preferred terminology had generally shifted from master/masters to primary/primaries, and from slave/slaves to secondary/secondaries. So the older terminology may be seen in some of the older versions, and in some cases may be the only supported option names. Also, even in some of the newer and still possibly current, not all such name references in in all configurations have necessarily changed, at least yet.

# umask 077
# mkdir /etc/bind/primary /var/cache/bind/keys
# chgrp bind /etc/bind/primary /var/cache/bind/keys
# chmod ug=rwx,o=,g+s /etc/bind/primary /var/cache/bind/keys

Serial number (& scheme) for primary zone file.

For the serial, we are showing the RFC recommended format: YYYYMMDDnn While that has the advantage of the more human readable form, with inline-signing - which we're using here, it will typically rather quickly fall out-of-sync with its intended alignment to actual corresponding YYYY-MM-DD date, and may lead to confusions and incorrect presumptions. Alternatively, one may want to instead use seconds since the epoch - "Unix time" (unixtime). Notably as inline-signing can directly support unixtime, whereas with YYYYMMDDnn, the only method inline-signing can use with that is increment, which will simply increment the serial, regardless of when it is done. If one wishes to use unixtime, the GNU date utility can conveniently provide that:

$ date +%s
1717846872

And likewise convert back if one wants to see the more human time, e.g.:

$ date -d @1717846872
Sat Jun  8 11:41:12 GMT 2024
$ date --iso-8601=seconds -d @1717846872
2024-06-08T11:41:12+00:00

One can also use GNU date to get the YYYYMMDD portion of YYYYMMDDnn format:

$ date +'%Y%m%d'
20240608

Note one also can't arbitrarily jump serial numbers around - that can cause significant to major problems. But if we're starting new, we're relatively free to pick - so long as we do a valid number within range (both unixtime, and YYYYMMDDnn will be within range).

Now create primary zone file, we show example.com - use your actual domain, and we show YYYYMMDDnn serial format - use your actual data/scheme. Likewise for the A record, we show 127.0.0.1, for the BIND9 DNS nameserver to be useful and functional beyond the local host, you'll need to use the applicable IP address(es) for the A and/or AAAA record(s) corresponding to the NS record. Also, hostmaster.example.com. should be or be replaced with valid email address - replacing @ with ., and if there are any .'s before that first @ in the email address, replace those with: \. And end with: . E.g.:

firstname.lastname@example.com

would become:

firstname\.lastname.example.com.

So, in /etc/bind/primary we create our file example.com:

$TTL    3600
@       IN      SOA     (
                        ns1.example.com.        ; MNAME
                        hostmaster.example.com. ; RNAME
                        2024060800              ; SERIAL
                        2H                      ; REFRESH
                        1H                      ; RETRY
                        2W                      ; EXPIRY
                        1H                      ; MINIMUM Negative Cache TTL
                        )
        IN      NS      ns1.example.com.
ns1     IN      A       127.0.0.1

File permissions and ownership should be 640 root:bind:

# ls -ld example.com
-rw-r----- 1 root bind 4207 Apr 21 10:01 example.com

so set accordingly, e.g.:

# chmod u=rw,g=r,o= example.com

If you implement dynamic DNS, you'll want to then change that to bind:bind, e.g.:

# chown bind example.com && chmod u=rw,g=r,o= example.com

And in /etc/bind, add the following section to named.conf.local:

zone "example.com" {
        type primary;
        file "/etc/bind/primary/example.com";
        allow-transfer { 127.0.0.1; };
};

Check that the service is running:

# rndc status

or, e.g.:

# systemctl status bind9.service

If it's already running, simply reload it:

# rndc reload

or, e.g.:

# systemctl reload bind9.service

If it's not running, enable and start the service, e.g.:

# systemctl enable bind9.service
# systemctl start bind9.service

You may need to install dnsutils for utilities such as dig and delv:

# apt-get install dnsutils

Check whether querying the zone works:

$ dig @127.0.0.1 +norecurse example.com. NS

The signing part

Keys ...

At least as of 2024-06-04:
domain key algorithm bits
.      KSK  8        2048
.      ZSK  8        2048
org.   KSK  8        2048
org.   ZSK  8        1024
net.   KSK 13         512
net.   ZSK 13         512
com.   KSK 13         512
com.   ZSK 13         512
...
algorithms:
8 RSASHA256
13 ECDSAP256SHA256

Domain Name System Security (DNSSEC) Algorithm Numbers
Note that when setting up keys, notably algorithm and number of bits, one would typically want to use same algorithm and number of bits for same key type(s) as is present on up the parent chain. Notably as for same key type and algorithm, more bits would add overhead (CPU and data transmission size), without significantly increasing strength, and decreasing bits would weaken chain, so optimal is generally to match. Note also that generally one can't do apples-to-apples type of comparison across different algorithms, as same bits on different algorithms may be of differing strengths, so for any given bit size, algorithms may vary in their cryptographic strength and overhead (CPU load and/or transmission size).

In our example here, with example.com, we pick values that (at least at the time of this writing) correspond with com (and .):

Note that dnssec-policy is available in ISC BIND >= 9.15.6 (and Debian bullseye 11), but not present in ISC BIND <= 9.15.5 (Debian buster 10). Where dnssec-policy is available, it is preferred and should generally be used rather than auto-dnssec, which has become deprecated and will be going away. By using dnssec-policy, named can create our keys for us, so we can skip the two following sets of steps where we manually create our initial keys and set their ownerships and permissions. However, if one doesn't have dnssec-policy available, and thus uses auto-dnssec, then one will want to do these manual steps to create initial keys and set the ownerships and permissions on the files.

Create our initial keys:

# cd /var/cache/bind/keys
# dnssec-keygen -a ECDSAP256SHA256 -b 512 -f KSK example.com
# dnssec-keygen -a ECDSAP256SHA256 -b 512 example.com

Set permissions so group bind can read the keys:

# chgrp bind Kexample.com.+*
# chmod g=r,o= Kexample.com.+*

And back to /etc/bind/ directory:

# cd /etc/bind

Add to the zone "example.com" section in the file named.conf.local:

        inline-signing yes;
        dnssec-policy default;
        serial-update-method increment;

One may also create and use a custom dnssec-policy if desired.

CAREFULLY NOTE: if zone already has DNSSEC enabled (DS record(s) in parent zone), care must be taken to avoid breaking DNSSEC when adding (or changing) dnssec-policy: Most notably when one switches to or puts dnssec-policy in place or changes it and activates that, BIND switches to using what that dnssec-policy specifies, and if, e.g. that conflicts with existing keys on the zone, they'll be instantly dropped (or rotation started), so that can then instantly break existing DNSSEC. So, to change existing zone that already has DNSSEC enabled (DS record(s) in parent zone) without breaking DNSSEC and while adding/updating dnssec-policy, use at least an initial dnssec-policy that doesn't conflict with existing keys that are in use and that one doesn't want to immediately drop. So, that includes not only key type, algorithm and bits, but also lifetime of applicable keys. It may be prudent to test first on test zone, to avoid potential unpleasant surprises.

One can also use:

$ named -C

to inspect what the compiled in defaults are for dnssec-policy default;
If one doesn't have dnssec-policy policy, then instead use the older:

        auto-dnssec maintain;

in its place, and in that case one will need to create one's key files as noted further above.

If one alternatively uses unixtime for the zone serial number (see also further above about serial numbers),
then instead use this for serial-update-method:

        serial-update-method unixtime;

Add:

     key-directory "/var/cache/bind/keys";

to the options section in named.conf.options

At this point, if you're using AppArmor (you generally are by default), if you haven't already done so, will want to adjust its configuration slightly, as follows:
In file:
/etc/apparmor.d/local/usr.sbin.named
add the line:

  /etc/bind/primary/* lrw,

Then reload the AppArmor profile for named:

# apparmor_parser -r /etc/apparmor.d/usr.sbin.named

reload the configuration:

# rndc reload

or e.g. for systemd:

# systemctl reload bind9.service

If you used dnssec-policy, you can skip these steps, otherwise execute:

# rndc loadkeys example.com
# rndc signing -nsec3param 1 0 10 auto example.com

to let bind sign the zone.

Verify that the zone works and is properly signed and ready for DS delegation by executing (note that the example script bit requires bash (uses: <(...)) - one could alternatively do that via temporary file, or explicitly creating temporary named pipe and removing it after):

# (d=example.com; k=$(printf '%05d' "$(dig @127.0.0.1 +norecurse "$d". DNSKEY | dnssec-dsfromkey -f - "$d" | awk '{print $4;}' | sort -u)"); delv @127.0.0.1 -a <(sed -e '/^;/d;s/[ \t]\{1,\}/ /g;s/ [0-9]\{1,\} IN DNSKEY / IN DNSKEY /;s/ IN DNSKEY / /;s/^[^ ]* [^ ]* [^ ]* [^ ]* /&"/;:s;/"[^ ]*$/b t;s/\("[^ ]*\) /\1/;b s;:t;s/$/";/;H;$!d;x;s/^\n//;s/.*/trusted-keys {\n    &\n};/' /var/cache/bind/keys/K"$d".+013+"$k".key) +root="$d" "$d". SOA +multiline)

You should see a first output line of:

; fully validated

followed by SOA record data and then signature related data.

If you get the errors "delv: No trusted keys were loaded" and/or that trusted-keys is deprecated you can verify the zone with:

# (d=example.com; k=$(dig @127.0.0.1 +norecurse "$d". DNSKEY | dnssec-dsfromkey -f - "$d" | awk '{print $4;}' | sort -u); delv @127.0.0.1 -a <(sed -e '/^;/d;s/[ \t]\{1,\}/ /g;s/ [0-9]\{1,\} IN DNSKEY / IN DNSKEY /;s/ IN DNSKEY / /;s/^[^ ]* [^ ]* [^ ]* [^ ]* /&"/;:s;/"[^ ]*$/b t;s/\("[^ ]*\) /\1/;b s;:t;s/$/";/;H;$!d;x;s/^\n//;s/.*/trust-anchors {\n    &\n};/;s/\n\(\s\+\S\+\)/\n\1 static-key/g' /var/cache/bind/keys/K"$d".+013+"$k".key) +root="$d" "$d". SOA +multiline)

One can also do likewise pulling the DNSKEY data from DNS - but be sure to do so only over sufficiently secure trusted network communications channel (e.g. 127.0.0.1):

$ (d=example.com; delv @127.0.0.1 -a <(dig @127.0.0.1 +noall +answer "$d". DNSKEY | sed -e '/^;/d;s/[ \t]\{1,\}/ /g;s/ [0-9]\{1,\} IN DNSKEY / IN DNSKEY /;s/ IN DNSKEY / /;s/^[^ ]* [^ ]* [^ ]* [^ ]* /&"/;:s;/"[^ ]*$/b t;s/\("[^ ]*\) /\1/;b s;:t;s/$/";/;H;$!d;x;s/^\n//;s/.*/trusted-keys {\n    &\n};/') +root="$d" "$d". SOA +multiline)

Once the above has been validated, before proceeding further, be sure to securely SAVE BACKUP COPIES of the keys! The keys are in /var/cache/bind/keys/ in our example configuration. Once the DS record(s) is(/are) set up in the delegating parent domain/zone, if keys are lost, DNS(SEC) will be failed for a substantial period of time, and one cannot be assured of fully and immediately rectifying such situation without those same keys - any resolvers, etc. that understand and use DNSSEC may continue to reject DNS data from the domain until the DNSSEC issue is resolved - including any relevant caching and timeouts on data and keys seen, including keys seen earlier.

It is CRUCIAL that the above validation checks be working properly before proceeding further, and one should likewise have the keys securely backed up.

So, here's the part where we go fully "live" with DNSSEC. Similar to NS records for nameserver delegation, there are DS records for delegation of digital signatures/signing.

So, one needs to have the relevant DS record(s) added to the delegating parent/upstream zone. Again, be sure the validation steps noted further above have first been successfully completed before proceeding. If you proceed with the next steps without first having the above working properly, you'll significantly break your DNS, and it may take a substantial period of time to fully rectify that. (Essentially if DS data is set up in delegating parent/upstream zone, and you're not properly functional and ready for that yet, essentially you're saying that you're using DNSSEC and not to trust the data if it's not properly set up and signed with the key data provided ... and then you'd not have proper key data set up on your zone, so any DNS that honored DNSSEC would reject your DNS data, and that may persist some fair while due to key lifetimes, DNS TTLs, expiration, etc.)

So, next we obtain the needed DS data from our signed zone:

$ (d=example.com; dig @127.0.0.1 +norecurse "$d". DNSKEY | dnssec-dsfromkey -f - "$d")

That gets one the DS records that need to be added to upstream/parent delegating zone to make the DNSSEC effective from the delegating authority(/ies). The precise procedure on adding those to the delegating upstream/parent will depend what exactly is there. It might be as simple as directly adding the relevant data to the DNS nameserver. For some registrars, it may be matter of doing copy/paste of the relevant data into some specific web form. Some registrars will fetch the DNSKEY data from your zone, determine the DS data from that, display it and ask for confirmation to add it (in which case, do be sure to check that it matches properly as expected). Others may have you input key data from the DNSKEY record(s). Some may possibly be able to utilize CDS and/or CDNSKEY records to update DS record(s), but note that CDS and/or CDNSKEY records can't be used to update DS without DS an associated signing already being in place for the CDS/CDNSKEY records. So, precise procedures will vary by nameserver(s) and/or registrar, etc., but by whatever the appropriate means are, one gets the relevant DS record(s) into the delegating authority parent zone. So, DNSKEY in the child zone (dnssec-policy or auto-dnssec should take care of that for you), and DS goes only in the parent, not in the child (so in that regard it's not quite so analogous to NS records). Once DS is in the parent zone, DNSSEC becomes live (possibly subject to relevant TTLs and SOA MINIMUM and respectively relevant caching and "negative" caching).

Not covered here - alternative trust anchors - how to have DNSSEC authority for nameservers, resolvers, etc. come from alternative source (e.g. local) directly (or indirectly), rather than directly or indirectly from the Internet root (.) DNS nameservers.

Documentation/References

DNSSEC
DNSSEC and BIND 9
DNSSEC Resolver Test
resolver DS and signing algorithm combination tester
BIND DNSSEC Guide
DNSViz - A DNS visualization tool (excellent visual DNS/DNSSEC analysis/troubleshooting)
Bind9 Bind9#BIND_9_Documentation