Jeffrey Harris

Securing your DNS with TSIG and Age Keys

Securing Your DNS: TSIG Keys for BIND 9 and Age Keys for Secrets

If you run your own authoritative DNS with BIND 9, you have probably hit the two classic problems: how do you let an automated system (a web control panel, a sync tool, or a script) update your zones securely, and how do you safely move zone data around without trusting the whole internet? And once you have DNS working, the next question is always secrets: how do you encrypt a message so only the right person — or the right machine — can read it?

This post walks through both. First, we create a TSIG key, wire it into BIND 9 so it can update your apex domain and every subdomain, and enable AXFR zone transfers with it. Then we generate an age key pair, publish the public half, and use it to encrypt and decrypt messages from the command line.


Section 1: TSIG Keys — Authenticated DNS Updates and Transfers

DNS updates and zone transfers are sensitive. By default, nsupdate traffic is unauthenticated and the server will happily talk to anyone who asks. TSIG (Transaction SIGnature) solves this by signing every DNS message with a shared secret key, proving to both sides that the message really came from who it says it did.

Step 1: Generate the Key

BIND ships with a dedicated tool for this, tsig-keygen (on older systems you may find dnssec-keygen -a HMAC-SHA256 -b 256 -n HOST instead). Run it from any machine; it just needs a name and produces a key snippet you can paste into your configuration.

$ tsig-keygen zoneforge. > /etc/bind/keys/zoneforge.key
$ cat /etc/bind/keys/zoneforge.key
key "zoneforge." {
    algorithm hmac-sha256;
    secret "Oeq1gvZbDD3hFcdPFsbgowpIpW9n7WqlveEHXeY+B8s=";
};

Any string which is a valid DNS name can be used as a key name.

Since we want a valid DNS name without any surprises, remember to add the dot to the end of your key name, just like a domain name.

Keep that output somewhere safe. The secret is a shared secret — anyone who has it can sign updates and request transfers as you. Don't worry about that key I showed, it's not a live key.

Step 2: Put the Key in the Configuration

On your BIND server, create a key file (or drop the snippet straight into named.conf). A tidy approach is to keep it in its own file and include it:

# /etc/bind/keys/tsig-keys.conf  (or wherever your named.conf lives)
key "zoneforge." {
    algorithm hmac-sha256;
    secret "Oeq1gvZbDD3hFcdPFsbgowpIpW9n7WqlveEHXeY+B8s=";
};

Then include it from named.conf:

include "/etc/bind/keys/tsig-keys.conf";

Step 3: Grant Update Permission for the Apex Domain and All Subdomains

Now the important part: telling BIND what this key is allowed to do. You do that inside your zone definition with update-policy. A common pattern is a "dynamic zone" managed entirely by your automation:

zone "example.com" {
    type master;
    file "/var/lib/bind/db.example.com";
    update-policy {
        grant zoneforge zonesub any;
    };
};

Let's break down that single grant line, because it is easy to get wrong:

Traefik and your TSIG Key

A common use is to create a key for your traefik instance and use it to validate DNS challenges:

Suppose we have this key:

key "traefik-acme." {
    algorithm hmac-sha256;
    secret "XPEosedwqiO3SiRUHh/fi52RdP1XkLFT2mFSs8EzrK4=";
};

Include it in your /etc/bind/named.conf.local file, and give permissions to the DNS challenge. In this case, we only want the traefik-acme key to have access to _acme-challenge.example.com. so we explicitly list it in the zone stanza.

include "/etc/bind/keys/traefik-acme.key";
zone "example.com" {
    type master;
    file "/var/lib/bind/db.example.com";
    update-policy {
        grant traefik-acme name _acme-challenge.example.com. TXT;
    };
};

In your traefik configuration, add the following keys (note the ending dots):

DNSUPDATE_TSIG_KEY=traefik-acme.
DNSUPDATE_TSIG_SECRET=XPEosedwqiO3SiRUHh/fi52RdP1XkLFT2mFSs8EzrK4=
DNSUPDATE_TSIG_ALGORITHM=hmac-sha256.

Restart Bind

After editing, reload the bind9 server:

$ rndc reload

Test it with nsupdate using the same key file:

$ nsupdate -k /etc/bind/tsig-keys.conf
> server ns1.example.com
> zone example.com
> update add _challenge.example.com 300 TXT "abc123"
> send

A silent success (no error message) means the update was accepted and signed.

Step 4: Allow AXFR Zone Transfers with the Same Key

Zone transfers (AXFR) let a secondary server — or a sync tool, or a backup system — pull a full copy of a zone. You should never allow open AXFR. Use the same TSIG key to restrict who may transfer, again via update-policy in the zone:

zone "example.com" {
    type master;
    file "/var/lib/bind/db.example.com";
    update-policy {
        grant zoneforge zonesub any;
    };
    also-notify { 192.0.2.10; };      /* your secondary / sync tool */
    allow-transfer { key zoneforge; };
};

allow-transfer { key zoneforge; }; says: only servers that can present this TSIG key may pull a full zone copy. That one line is the difference between "anyone can slurp my zone data" and "only my authenticated peers."

Verify the transfer works from the requesting side with dig and the key:

$ dig @ns1.example.com example.com AXFR -y hmac-sha256:zoneforge:aB3dEf9...long-base64-secret...

If you see the records stream out, the AXFR with TSIG is working.

Putting It Together

The same key now does double duty: authenticated updates for the apex and all subdomains (update-policy grant ... zonesub any), and authenticated zone transfers (allow-transfer { key zoneforge; }). Add allow-update { key zoneforge; }; only if you are on an older BIND version — update-policy is the preferred mechanism on BIND 9.16+.


Section 2: Age Keys — Modern, Simple File Encryption

Now let's talk secrets. age is a modern file-encryption tool: simple, audited, and designed as a friendlier replacement for PGP for the common case of "encrypt this file for this person." It uses modern cryptography (X25519 + ChaCha20-Poly1305) and has a tiny, comprehensible CLI.

Step 1: Install and Generate a Key Pair

Install it — brew install age on macOS, apt install age on Debian/Ubuntu, pacman -S age on Arch, or grab a release binary from the project. Then generate a key pair:

$ age-keygen -o key.txt
Public key: age1qrv95sau5h02a7wueelak7c83qgf6xf4tx08vcnyaqs6ef99ppvq34x062

Two things just happened:

Step 2: Publish the Public Key

The public key is meant to be public. Add it to your website, your SSH/Git profile, a keys page in your docs, or a key.pub file in your repo — however you want people to find you. For example, put this in a keys.md or a pubkey endpoint:

My age public key:

age1examplemushroom...long-public-key...

Anyone who sees it can now encrypt a file that only you can read. That is the whole trick of asymmetric encryption: share the public key freely, guard the identity.

Step 3: Encrypt a Message

Encrypt a file for a recipient using their public key:

$ age -e -r age1examplemushroom...long-public-key... \
    -o message.age message.txt

-e means encrypt, -r specifies the recipient's public key, and -o names the output file. The result, message.age, is binary and safe to email, post, or commit — it leaks nothing without the matching identity.

You can also pipe text straight in without touching disk:

$ echo "hello, secure world" | age -e -r age1examplemushroom... > message.age

Just like gpg, you can use the -a/--armor option to armor encode the output.

And you can encrypt to multiple recipients at once — handy when a secret needs to reach several people:

$ age -e \
    -r age1alice... \
    -r age1bob... \
    -o secret.age secret.txt

Step 4: Decrypt a Message

To decrypt, use your identity file (the private half you generated earlier):

$ age -d -i key.txt -o message.txt message.age
$ cat message.txt
hello, secure world

You can also decrypt from stdin without saving the decrypted output to disk:

$ age -d -i key.txt < message.age

A Practical Combined Example

Encrypt a backup, then decrypt it later — or on another machine that holds the identity:

$ tar czf - /var/lib/bind | age -e -r age1examplemushroom... > bind-backup.tgz.age
$ age -d -i key.txt < bind-backup.tgz.age | tar xzf -

Why This Matters Next to TSIG

Notice a theme: in both halves of this post, one secret does the work. Your TSIG key authenticates DNS updates and zone transfers between machines that share it. Your age identity decrypts secrets sent to you by anyone who has your public key. In each case the rule is the same:

DNS controls where things live and who can talk to the servers; age controls what is readable and by whom. Put both together, and you have a solid foundation for automating DNS administration without handing out keys or secrets to everyone who asks.

I use my agekey to encrypt messages in my DNS zones. When I was manually editing a zone, then rndc reload example.com then remembering that I forgot to update the serial number, and reediting, and seeing something else that just wasn't formatted right, and doing it all over again, and wondering if the serial number increment and rndc reload command was working correctly, I had comments in my zone files. An old IP address, a service renewal, an url of a site that that had the SSHFP record generator: ssh-keygen -r jeffharris.us.

Then, when I started using nsupdate, and using dynamic zones, I lost my comments. So now, I use my agekey to encrypt a string to myself, and add it as a TXT record in an _infra zone. Agekey strings are long, so don't try to encrypt a whole novel, but it works, and you don't know what's there.

I wonder if this will ever turn up in a spy novel as a way of passing information across the globe. Using an internet cafe or a VPN to do a dig of some site, copy the data, and you have your secret instructions for world domination.

Why This Post

I built age encryption into ZoneForge, which conveniently lets you use your TSIG key, if configured correctly to allow both nsupdate and AXFR transfers, to administer your bind9 files without that pesky SSH, and totally without webmin. I'm still working on it, so your miles may vary if it's close to the date of this post.

Sign up, enter your TSIG key, and validate your first zone. We'll use your key to place a temporary TXT record on your zone, then pull the record from DNS and remove the record. From then, you can make any changes you need. If you make other changes outside the ZoneForge system, just click the button for an AXFR transfer, and we'll fetch all the records and update the view.

This is designed for those of us still using our own Bind9 DNS servers, and not the registrar's name servers.