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:
grant zoneforge— the name of the key we created.zonesub— the scope of the grant.zonesubmatches the apex zone name and every name beneath it, i.e.example.com,www.example.com,mail.example.com, and so on. If you usedzone, the key could only touch records at the apex itself; if you usedsubdomain, only records strictly below the apex. For "apex + all subdomains",zonesubis exactly right.any— the record types the key may touch.anylets it add, delete, and update any record type. If you want to be more conservative you can list specific types, e.g.grant zoneforge zonesub TXT A AAAA;.
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:
key.txtcontains your identity (private key), which can decrypt messages meant for you. It is a secret — protect it like one (chmod 600, keep it in a vault, etc.).- The terminal printed your public key (
age1...). This is what people and machines use to encrypt messages to you. It is safe to share anywhere.
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
-dmeans decrypt.-i key.txttells age which identity to use. If the file was encrypted to that key, decryption succeeds; otherwise age will error out and produce nothing.
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:
- Publish the non-secret part (the age public key; the zone names and policy you configure).
- Guard the secret part (the TSIG secret; the age identity).
- Restrict what the key can do (a
zonesubgrant instead of a blanketallow-update; an age identity kept off shared filesystems).
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.