--- title: "How to Set Up an Authoritative DNS Server with BIND9" description: "Set up BIND9 on Ubuntu 24.04 as an authoritative primary and secondary DNS server for your domain, with zone transfers, DNSSEC signing and dig verification." category: "Use Cases" language: en source: https://cubepath.com/docs/use-cases/dns-server-with-bind9 updated: 2026-09-25 --- # How to Set Up an Authoritative DNS Server with BIND9 BIND9 is the reference DNS server from the Internet Systems Consortium (ISC) and can act as an authoritative name server, a recursive resolver, or both. In this tutorial you will run your own authoritative nameservers for a domain on Ubuntu 24.04: a primary server (`ns1`) that holds the zone file and a secondary server (`ns2`) that receives copies through zone transfers. You will also sign the zone with DNSSEC and verify every step with `dig`. Recursion stays disabled on both servers. An authoritative server that also answers recursive queries for the whole Internet is an open resolver and gets abused for DNS amplification attacks. ## Prerequisites To follow this tutorial you need: - Two servers running Ubuntu 24.04 LTS, each with a public IPv4 address, for example two CubePath VPS in different locations. This guide uses `203.0.113.10` for `ns1` and `203.0.113.20` for `ns2`. - A non-root user with `sudo` privileges on both servers. - A registered domain. This guide uses **`example.com`**; replace it with **`your_domain`** everywhere. - Access to your registrar's control panel, where you will register the nameserver hostnames (glue records) and, optionally, the DNSSEC DS record. Most registrars require at least two nameservers on different IP addresses, which is why this guide uses two servers. Commands are labelled with the server they run on. ## Step 1 - Installing BIND9 on both servers Install BIND9 and its command-line tools on **ns1 and ns2**. The `bind9-dnsutils` package provides `dig`, which you will use for testing. ```bash sudo apt update sudo apt install bind9 bind9-utils bind9-dnsutils ``` Ubuntu starts the service right away. It is called `named` (the `bind9` name is an alias): ```bash sudo systemctl status named --no-pager ``` ```output ● named.service - BIND Domain Name Server Loaded: loaded (/usr/lib/systemd/system/named.service; enabled; preset: enabled) Active: active (running) ``` Check the installed version: ```bash named -v ``` ```output BIND 9.18.x-0ubuntu0.24.04.x-Ubuntu (Extended Support Version) ``` Ubuntu 24.04 ships BIND 9.18, which supports the `dnssec-policy` and `primaries` syntax used below. ## Step 2 - Configuring global options The main configuration is split across several files in `/etc/bind/`. Global settings live in `named.conf.options`. On **both servers**, open it: ```bash sudo nano /etc/bind/named.conf.options ``` Replace its contents with the following block: ```text options { directory "/var/cache/bind"; // Authoritative only: never resolve names for other clients recursion no; allow-query { any; }; // Deny zone transfers by default, allow them per zone allow-transfer { none; }; listen-on { any; }; listen-on-v6 { any; }; dnssec-validation auto; // Do not reveal the exact BIND version version "not disclosed"; }; ``` The important settings are: - `recursion no;` turns the server into a pure authoritative server, so it only answers for zones it hosts. - `allow-transfer { none; };` blocks AXFR zone transfers globally. You will open them only from `ns1` to `ns2`. - `version` hides the version string that `dig version.bind CH TXT` would otherwise return. Check the syntax. No output means the configuration is valid: ```bash sudo named-checkconf ``` ## Step 3 - Creating the zone file on the primary The zone file contains the DNS records for your domain. Store it in `/var/lib/bind/`: BIND runs as the `bind` user, and that directory is writable by it and allowed by Ubuntu's AppArmor profile, which DNSSEC inline signing needs later because BIND writes signed copies next to the zone file. On **ns1**, create the file: ```bash sudo nano /var/lib/bind/db.example.com ``` Add the following zone, adjusting names and addresses: ```text $TTL 3600 @ IN SOA ns1.example.com. hostmaster.example.com. ( 2026092501 ; serial (YYYYMMDDNN) 3600 ; refresh 900 ; retry 1209600 ; expire 300 ) ; negative caching TTL ; Nameservers @ IN NS ns1.example.com. @ IN NS ns2.example.com. ; Glue for the nameservers themselves ns1 IN A 203.0.113.10 ns2 IN A 203.0.113.20 ; Website and mail @ IN A 203.0.113.50 www IN CNAME example.com. @ IN MX 10 mail.example.com. mail IN A 203.0.113.60 @ IN TXT "v=spf1 mx -all" ``` A few rules to remember when editing zone files: - Every fully qualified name ends with a dot (`ns1.example.com.`). Without the dot, BIND appends the zone name. - The SOA email address uses a dot instead of `@`: `hostmaster.example.com.` means `hostmaster@example.com`. - The **serial** must increase every time you change the file, or the secondary will not pick up the change. The `YYYYMMDDNN` format makes this easy. Give the file to the `bind` user and validate it: ```bash sudo chown bind:bind /var/lib/bind/db.example.com sudo named-checkzone example.com /var/lib/bind/db.example.com ``` ```output zone example.com/IN: loaded serial 2026092501 OK ``` ## Step 4 - Declaring the zone on the primary Zones are declared in `/etc/bind/named.conf.local`. On **ns1**, open it: ```bash sudo nano /etc/bind/named.conf.local ``` Add the zone definition: ```text zone "example.com" { type primary; file "/var/lib/bind/db.example.com"; // Only ns2 may transfer the zone, and it is notified on every change allow-transfer { 203.0.113.20; }; also-notify { 203.0.113.20; }; notify yes; }; ``` Validate the configuration and the zone together, then reload BIND: ```bash sudo named-checkconf -z sudo systemctl reload named ``` ```output zone example.com/IN: loaded serial 2026092501 ``` ## Step 5 - Opening the firewall DNS uses UDP port 53 for most queries and TCP port 53 for large responses and zone transfers, so both protocols must be open. On **both servers**: ```bash sudo ufw allow 53 sudo ufw status ``` ```output To Action From -- ------ ---- OpenSSH ALLOW Anywhere 53 ALLOW Anywhere ``` If UFW is not enabled yet, run `sudo ufw allow OpenSSH` before `sudo ufw enable` so you do not lock yourself out. Now query the primary from any other machine: ```bash dig @203.0.113.10 example.com A +norecurse ``` ```output ;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; ANSWER SECTION: example.com. 3600 IN A 203.0.113.50 ``` The `aa` flag (authoritative answer) confirms that `ns1` is serving the zone itself rather than from a cache. ## Step 6 - Configuring the secondary server The secondary does not need a zone file of its own: it downloads the zone from the primary and keeps it up to date. On **ns2**, open `named.conf.local`: ```bash sudo nano /etc/bind/named.conf.local ``` Add the secondary zone: ```text zone "example.com" { type secondary; primaries { 203.0.113.10; }; file "/var/lib/bind/db.example.com"; }; ``` Check the configuration and reload: ```bash sudo named-checkconf sudo systemctl reload named ``` Look at the log to confirm that the transfer succeeded: ```bash sudo journalctl -u named --since "5 minutes ago" | grep -i transfer ``` ```output named[1234]: transfer of 'example.com/IN' from 203.0.113.10#53: Transfer status: success named[1234]: transfer of 'example.com/IN' from 203.0.113.10#53: Transfer completed: 1 messages, 11 records, ... ``` Query `ns2` directly and compare the SOA serial with the primary: ```bash dig @203.0.113.20 example.com SOA +short ``` ```output ns1.example.com. hostmaster.example.com. 2026092501 3600 900 1209600 300 ``` Finally, confirm that zone transfers are refused to everyone else. From a machine that is not `ns2`: ```bash dig @203.0.113.10 example.com AXFR ``` ```output ; Transfer failed. ``` ## Step 7 - Pointing the domain at your nameservers Your servers are ready, but resolvers on the Internet will not use them until the registrar delegates the domain to them. In your registrar's control panel: 1. Register the **host records** (also called glue records or child nameservers) `ns1.example.com` → `203.0.113.10` and `ns2.example.com` → `203.0.113.20`. They are required because the nameservers live inside the domain they serve. 2. Change the domain's nameservers to `ns1.example.com` and `ns2.example.com`. Delegation changes can take from a few minutes to 48 hours to propagate, depending on the TTL of the previous NS records at the TLD. Check the delegation from the top with `+trace`: ```bash dig example.com A +trace ``` The last section of the output should show the answer coming from `ns1.example.com` or `ns2.example.com`. ## Step 8 - Signing the zone with DNSSEC DNSSEC adds cryptographic signatures to your records so resolvers can detect forged answers. BIND 9.18 can generate keys, sign the zone and roll keys automatically with `dnssec-policy`. On **ns1**, edit the zone in `/etc/bind/named.conf.local` and add two lines: ```bash sudo nano /etc/bind/named.conf.local ``` ```text zone "example.com" { type primary; file "/var/lib/bind/db.example.com"; allow-transfer { 203.0.113.20; }; also-notify { 203.0.113.20; }; notify yes; dnssec-policy default; inline-signing yes; }; ``` The `default` policy uses a single ECDSA P-256 key (algorithm 13). With `inline-signing yes`, you keep editing the unsigned `db.example.com` file as usual, and BIND maintains a signed copy (`db.example.com.signed`) that it serves and transfers to `ns2`. The secondary needs no changes. Reload and check the signing status: ```bash sudo named-checkconf sudo systemctl reload named sudo rndc dnssec -status example.com ``` ```output dnssec-policy: default current time: Fri Sep 25 10:00:00 2026 key: 12345 (ECDSAP256SHA256), CSK published: yes - since Fri Sep 25 09:59:00 2026 key signing: yes - since Fri Sep 25 09:59:00 2026 zone signing: yes - since Fri Sep 25 09:59:00 2026 ... ``` Confirm that answers now carry signatures: ```bash dig @203.0.113.10 example.com A +dnssec +norecurse ``` The answer section should contain an `RRSIG` record next to the `A` record. To complete the chain of trust, publish a **DS record** at your registrar. Generate it from the zone's public key: ```bash dig @127.0.0.1 example.com DNSKEY +norecurse | dnssec-dsfromkey -f - example.com ``` ```output example.com. IN DS 12345 13 2 3F5A...C1E9 ``` Enter the key tag (`12345`), algorithm (`13`), digest type (`2`) and digest into the registrar's DNSSEC form. Once the DS record is live, validating resolvers such as `1.1.1.1` will return the `ad` (authenticated data) flag: ```bash dig @1.1.1.1 example.com A +dnssec ``` ```output ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ``` > Warning: Do not remove `dnssec-policy` or delete the keys in `/var/cache/bind` while a DS record is published. Resolvers would treat your zone as forged and return SERVFAIL. Remove the DS record at the registrar first and wait for its TTL to expire. ## Updating records Every change to the zone follows the same routine on **ns1**: 1. Edit `/var/lib/bind/db.example.com`. 2. Increase the serial number. 3. Validate and reload only that zone: ```bash sudo named-checkzone example.com /var/lib/bind/db.example.com sudo rndc reload example.com ``` The primary sends a NOTIFY to `ns2`, which transfers the new version within seconds. Compare the serials to confirm: ```bash dig @203.0.113.10 example.com SOA +short dig @203.0.113.20 example.com SOA +short ``` ## Troubleshooting **The zone does not load after a reload.** Run `sudo named-checkconf -z` and read the error, then check `sudo journalctl -u named -n 50`. A common cause is `permission denied` when the zone file is not owned by `bind` or lives outside `/var/lib/bind`, which AppArmor blocks. **The secondary keeps the old serial.** You probably forgot to increase the serial on the primary. Also check that TCP port 53 is open on `ns1` and that `allow-transfer` lists the exact IP address `ns2` connects from. On `ns2`, `sudo rndc retransfer example.com` forces a new transfer. **`dig` returns `REFUSED`.** The server is not authoritative for the name you asked about, or the zone failed to load. Since recursion is disabled, queries for other domains are refused by design. **Resolvers return `SERVFAIL` after enabling DNSSEC.** The DS record at the registrar does not match the key BIND is using. Compare the output of `dnssec-dsfromkey` with the DS record shown by `dig example.com DS +short` and correct it at the registrar. ## Conclusion You now run two authoritative BIND9 nameservers for your domain, with restricted zone transfers, automatic NOTIFY to the secondary and a DNSSEC-signed zone. From here you can add a reverse zone if your provider delegates PTR records for your IP range to you, set up TSIG keys to authenticate zone transfers instead of relying on IP addresses, or add a third nameserver in another region for more resilience.