DNS and DNSSEC

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.

Updated Houssam Hammoudi, CTOTested with Knot DNS 3.5.4, BIND delv 9.20.29

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

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:

yaml
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:

yaml
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/geo

The zone keeps a default for every type the module serves:

text
www  A  192.0.2.10   ; default answer, and the anchor for the NSEC/NSEC3 chain

Keys must exist before the first start:

bash
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/knot

A planned ZSK rollover (pre-publish), with the reload at the right moment:

bash
# 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:

text
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:

text
[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 validated

A resolver forwarding EDNS Client Subnet gets the same tailored answer:

bash
kdig @127.0.0.1 www.example.com A +subnet=127.0.0.3/32 +norecurse +short
text
203.0.113.10

Knot rolls the ZSK and removes key 59304:

text
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+0000

The default answer is re-signed and validates. The regional answers still carry signatures from the removed key, and fail:

text
[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#53

After: 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:

text
$ 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
Reloaded

After the old key is removed, every region validates:

text
[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 validated

Mistakes 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: on in the policy used by any zone with mod-geoip.
  • Generate the KSK and ZSK with keymgr before the first start.
  • Keep a default RRset in the zone for every type the module returns.
  • Set edns-client-subnet: on if resolvers send ECS.
  • Plan each ZSK rollover with publish, active, retire and remove timers.
  • Run knotc reload after the new ZSK becomes active and before the old one is removed.
  • Use the same keys on every GeoDNS instance.
  • Validate with delv for each region (by source address or +subnet=) after every key change.
  • Keep the module ttl short 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 free

The 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