Andrew Mercer
on this page

Authoritative + caching DNS server for andrewmercer.local, plus the domain-to-subdomain migration to dev.andrewmercer.com.

Install

sudo apt update
sudo apt install -y bind9
sudo systemctl enable --now bind9.service

Configuration

Debian/Ubuntu splits named.conf into includes so options, logging, and zone definitions can be edited independently.

/etc/bind/named.conf

include "/etc/bind/named.conf.logging";
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";

/etc/bind/named.conf.options

acl "trusted" {
    10.10.13.101;
    10.10.13.102;
    10.10.13.103;
    10.10.13.104;
};

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

    recursion yes;
    allow-recursion { trusted; };
    listen-on { 10.10.13.5; };
    listen-on-v6 { any; };
    allow-transfer { trusted; };

    dnssec-validation auto;
    auth-nxdomain no;    # conform to RFC1035
};

Notes on this block: - trusted covers the four LAN hosts allowed to use this server recursively and to pull zone transfers — anything not in that list is refused both, which is the correct default for a homelab server not meant to be internet-facing. - listen-on binds only the specific LAN address (10.10.13.5) rather than every interface, which matters if this host has more than one NIC (e.g., a separate WAN-facing interface) — no need to expose the resolver there. - auth-nxdomain no is the RFC 1035–conformant setting and should stay no unless a specific downstream tool depends on the older non-conformant behavior.

/etc/bind/named.conf.logging

The original version logged every category at debug — useful while getting BIND running the first time, but far too verbose (and disk-hungry) to leave on indefinitely, since queries/client/network/dispatch log every single lookup. Two versions below: the debug config for active troubleshooting, and a leaner steady-state config to switch to once things are working.

While troubleshooting:

logging {
    channel "debug" {
        file "/var/log/named/named.log" versions 2 size 50m;
        print-time yes;
        print-category yes;
    };

    category "default"     { "debug"; };
    category "general"     { "debug"; };
    category "database"    { "debug"; };
    category "security"    { "debug"; };
    category "config"      { "debug"; };
    category "resolver"    { "debug"; };
    category "xfer-in"     { "debug"; };
    category "xfer-out"    { "debug"; };
    category "notify"      { "debug"; };
    category "client"      { "debug"; };
    category "unmatched"   { "debug"; };
    category "network"     { "debug"; };
    category "update"      { "debug"; };
    category "queries"     { "debug"; };
    category "dispatch"    { "debug"; };
    category "dnssec"      { "null"; };
    category "lame-servers"{ "null"; };
};

Steady-state (switch to this once the server is confirmed working):

logging {
    channel "default_log" {
        file "/var/log/named/named.log" versions 5 size 20m;
        severity notice;
        print-time yes;
        print-category yes;
    };
    channel "security_log" {
        file "/var/log/named/security.log" versions 5 size 20m;
        severity info;
        print-time yes;
    };

    category default   { default_log; };
    category security  { security_log; };
    category config    { default_log; };
    category update    { default_log; };
};

Either way, the log directory must be owned by the bind user or named fails at startup with a permission error (see Troubleshooting below):

sudo mkdir -p /var/log/named
sudo chown bind:bind /var/log/named

/etc/bind/named.conf.local

// Consider adding the 1918 zones here, if they are not used in your organization
include "/etc/bind/zones.rfc1918";

zone "andrewmercer.local" {
    type master;
    file "/etc/bind/db.andrewmercer.local";
    allow-update { trusted; };
};

zone "13.10.10.in-addr.arpa" {
    type master;
    file "/etc/bind/db.13.10.10";
    allow-update { trusted; };
};

/etc/bind/db.andrewmercer.local (forward zone)

$TTL 604800
@   IN  SOA  ns1.andrewmercer.local. admin.ns1.andrewmercer.local. (
                2               ; Serial
                604800          ; Refresh
                86400           ; Retry
                2419200         ; Expire
                604800 )        ; Negative cache TTL

; NS Records
    IN  NS   ns1.andrewmercer.local.
    IN  NS   ns2.andrewmercer.local.
    IN  NS   ns3.andrewmercer.local.
    IN  NS   ns4.andrewmercer.local.

; A Records
ns1.andrewmercer.local.         IN  A     10.10.13.5
ns2.andrewmercer.local.         IN  A     10.10.13.6
ns3.andrewmercer.local.         IN  A     10.10.13.7
ns4.andrewmercer.local.         IN  A     10.10.13.8

; CNAME Records
docker-repo.andrewmercer.local. IN  CNAME andrewmercer1.andrewmercer.local.

Note: docker-repo points via CNAME at andrewmercer1.andrewmercer.local, which doesn't otherwise appear in this zone file — confirm that name is defined elsewhere (another zone, or a missing A record here) before relying on it, since a CNAME to a non-existent target resolves to nothing.

/etc/bind/db.13.10.10 (reverse zone for 10.10.13.0/24)

$TTL 604800
@   IN  SOA  andrewmercer.local. admin.andrewmercer.local. (
                2               ; Serial
                604800          ; Refresh
                86400           ; Retry
                2419200         ; Expire
                604800 )        ; Negative cache TTL

    IN  NS   ns1.andrewmercer.local.
    IN  NS   ns2.andrewmercer.local.
    IN  NS   ns3.andrewmercer.local.
    IN  NS   ns4.andrewmercer.local.

5.13.10.10.in-addr.arpa.   IN  PTR  ns1.andrewmercer.local.
6.13.10.10.in-addr.arpa.   IN  PTR  ns2.andrewmercer.local.
7.13.10.10.in-addr.arpa.   IN  PTR  ns3.andrewmercer.local.
8.13.10.10.in-addr.arpa.   IN  PTR  ns4.andrewmercer.local.

Note: the forward zone's SOA MNAME is ns1.andrewmercer.local. but the reverse zone's SOA MNAME is andrewmercer.local. (not ns1...) — worth making these consistent, since the SOA's primary-server field is conventionally the actual name server hostname, not the bare zone name.

Reminder for any future edit to either file: bump the Serial before reloading, or secondaries/caches won't notice the change.

Apply and check:

sudo named-checkconf
sudo named-checkzone andrewmercer.local /etc/bind/db.andrewmercer.local
sudo named-checkzone 13.10.10.in-addr.arpa /etc/bind/db.13.10.10
sudo rndc reload

Migrating andrewmercer.local → dev.andrewmercer.com

Converting the internal zone into a proper subdomain of a real registered domain. This is a rename of the zone plus new delegation, not a special "conversion" record type — see the general BIND guide, §10, for the underlying steps this follows.

/etc/bind/db.dev.andrewmercer.com

$TTL 604800
@   IN  SOA  ns1.dev.andrewmercer.com. admin.ns1.dev.andrewmercer.com. (
                2               ; Serial
                604800          ; Refresh
                86400           ; Retry
                2419200         ; Expire
                604800 )        ; Negative cache TTL

; NS Records
    IN  NS   ns1.dev.andrewmercer.com.
    IN  NS   ns2.dev.andrewmercer.com.
    IN  NS   ns3.dev.andrewmercer.com.
    IN  NS   ns4.dev.andrewmercer.com.

; A Records
ns1.dev.andrewmercer.com.           IN  A     10.10.13.5
ns2.dev.andrewmercer.com.           IN  A     10.10.13.6
ns3.dev.andrewmercer.com.           IN  A     10.10.13.7
ns4.dev.andrewmercer.com.           IN  A     10.10.13.8

; CNAME Records
docker-repo.dev.andrewmercer.com.   IN  CNAME andrewmercer1.dev.andrewmercer.com.

(Fixed from the original draft: the SOA's admin/RNAME field was admin.ns1.dev.andrewmercer.local — mixing the new .com names with the old .local domain. It should reference the new namespace consistently, admin.ns1.dev.andrewmercer.com., as above.)

/etc/bind/db.13.10.10 (reverse zone, updated to match)

$TTL 604800
@   IN  SOA  dev.andrewmercer.com. admin.dev.andrewmercer.com. (
                2               ; Serial
                604800          ; Refresh
                86400           ; Retry
                2419200         ; Expire
                604800 )        ; Negative cache TTL

    IN  NS   ns1.dev.andrewmercer.com.
    IN  NS   ns2.dev.andrewmercer.com.
    IN  NS   ns3.dev.andrewmercer.com.
    IN  NS   ns4.dev.andrewmercer.com.

5.13.10.10.in-addr.arpa.   IN  PTR  ns1.dev.andrewmercer.com.
6.13.10.10.in-addr.arpa.   IN  PTR  ns2.dev.andrewmercer.com.
7.13.10.10.in-addr.arpa.   IN  PTR  ns3.dev.andrewmercer.com.
8.13.10.10.in-addr.arpa.   IN  PTR  ns4.dev.andrewmercer.com.

/etc/bind/named.conf.local

include "/etc/bind/zones.rfc1918";

zone "dev.andrewmercer.com" {
    type master;
    file "/etc/bind/db.dev.andrewmercer.com";
    allow-update { trusted; };
};

zone "13.10.10.in-addr.arpa" {
    type master;
    file "/etc/bind/db.13.10.10";
    allow-update { trusted; };
};

Cutover checklist

  1. Write the new zone files above; bump the Serial on both.
  2. named-checkzone dev.andrewmercer.com /etc/bind/db.dev.andrewmercer.com and the same for the reverse zone.
  3. Swap the zone blocks in named.conf.local — remove the old andrewmercer.local block once the new one is confirmed loading (rndc reload, then dig @10.10.13.5 dev.andrewmercer.com NS).
  4. If andrewmercer.com is a real registered domain with its own external DNS provider, delegate dev.andrewmercer.com there: add NS records for ns1–ns4.dev.andrewmercer.com at the parent, plus glue A records for them (required since these name servers' own names live inside the zone they serve).
  5. Update /etc/resolv.conf / DHCP options on LAN clients if the internal search domain changes.
  6. Keep the old andrewmercer.local zone resolvable for a while (or at least log queries against it) to catch anything still hardcoded to the old name before fully retiring it.

Troubleshooting

isc_stdio_open '/etc/bind/named.log' failed: permission denied The log path in named.conf.logging isn't writable by the bind user. Confirm the log file's actual configured location matches where you've set permissions — in this setup it should be under /var/log/named/, owned bind:bind:

sudo mkdir -p /var/log/named
sudo chown bind:bind /var/log/named
sudo systemctl restart bind9

rndc: connect failed: 127.0.0.1#953: connection refused named isn't running, or crashed on startup (often because of the log permission issue above — check journalctl -u bind9 first). Confirm with systemctl status bind9, fix the underlying cause, then restart.

References

  • https://www.isc.org/bind
  • https://en.wikipedia.org/wiki/BIND
  • https://bind9.readthedocs.io/en/latest/dnssec-guide.html