LY

DNS Migration & DNSSEC

dns

Aug 26, 2026

DNS Migration & DNSSEC

I run two VPS instances. server1 is an OpenBSD box that serves static files and acts as my secure anchor. server2 is a Debian machine that runs containers and does the heavy lifting. The goal was to build a DNS architecture where server1 is the hidden master — it holds the authoritative zone file but never appears in public NS records. Public queries hit server2 and a cloud provider's secondary nameserver. server1's IP stays out of DNS entirely.

1. The architecture

server1 runs NSD as the hidden master. The zone file lives at /var/nsd/zones/master/ and contains all records for the primary domain. server2 also runs NSD, configured as a secondary that pulls from server1 via AXFR (zone transfer). A cloud provider acts as the third nameserver, synced manually with a script.

The nameserver delegation at the registrar points to ns1.primarydomain (server2) and the cloud provider's NS. server1 is nowhere in the public chain — it only speaks to server2 over a private AXFR channel.

2. NSD configuration on server1

NSD on OpenBSD is straightforward. The config at /var/nsd/etc/nsd.conf defines the zone and allows AXFR transfers only to server2's IP. The zone file follows standard RFC 1035 format with SOA, NS, A, CNAME, and MX records. Every edit requires incrementing the serial number — I use the YYYYMMDDNN convention so it's always monotonically increasing.

After editing, doas rcctl reload nsd picks up the changes. server2 polls for SOA serial changes and triggers AXFR automatically when it detects a higher serial.

3. NSD configuration on server2

server2's NSD is configured as a secondary with request-xfr pointing to server1. It stores the received zone in /var/lib/nsd/. The refresh interval is set to 3600 seconds (1 hour), but NOTIFY messages from server1 trigger immediate transfers for most changes.

4. Cloud provider sync

The cloud provider doesn't support AXFR inbound, so I wrote ~/bin/sync-dns-to-cloud — a shell script that reads the zone file, parses it, and pushes records via the provider's API. It handles A, AAAA, CNAME, MX, and TXT records. Not ideal, but it only needs to run when I change the zone, which is rare.

5. Zone management workflow

The workflow for any DNS change is: (1) SSH to server1, (2) edit the zone file and increment the serial, (3) doas rcctl reload nsd, (4) server2 picks it up via AXFR within seconds, (5) run the sync script for the cloud provider. Three steps if I skip the cloud sync for records that don't need geographic redundancy.

6. DNSSEC signing

After the base DNS was working, I signed the zone with DNSSEC. Generated both a Zone Signing Key (ZSK) and a Key Signing Key (KSK) using ldns-keygen with ECDSAP256SHA256 (algorithm 13). The ZSK signs individual records, the KSK signs the DNSKEY RRset.

The signing process: ldns-signzone takes the unsigned zone file and both keys, outputs a signed zone with RRSIG, NSEC, and DNSKEY records. I then published the DS record at the registrar — this is the trust anchor that chains the domain to the root zone. The DS record contains the KSK's key tag, algorithm, digest type, and digest hash.

7. Verification

Tested with dig +dnssec to confirm RRSIG records are returned, delv for full chain-of-trust validation, and online tools like DNSViz to visualize the chain. All checks passed — the zone validates from root to leaf.

8. Why this architecture

The hidden master pattern keeps server1's IP out of public records entirely. Anyone querying DNS sees only server2 and the cloud provider. server1 is invisible to scanners, bots, and anyone enumerating the domain. This matters because server1 also handles other sensitive services — the less exposure, the better.

Geographic redundancy across two physical locations and a cloud provider means DNS stays up even if one goes down. And since server1 is the single source of truth, there's never a split-brain problem — changes flow in one direction only.

9. Edge cases handled

If server1 goes down, server2 and the cloud provider continue serving the cached zone until the SOA expiry (set to 1 week). If server2 goes down, the cloud provider handles all queries. If both secondaries go down, DNS is offline but server1 is untouched and can be re-synced once connectivity returns.

The AXFR channel between server1 and server2 is restricted by IP in NSD's config — only server2's IP can request transfers. This prevents unauthorized zone dumps.

10. What's not included

I didn't set up automated DNSSEC key rotation. The current keys work indefinitely, but best practice is to rotate the ZSK quarterly and the KSK annually. That's a future task. I also didn't automate the cloud provider sync — it's manual because DNS changes are infrequent and I want to review each one before it goes live.

← all posts