External validation of DNSSEC chain in Knot DNS

Starting with version 3.5, Knot DNS allows you to perform an external validation of a new zone version before updating it. This article describes how is this feature used to validate (not only) .CZ zones.

Motivation

Five years ago, we began using Knot DNS to manage DNSSEC for the .CZ zone.

We removed all in-house scripts from the signing process and relied entirely on the implementation in Knot DNS. Real-world experience has confirmed that this was the right decision. The complexity of the signing system has been reduced; the entire process is highly reliable and ensures that the DNS data sent to the public anycast is properly signed, valid, and consistent.

To avoid the unintended publication of a zone with invalid DNSSEC signature, we have added another intermediate validation server between the hidden master (HM) and the public DNS anycast. Knot DNS also runs on this server with the option dnssec-validation: on enabled.

This feature essentially covers most of the issues that can arise when signing a zone (a complete list of validations is provided in the documentation), and thanks to incremental processing, the impact on propagation time is minimal. However, we also wanted to address other risks in the validation process.

One of these is checking DNSSEC signatures not only within the zone itself but also against the DS record in the parent zone. The second risk is a potential error in the software implementation. Any software can contain errors, and if we use the same software for both signing and validation, it is advisable to include a second, independent implementation using a different tool in the checks.

Both of these can be addressed through external validations.

How does the external validation work

First, it is important to note that external validation is not necessarily a substitute for DNSSEC validation or ZONEMD verification, but can function alongside them.

Unlike the options mentioned above, this is not, strictly speaking, active verification on the part of Knot DNS. Instead, Knot DNS makes a new version of the zone (or part of it) available in the form of a zone file, while retaining the existing version itself. It then suspends all update operations (zone file updates, incoming IXFR/AXFR, DNSSEC re-signing) and waits for either a zone-commit or zone-abort command. If it does not receive either within the specified time window, the zone update will not proceed.

Configuration

The external validation configuration consists of two parts. First, you need to define the validation parameters in a separate section:

external:
- id: STR
timeout: TIME
dump-new-zone: STR
dump-removals: STR
dump-additions: STR

Of the parameters listed, only the timeout has a default value—it is specified in seconds and defaults to 300. All three dump parameters specify the path to the file where the new version of the zone (in the case of dump-new-zone) or a portion of it (for the other two) is to be saved. The resulting configuration might look like this:

external:
- id: validate-zone
timeout: 300
dump-new-zone: "/var/lib/knot/validations/%s.zone"

The validation specified with the ID validate-zone saves a zone file named domain.zone to the /var/lib/knot/validations directory and waits for confirmation for five minutes.

This configuration is then applied to the zones that are to undergo external validation. This is a zone parameter that can also be applied to a template:

zone:
- domain: DNAME
external-validation: external_id
...

Here’s another specific example:

template:
- id: validated-zones
storage: "/var/lib/knot/zones"
journal-max-usage: 8G
journal-max-depth: 10
master: hm-remote
dnssec-validation: on
external-validation: validate-zone

All zones using the validated-zones template will be checked by internal DNSSEC validation and will wait for external validation.

While this configuration works, it has one major drawback. As it stands, the operator is not notified in any way that the system is currently waiting for the result of external validation. According to the documentation, there are two solutions: either check the zone status using knotc or use D-Bus. To use D-Bus, you need to add the appropriate settings in the server section:

server:
dbus-event: external-verify

If everything is configured correctly, the incoming zone transfer will look like this:

[cz.] refresh, ... remote serial 1781259435, zone is outdated
[cz.] IXFR, incoming, ... started
[cz.] IXFR, incoming, ... finished, remote serial 1781259435, 0.00 seconds, 2 messages, 30183 bytes
[cz.] DNSSEC, incremental validation successful, checked RRSIGs 191
[cz.] waiting for external validation
[cz.] control, received command 'zone-commit'
[cz.] refresh, ... zone updated, 8.84 seconds, serial 1781257635 -> 1781259435, expires in 10783 seconds

The first three lines correspond to a standard zone update via IXFR. The fourth line indicates that the DNSSEC validation was successful. Knot DNS then exports the zone and generates an external_verify signal on D-Bus, as shown in the fifth line. The next line shows that the validation was successful and the zone change is confirmed by the zone-commit signal. Only then is the zone updated on the Knot DNS side.

However, for the zone to actually be validated, D-Bus messages must first be processed in some way.

D-Bus client

The official documentation for Knot DNS also includes a sample implementation of a D-Bus client. The basic structure (in our case) of a Python script intended for deployment as a systemd service might look something like this:

import socket
import dasbus.connection
import dasbus.loop
import signal
import sys
import time
import subprocess
import os
import logging


if __name__ == '__main__':
bus = dasbus.connection.SystemMessageBus()
loop = dasbus.loop.EventLoop()

def handler_shutdown(signum, frame):
log.info(f"stopping validator")
loop.quit()
sys.exit(0)

signal.signal(signal.SIGTERM, handler_shutdown)
signal.signal(signal.SIGINT, handler_shutdown)
signal.signal(signal.SIGHUP, handler_shutdown)

def connect_to_signal(_signal, _callback):
bus.connection.signal_subscribe(
"cz.nic.knotd",
"cz.nic.knotd.events",
_signal,
"/cz/nic/knotd",
None,
0,
lambda _, sender, path, interface, signal, args: _callback(
sender, path, interface, signal, args.unpack()
),
)

connect_to_signal("external_verify", handler_external)

log.info("starting validator")
loop.run()

sys.exit(0)

The most important function is connect_to_signal, which binds a specific function to a specific D-Bus signal and passes the relevant parameters to it. The validation itself then takes place in the handler_external function. In our case, we validate not only the .CZ zone, but also the second level domain (SLD) zones and catalog zones we manage. The handler_external function therefore serves only as a dispatcher that calls the appropriate validation function based on the zone type.

In addition to the validation itself, it is crucial to link it to the propagation of the result to Knot DNS:

def handler_external(sender, path, interface, signal, args):
(zone, serial) = args

if zone_is_valid(zone):
subprocess.run(["/usr/sbin/knotc", "zone-commit", zone])
else:
subprocess.run(["/usr/sbin/knotc", "zone-abort", zone])

And finally, here is an example of the definition of a systemd service itself:

[Unit]
Description=Knot validator service
Requires=knot.service
After=knot.service
PartOf=knot.service

[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/sbin/knot-validator.py
Restart=on-failure
User=knot
Environment=PYTHONUNBUFFERED=1
TimeoutStopSec=5
SyslogIdentifier=knot-validator

[Install]
WantedBy=multi-user.target

Its main and only function is to run the D-Bus client along with Knot DNS.

Validation

The validations performed can be of any kind, but they must be completed before the timeout expires. It is particularly important to keep in mind that loading the zone itself takes a considerable amount of time, especially when there is a large number of records. For this reason, most validations for the .CZ zone are performed immediately after generation.

Validators perform only the validation of the DNSSEC chain of trust. To do this, it is sufficient to load the zone apex, where the relevant DNSKEY records are located. Subsequently, the addresses of the authoritative name servers are obtained from the NS records of the parent zone, from which the DS records for the validated zone are obtained. Then, the obtained DS records are compared with those generated from the DNSKEY records, and if there is at least one match, the chain of trust is confirmed. The same validation method is used for selected SLD zones under our management.

Catalog zone validation involves verifying the number and presence of selected member zones. The goal is to prevent the unintended propagation of an empty catalog zone, which would result in the rapid removal of the relevant zones from the internet.

All DNS processing takes place directly within a Python script using the dnspython library.

Monitoring

Monitoring is an essential part of the validation process. In our case, we use the Centreon platform, which is based on Nagios. Monitoring is performed either actively by calling local scripts on the monitoring platform side, or passively, where the status of the monitored service is updated via the REST API. The latter method can be easily combined with the evaluation of D-Bus signals.

First, it is advisable to modify the Knot DNS configuration so that, in addition to external validation, it generates signals indicating the status of the zone update:

server:
dbus-event: [ external-verify, zone-updated ]

This makes it possible to add a new set of functions that are independent of external validation:

if __name__ == '__main__':
# ...
connect_to_signal("zone_updated", handler_updated)
connect_to_signal("zone_not_updated", handler_not_updated)

These involve calling the monitoring platform’s API and updating the status of the relevant service. Alternatively, the existing handler_external function can be used with added call to the monitoring platform’s API along with the knotc call.

Conclusion

Knot DNS offers a robust and straightforward way to implement external validation. By using a file to store new zone versions (or their changes), it is possible—with appropriate permission settings—to completely separate validation from the DNS daemon and limit interaction to sending a signal to confirm or reject a zone change.

If someone prefers to perform validations against an active DNS server instead of a zone file, that option is also available—they can direct their queries to the primary server from which the new zone version originated.

In addition to the .CZ zone, we also make good use of external validation for the gov.cz domain, where, among other things, the aforementioned domain catalog is deployed; furthermore, this form of validation is also available for TLD domains for which we provide hosting on our DNS anycast.

Additional validation takes some time to process compared to the situation without it, so it may affect the speed at which changes propagate. From our perspective, however, the validity, stability, and robustness of DNS data take priority.

Autor:

Zanechte komentář

Všechny údaje jsou povinné. E-mail nebude zobrazen.