GeoDNS with Knot and DNSSEC, the secure way
GeoDNS with DNSSEC fails in the rudest way possible: only for the users in one region, only after a key rollover, and never from the office where you test it.
The short answer
Use Knot mod-geoip with full zone signing and manual key management. Keep a default record of each type in the zone, generate keys before the module loads, and after every ZSK change run knotc reload so the module re-signs its tailored answers. Validate from a client in each region, not only from your own.
On this page
What goes wrong
GeoDNS gives different answers to different clients. A resolver in one
region gets the address of the nearest edge; everyone else gets a default.
Knot does this with the mod-geoip module, by subnet or by a MaxMind
database.
With DNSSEC, every answer needs a valid signature. Knot pre-signs the module's alternative answers when the module loads. The zone's own records are re-signed whenever keys change. The module's answers are not. After an automatic ZSK rollover removes the old key, the tailored answers still carry signatures from a key that is no longer in the DNSKEY set. Validating resolvers in those regions return SERVFAIL. The default answer still works, so your own checks stay green.
What the docs say
Also, it is STRONGLY RECOMMENDED to use manual key management in this setting, as the corresponding zone has to be reloaded when the signing key changes and to have better control over key synchronization to all instances of the server.
Source: Knot DNS 3.5 docs, geoip module, Full zone signing
DNSSEC keys for computing record signatures MUST exist in the KASP database or be generated before the module is launched, otherwise the module fails to compute the signatures and does not load.
Source: Knot DNS 3.5 docs, geoip module, Full zone signing
every RRset configured in the module should have a default RRset of the same type contained in the zone, so that the NSEC(3) chain can be built correctly.
Source: Knot DNS 3.5 docs, geoip module, Full zone signing
The docs say the zone "has to be reloaded". In the test below, knotc zone-reload did not make the module re-sign; a server knotc reload did.
The docs also do not mention that the failure is regional, which is what
makes it slow to detect.
The secure configuration
/etc/knot/geo.conf, the module's answer map:
www.example.com:
- net: 198.51.100.0/24 # region A resolvers
A: 198.51.100.10
- net: 203.0.113.0/24 # region B resolvers
A: 203.0.113.10/etc/knot/knot.conf:
server:
edns-client-subnet: on # use the client subnet that resolvers forward
mod-geoip:
- id: geo
config-file: /etc/knot/geo.conf
ttl: 60 # short TTL for tailored answers
mode: subnet # or geodb with geodb-file and geodb-key
policy:
- id: geo_signing
manual: on # no automatic rollovers behind the module's back
algorithm: ecdsap256sha256
dnskey-ttl: 3600
zone-max-ttl: 3600
zone:
- domain: example.com
dnssec-signing: on
dnssec-policy: geo_signing
module: mod-geoip/geoThe zone keeps a default for every type the module serves:
www A 192.0.2.10 ; default answer, and the anchor for the NSEC/NSEC3 chainKeys must exist before the first start:
keymgr example.com generate algorithm=ecdsap256sha256 ksk=yes zsk=no
keymgr example.com generate algorithm=ecdsap256sha256 ksk=no zsk=yes
chown -R knot:knot /var/lib/knotA planned ZSK rollover (pre-publish), with the reload at the right moment:
# Day 0: publish the new ZSK now, activate it after the DNSKEY TTL plus propagation.
keymgr example.com generate algorithm=ecdsap256sha256 ksk=no zsk=yes publish=+0 active=+2h
keymgr example.com set <old-zsk-tag> retire=+2h remove=+4h
knotc zone-keys-load example.com
# After "active": re-create the module so its answers are signed with the new ZSK.
knotc reload
# The old ZSK leaves the DNSKEY set at "remove", after the module TTL has passed.If you run several GeoDNS servers, share the KASP database (or run the same key operations on each) and reload them all before the old key is removed.
Prove it
The test (secure-tests/geodns-knot-dnssec/run.sh) runs Knot 3.5.4 with
the module in subnet mode. Clients at 127.0.0.2 ("region A") and 127.0.0.3
("region B") get tailored answers; 127.0.0.1 gets the default.
First start without keys, as the docs warn:
error: [example.com.] DNSSEC, no keys are available
error: [example.com.] module 'mod-geoip/geo', failed to load DNSSEC keys
error: [example.com.] module 'mod-geoip/geo', failed to load (no keys for signing)
error: config, failed to activate modules (invalid module)
error: [example.com.] zone cannot be activated (invalid module)Before: automatic ZSK rollover
With keys and an automatic policy (ZSK lifetime 20 seconds in the lab), every region validates at first:
[20:58:45] $ kdig @127.0.0.1 -b 127.0.0.2 www.example.com A +dnssec +norecurse +noall +answer
www.example.com. 5 IN A 198.51.100.10
www.example.com. 5 IN RRSIG A 13 3 5 20261008205841 20260924192841 59304 example.com. ...
$ delv @127.0.0.1 -b 127.0.0.2 -a anchor.conf +root=example.com www.example.com A
; fully validatedA resolver forwarding EDNS Client Subnet gets the same tailored answer:
kdig @127.0.0.1 www.example.com A +subnet=127.0.0.3/32 +norecurse +short203.0.113.10Knot rolls the ZSK and removes key 59304:
20:59:01 info: [example.com.] DNSSEC, ZSK rollover started
20:59:01 info: [example.com.] DNSSEC, next key action, ZSK tag 21969, replace at 2026-09-24T20:59:07+0000
20:59:07 info: [example.com.] DNSSEC, next key action, ZSK tag 59304, remove at 2026-09-24T20:59:13+0000The default answer is re-signed and validates. The regional answers still carry signatures from the removed key, and fail:
[20:59:31] $ kdig @127.0.0.1 -b 127.0.0.1 www.example.com A +dnssec +norecurse +noall +answer
www.example.com. 5 IN A 192.0.2.10
www.example.com. 5 IN RRSIG A 13 3 5 20261008205907 20260924192907 21969 example.com. ...
$ delv @127.0.0.1 -b 127.0.0.1 -a anchor.conf +root=example.com www.example.com A
; fully validated
[20:59:31] $ kdig @127.0.0.1 -b 127.0.0.2 www.example.com A +dnssec +norecurse +noall +answer
www.example.com. 5 IN A 198.51.100.10
www.example.com. 5 IN RRSIG A 13 3 5 20261008205841 20260924192841 59304 example.com. ...
$ delv @127.0.0.1 -b 127.0.0.2 -a anchor.conf +root=example.com www.example.com A
;; validating www.example.com/A: no valid signature found
;; insecurity proof failed resolving 'www.example.com/A/IN': 127.0.0.1#53
[20:59:31] $ kdig @127.0.0.1 -b 127.0.0.3 www.example.com A +dnssec +norecurse +noall +answer
www.example.com. 5 IN A 203.0.113.10
www.example.com. 5 IN RRSIG A 13 3 5 20261008205841 20260924192841 59304 example.com. ...
$ delv @127.0.0.1 -b 127.0.0.3 -a anchor.conf +root=example.com www.example.com A
;; validating www.example.com/A: no valid signature found
;; insecurity proof failed resolving 'www.example.com/A/IN': 127.0.0.1#53After: manual keys and a server reload
A planned roll from ZSK 35469 to 53035. A zone reload alone leaves the module on the old key:
$ keymgr example.com generate algorithm=ecdsap256sha256 ksk=no zsk=yes publish=+0 active=+10
$ keymgr example.com set 35469 retire=+10 remove=+20
OK
$ knotc zone-keys-load example.com
OK
[20:59:48] new ZSK active: 'zone-reload' is not enough for the module ...
$ knotc zone-reload example.com
OK
$ kdig @127.0.0.1 -b 127.0.0.2 www.example.com A +dnssec +norecurse +noall +answer | awk '$4=="RRSIG"{print $5, "signed by key", $11}'
A signed by key 35469
[20:59:51] ... a server reload re-creates the module and re-signs its answers with the new ZSK
$ knotc reload
ReloadedAfter the old key is removed, every region validates:
[21:00:01] $ kdig @127.0.0.1 -b 127.0.0.2 www.example.com A +dnssec +norecurse +noall +answer
www.example.com. 5 IN A 198.51.100.10
www.example.com. 5 IN RRSIG A 13 3 5 20261008205951 20260924192951 53035 example.com. ...
$ delv @127.0.0.1 -b 127.0.0.2 -a anchor.conf +root=example.com www.example.com A
; fully validated
[21:00:01] $ kdig @127.0.0.1 -b 127.0.0.3 www.example.com A +dnssec +norecurse +noall +answer
www.example.com. 5 IN A 203.0.113.10
www.example.com. 5 IN RRSIG A 13 3 5 20261008205951 20260924192951 53035 example.com. ...
$ delv @127.0.0.1 -b 127.0.0.3 -a anchor.conf +root=example.com www.example.com A
; fully validatedMistakes people make
Testing only from where you sit
Your office resolver gets the default answer, which Knot re-signs on its
own. Only the tailored answers break. Run the validation check from a
resolver in each region, or from one host with +subnet= per region.
Leaving automatic key management on
The automatic policy works for plain zones and quietly breaks the module's
answers at every ZSK rollover. Use manual: on and plan rollovers, as the
docs recommend.
Reloading only when keys change
The module signs its answers when it loads, and those signatures have a
lifetime. With no key change and no reload, they simply expire, and
validating resolvers in the tailored regions return SERVFAIL. Run
knotc reload on a schedule well inside rrsig-lifetime; the lab run is in
building your own GeoIP CDN.
Reloading the zone instead of the server
knotc zone-reload re-reads the zone. In the test, it did not re-sign the
module's answers. knotc reload did. Put knotc reload in the rollover
runbook, between "new key active" and "old key removed".
A tailored type with no default in the zone
The NSEC or NSEC3 chain is built from the zone, not from the module. If the module serves a type the zone does not have at that name, the chain and the answer disagree. The docs require a default RRset of the same type for every RRset in the module config. Add one before you add the module entry.
Several servers, several key sets
With automatic key generation on each server, every instance invents its own keys. Resolvers see different DNSKEY sets from different servers. Use one key set for all instances.
Checklist
- Set
manual: onin the policy used by any zone withmod-geoip. - Generate the KSK and ZSK with
keymgrbefore the first start. - Keep a default RRset in the zone for every type the module returns.
- Set
edns-client-subnet: onif resolvers send ECS. - Plan each ZSK rollover with
publish,active,retireandremovetimers. - Run
knotc reloadafter the new ZSK becomes active and before the old one is removed. - Use the same keys on every GeoDNS instance.
- Validate with
delvfor each region (by source address or+subnet=) after every key change. - Keep the module
ttlshort so a bad answer leaves caches quickly.
GeoDNS answers differently depending on who asks, so test it the same way. A green check from one place proves only that one place works.
H2-CPQE
Learn it on a live range
Anycast and GeoDNS, in Edge and Post-Quantum Networking: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome