Première rédaction de cet article le 13 octobre 2025
Annoncé officiellement le 7 octobre, Cascade est le successeur d'OpenDNSSEC. Ce programme sert à gérer automatiquement les opérations répétitives liées à DNSSEC comme la re-signature ou le remplacement d'une clé.
OpenDNSSEC est, depuis 15
ans, le logiciel utilisé par mes domaines personnels (comme
bortzmeyer.org
par lequel vous êtes passé pour
voir cet article) mais aussi par plusieurs TLD comme
.fr
. Mais il est désormais en fin de vie (cela
a été
annoncé le 3 octobre), il n'y a plus que les mises à jour de
sécurité. La même organisation, NLnet Labs, a développé un
successeur, Cascade. Celui-ci
est actuellement en alpha (ne l'utilisez pas pour la
production !) et voici un premier essai.
DNSSEC, contrairement à ce qui se passe avec le DNS d'antan, nécessite des actions périodiques. La plus évidente est la re-signature, car les signatures DNSSEC ont une date d'expiration (pour éviter les attaques par rejeu). Ici, cette signature expire le 18 octobre 2025 (le « 20251018235702 ») :
% dig +dnssec cyberstructure.fr TXT … ;; ANSWER SECTION: cyberstructure.fr. 86400 IN TXT "v=spf1 mx -all" cyberstructure.fr. 86400 IN RRSIG TXT 13 2 86400 ( 20251018235702 20251004235301 34065 cyberstructure.fr. Hg/ModpW3pTLgUIcgp7yrVUxUyVAXdEIOtfONpN3pxG0 …
Mais il y a aussi la nécessité de pouvoir changer les clés cryptographiques utilisées, par exemple en cas de compromission, avérée ou suspectée. (La plupart des organisations changent les clés systématiquement, compromission ou pas, notamment pour être sûr de pouvoir le faire sans incident le jour où ça sera vraiment indispensable.) Cascade, comme OpenDNSSEC avant lui, automatise toutes ces tâches.
Cascade est développé en Rust, le langage à la mode, et qui pourrait remplacer une bonne partie du logiciel d'infrastructure de l'Internet, qui est quasi-uniquement en C (et C++). Il doit son nom au fait qu'une zone DNS qu'on va signer passe par plusieurs étapes successives, où on peut insérer différentes opérations, par exemple de validation.
Jouons donc un peu avec Cascade (j'ai bien dit que c'était en version alpha ; ne le faites pas en production). Notez que vous pouvez lire la documentation officielle au lieu de cet article. Mais j'ai bien dit que le logiciel était en version alpha, ne vous étonnez pas s'il manque beaucoup de choses dans la documentation (et dans le code, bien sûr). Il existe déjà des paquetages tout faits (mais évidemment pas encore distribués avec votre système d'exploitation) mais on va compiler, c'est plus amusant. Il faut donc une version de Rust qui marche. Rust est très pénible, avec ses changements permanents, qui font que certain·es conseillent même de récupérer et d'installer automatiquement une nouvelle version du compilateur chaque nuit. Mais, bon, la version qui est dans Debian stable (la version 13 de Debian) marche bien. Cascade est un logiciel libre, et il est hébergé sur GitHub :
% git clone https://github.com/NLnetLabs/cascade.git … # Installer les dépendances % apt install cargo pkg-config libssl-dev … % rustc --version rustc 1.85.0 (4d91de4e4 2025-02-17) (built from a source tarball) % cd cascade % cargo build …
Et vous n'avez plus qu'attendre que votre machine compile quelques
centaines de crates
Rust. Vous vous retrouvez alors avec deux exécutables,
./target/debug/cascade
et
./target/debug/cascaded
. Le second est le
démon qui va tourner en permanence. (Si vous
venez d'OpenDNSSEC, notez qu'il n'y a cette fois qu'un seul démon.)
Notez que ce démon, bien qu'il inclue un serveur DNS minimal, ne doit
pas être exposé sur l'Internet. La
configuration recommandée est de l'utiliser comme serveur maitre
caché, sur lequel s'alimenteront un ou plusieurs des serveurs faisant autorité pour
vos domaines. (Si vous venez d'OpenDNSSEC, notez que Cascade ne sait
actuellement pas écrire la zone sur le disque, il faut faire un
transfert de zones, cf. RFC 5936.) Le premier
exécutable, lui, est le programme que vous lancez pour accomplir
telle ou telle tâche, et qui parle au démon, qui fera le boulot.
Sinon, une autre solution, pour compiler, est de suivre les instructions :
cargo install --locked --git https://github.com/nlnetlabs/cascade cargo install --locked --branch keyset --git https://github.com/nlnetlabs/dnst
Et les exécutables atterriront dans votre ~/.cargo/bin
.
Regardons les options des deux programmes :
% ./target/debug/cascade Usage: cascade [OPTIONS] <COMMAND> Commands: config Manage Cascade's configuration zone Manage zones policy Manage policies keyset Execute manual key roll or key removal commands hsm Manage HSMs template Print example config or policy files Options: -s, --server <IP:PORT> The cascade server instance to connect to [default: 127.0.0.1:4539] --log-level <LEVEL> The minimum severity of messages to log [default: warning] [possible values: trace, debug, info, warning, error, critical] -h, --help Print help -V, --version Print version % ./target/debug/cascaded -h Usage: cascaded [OPTIONS] Options: --check-config Check the configuration and exit --state <PATH> The global state file to use -c, --config <PATH> The configuration file to load --log-level <LEVEL> The minimum severity of messages to log [possible values: trace, debug, info, warning, error, critical] -l, --log <TARGET> Where logs should be written to [possible values: stdout, stderr, file:<PATH>, syslog] -d, --daemonize Whether Cascade should fork on startup -h, --help Print help -V, --version Print version
Si vous avez procédé comme moi, un dernier truc à compiler, il nous faut aussi cascade-dnst, par la même organisation :
% cargo install --locked --branch keyset --git https://github.com/nlnetlabs/dnst
Vérifiez bien que les exécutables sont dans votre
PATH
(par exemple, que
~/.cargo/bin
y soit). Pour le démon, vous
pouvez aussi utiliser la variable
dnst-binary-path
dans le
config.toml
.
Pour utiliser Cascade, vous devrez, comme avec OpenDNSSEC, définir une politique (quel algorithme de cryptographie, quelle durée de vie des signatures, quand remplacer les clés, etc), indiquer quelles zones Cascade doit gérer, et avoir un serveur faisant autorité qui va récupérer la zone sur Cascade.
Maintenant, configurons le logiciel, toujours en suivant la
documentation. (Si vous oubliez cette étape, vous aurez un
message « Cascade couldn't be configured: could not load
the config file '/etc/cascade/config.toml': No such file or
directory (os error 2) ».) Vous avez avec Cascade un
fichier de configuration d'exemple,
./etc/config.template.toml
, à la syntaxe
TOML. Le fichier d'exemple peut être utilisé
tel quel dans beaucoup de cas. J'ai juste modifié les paramètres de
journalisation :
[daemon] log-level = "debug" log-target = { type = "file", path = "/dev/stdout"} # Mais log-target = { type = "syslog" } est bien aussi.
Lançons maintenant le démon (voyez plus loin les mécanismes pour
l'avoir en permanence). D'abord, on crée les répertoires configurés
dans le config.toml
:
% sudo mkdir /var/lib/cascade % sudo chown $USER /var/lib/cascade % sudo mkdir /etc/cascade/policies
Puis on y va :
% ./target/debug/cascaded [2025-10-09T15:56:07.480Z] INFO cascaded: State file not found; starting from scratch … [2025-10-09T15:56:09.182Z] DEBUG cascade::state: Saved the global state (to '/var/lib/cascade/state.db') …
Maintenant que le démon tourne, testons le programme de contrôle :
% ./target/debug/cascade zone list %
Pas de message d'erreur, mais pas de zones configurées, c'est normal. (Si le démon ne tournait pas, vous auriez eu « [2025-10-09T15:59:37.183Z] ERROR cascade: Error: HTTP request failed: error sending request for url (http://127.0.0.1:4539/zone/) ».)
Maintenant, il faut créer une politique. La plus courte est :
version = "v1"
mais elle n'est pas très utile. Pour avoir un point de départ, on va demander à Cascade de nous générer un exemple :
% ./target/debug/cascade template policy > mypolicy.toml
(On peut aussi utiliser l'exemple en
etc/policy.template.toml
.)
La politique contient l'algorithme de cryptographie utilisé (j'ai
choisi Ed25519, cf. RFC 8080) :
algorithm = "ED25519"
Je n'ai rien changé d'autre.
On copie le fichier
en /etc/cascade/policies/default.toml
(on peut
évidemment avoir plusieurs politiques, je vous laisse choisir les
noms). Et on indique à Cascade de la charger (ce n'est à faire
qu'une fois, ce sera mémorisé) :
% ./target/debug/cascade policy reload Policies reloaded: - default added % ./target/debug/cascade policy list default % ./target/debug/cascade policy show default default: zones: <none> … signer: serial policy: date counter signature inception offset: 86400 seconds signature validity offset: 1209600 seconds denial: NSEC …
Bon, on peut maintenant ajouter une zone à gérer.
On crée un fichier de zone (je l'ai mis dans
/etc/cascade/zones
mais vous faites comme vous
voulez). Et on dit à Cascade d'y aller, en indiquant la politique à
suivre :
% ./target/debug/cascade zone add --source /etc/cascade/zones/rutaba.ga --policy default rutaba.ga Added zone rutaba.ga
Et voilà :
% ./target/debug/cascade zone list rutaba.ga % ./target/debug/cascade zone status rutaba.ga Status report for zone 'rutaba.ga' using policy 'default' ✔ Waited for a new version of the rutaba.ga zone ✔ Loaded version 2025100901 Loaded at 2025-10-09T16:40:36+00:00 (17s ago) Loaded 246 B and 5 records from the filesystem in 0 seconds ✔ Auto approving signing of version 2025100901, no checks enabled in policy. ✔ Approval received to sign version 2025100901, signing requested ✔ Signed version 2025100901 as version 2025100902 Signing requested at 2025-10-09T16:40:36+00:00 (17s ago) Signing started at 2025-10-09T16:40:36+00:00 (17s ago) Signing finished at 2025-10-09T16:40:36+00:00 (17s ago) Collected 5 records in 0s, sorted in 0s Generated 4 NSEC(3) records in 0s Generated 5 signatures in 0s (5 sig/s) Inserted signatures in 0s (5 sig/s) Took 0s in total, using 1 threads Current action: Finished ✔ Auto approving publication of version 2025100902, no checks enabled in policy. ✔ Published version 2025100902 Published zone available on 127.0.0.1:4543
(L'option --detailed
après
status
vous donnera… des détails.)
Vous pouvez maintenant récupérer la zone signée :
% dig @localhost -p 4543 rutaba.ga AXFR … rutaba.ga. 600 IN SOA ns4.bortzmeyer.org. hostmaster.bortzmeyer.org. ( 2025100902 ; serial … rutaba.ga. 3600 IN DNSKEY 257 3 15 ( oQjxu7aDwhDyqZbqPfx0e3I4Y+UowV0eYlssuXgiMw0= ) ; KSK; alg = ED25519 ; key id = 57782 … rutaba.ga. 600 IN NS ns4.bortzmeyer.org. rutaba.ga. 600 IN NS ns1.bortzmeyer.org. … rutaba.ga. 600 IN RRSIG SOA 15 2 600 ( 20251023164036 20251008164036 64849 rutaba.ga. xn3KC4qzitoUz5ABlJDTtMd9VGPU2l8dYpsd4bYjeCqo vpjBdWcBbwwk1HENj5ESCLEIobpjNS3/rA4qidUvCg== ) rutaba.ga. 600 IN RRSIG NS 15 2 600 ( 20251023164036 20251008164036 64849 rutaba.ga. gWrzM1jYO7GJ234bH8JDejrxMhwtHrGWjxRM30gz37Fb dxnSv2/DiAgOt5OhdTPKYc7+IX4gLR1DPAUlJgXqDA== ) rutaba.ga. 600 IN RRSIG DNSKEY 15 2 3600 ( 20251023163739 20251008163739 57782 rutaba.ga. t3wZ31rY7TgXY+QZXd/Yy7QtTmYdJy6v7Kuj6IN8G+m4 GycTLLn9W3lXErMS7f93RfkvXfERrwzbHqrTp1/oDg== ) … ;; Query time: 1 msec ;; SERVER: ::1#4543(localhost) (TCP) ;; WHEN: Thu Oct 09 16:43:24 UTC 2025 ;; XFR size: 15 records (messages 1, bytes 1201)
(On verra plus loin comment configurer le « vrai » serveur de noms pour utiliser cette zone.)
Comme OpenDNSSEC, Cascade regarde sa montre pour savoir si un
délai suffisant s'est écoulé (rappelez-vous que les résolveurs DNS ont une mémoire, et que cela a
une grande importance opérationnelle ; dans le DNS, rien n'est
jamais instantané). Mais, contrairement à OpenDNSSEC, il demande au
résolveur local des tests (à l'heure actuelle, la machine qui fait
tourner Cascade doit donc avoir accès à un résolveur qui marche). En
attendant, cascade zone status --detailed
vous
affichera des choses comme :
Roll AlgorithmRoll, state Propagation1: Wait until the new DNSKEY RRset has propagated to all nameservers. Try again after 2025-10-09T16:47:42Z
Voyons plutôt une fonction essentielle de Cascade : les étapes de
validation. Pour l'instant, je n'en ai configuré aucune. Mais, en
production, on veut éviter à tout prix ces ennuyeuses erreurs, qui plantent DSNSSEC et
toute la zone. On va donc forcément valider la zone signée avant de
la publier. Le principe est d'écrire un programme (typiquement un
script shell) qui va lancer un programme de validation comme validns ou le
dnssec-verify
de
BIND. On peut avoir cette étape de validation
avant la signature (pour tester le fichier de zone original) ou
après (ce que je fais ici). Essayons pour commencer avec un script
de validation méchant qui refuse tout :
% cat /etc/cascade/reviews/always-reject.sh #!/bin/sh logger "Rejecting ${CASCADE_ZONE} of serial ${CASCADE_SERIAL} from ${CASCADE_SERVER}" ♯ Cascade va utiliser le code de retour pou savoir si la zone est # validée ou pas. exit 1
On configure la politique (pas la configuration générale : valider ou pas fait partie de la politique) :
% cat /etc/cascade/policies/default.toml … # How signed zones are reviewed. [signer.review] required = true cmd-hook = "/etc/cascade/reviews/always-reject.sh"
On recharge la politique (cascade policy
reload
) et la validation est configurée (Cascade étant en
version alpha, pour l'instant, il faut redémarrer le démon mais ce
n'est pas normal). Le résultat est l'attendu :
% ./target/debug/cascade zone status rutaba.ga … • Waiting for approval to publish version 2025101004 Configured to invoke /etc/cascade/reviews/always-reject.sh x An error occurred that prevents further processing of this zone version: x Signed zone was rejected at the review stage.
Et dans le journal du démon :
[2025-10-10T06:32:49.335Z] INFO cascade::units::zone_server: [RS2]: Executed hook '/etc/cascade/reviews/always-reject.sh' for signed zone 'rutaba.ga' at serial 2025101004 [2025-10-10T06:32:49.339Z] DEBUG cascade::units::zone_server: [RS2]: Hook '/etc/cascade/reviews/always-reject.sh' exited with status exit status: 1 [2025-10-10T06:32:49.339Z] DEBUG cascade::targets::central_command: [CC]: Event received: ReviewZone { name: Name(rutaba.ga.), stage: Signed, serial: Serial(2025101004), decision: Reject } … [2025-10-10T06:32:49.339Z] DEBUG cascade::targets::central_command: [CC]: Event received: SignedZoneRejectedEvent { zone_name: Name(rutaba.ga.), zone_serial: Serial(2025101004) }
Écrivons maintenant un script plus sérieux. Il va obtenir la zone
à valider via un transfert de zones. Notez que le
port utilisé est obtenu via une variable
d'environnement ; Cascade sert la zone sur des ports différents
selon qu'elle a été validée ou pas (cf. les paramètres
servers
dans config.toml
). Donc, le « vrai » serveur,
présenté plus loin, n'utilisera pas le même port. Voici le script de
validation utilisable en production :
% cat /etc/cascade/reviews/validate-signed.bash #!/bin/bash # Yes, bash is required, to parse CASCADE_SERVER set -e logger -p daemon.notice -t cascade "Validating ${CASCADE_ZONE} of serial ${CASCADE_SERIAL} from ${CASCADE_SERVER}" # Parsing magic by mozzieongit SERVER=${CASCADE_SERVER%:*} SERVER_IP="${SERVER//[\[\]]/}" # remove brackets from IPv6 SERVER_PORT=${CASCADE_SERVER##*:} # Using double '##' in case its an IPv6 tmp_zone=$(mktemp /tmp/.cascade_zone.XXXXXXXXXX) # Clean when leaving trap "rm -f ${tmp_zone}; exit 1" 1 2 3 15 trap "rm -f ${tmp_zone}" EXIT # Too bad dig logs some errors on standard output :-( dig @${SERVER_IP} -p ${SERVER_PORT} ${CASCADE_ZONE} AXFR > ${tmp_zone} # validns does not handle Ed25519 # validns -z ${CASCADE_ZONE} -p all ${tmp_zone} # or: dnssec-verify -q -o ${CASCADE_ZONE} ${tmp_zone} # Le code de retour du programme de vérification va être celui du script.
Et la zone est acceptée :
[2025-10-10T07:01:01.462Z] DEBUG cascade::units::zone_server: [RS2] Received command: SeekApprovalForSignedZone { zone_name: Name(rutaba.ga.), zone_serial: Serial(2025101001) } [2025-10-10T07:01:01.462Z] INFO cascade::units::zone_server: [RS2]: Seeking approval for signed zone 'rutaba.ga' at serial 2025101001. [2025-10-10T07:01:01.463Z] INFO cascade::units::zone_server: [RS2]: Executed hook '/etc/cascade/reviews/validns.sh' for signed zone 'rutaba.ga' at serial 2025101001 [2025-10-10T07:01:01.485Z] INFO domain::net::server::middleware::cookies: preprocess; request_ip=127.0.0.1 [2025-10-10T07:01:01.485Z] INFO domain::net::server::middleware::xfr::service: AXFR for rutaba.ga from 127.0.0.1:35159 [2025-10-10T07:01:01.498Z] DEBUG cascade::units::zone_server: [RS2]: Hook '/etc/cascade/reviews/validns.sh' exited with status exit status: 0 [2025-10-10T07:01:01.498Z] DEBUG cascade::targets::central_command: [CC]: Event received: ReviewZone { name: Name(rutaba.ga.), stage: Signed, serial: Serial(2025101001), decision: Approve } [2025-10-10T07:01:01.498Z] INFO cascade::targets::central_command: [CC]: Passing back zone review
Bon, maintenant, configurons un « vrai » serveur de noms pour
charger la zone et la servir à tous les résolveurs de
l'Internet. J'utilise nsd. Dans le
nsd.conf
, je mets :
zone: name: "internautique.fr" zonefile: "/var/lib/nsd/%s.zone" request-xfr: AXFR 127.0.0.1@8053 NOKEY allow-notify: 127.0.0.1 NOKEY # Mettre ici les autres serveurs faisant autorité, qu'il faut notifier et autoriser.
On va autoriser 127.0.0.1
(la machine locale,
où tourne Cascade) à nous notifier les changements de la zone
signée, et on demandera les transferts de zone à cette même machine,
sur le port où écoute Cascade (ici 8053). nsd va alors charger la
zone, éventuellement notifier les autres serveurs faisant autorité
et tout roule.
Si vous connaissez DNSSEC, vous avez vu qu'il manque une étape : ajouter l'enregistrement DS (Delegation Signer) dans la zone parente. Cascade n'affiche pas ce DS tout de suite, il sait que les résolveurs ont une mémoire et il attend d'avoir vu la zone signée publiée et que la durée configurée soit écoulée. En version alpha, cette étape ne marche pas toujours donc on va le faire à la main (ne le faites pas en production !) :
% dig @127.0.0.1 -p 8053 AXFR internautique.fr > /tmp/internautique.fr % dnssec-dsfromkey -f /tmp/internautique.fr internautique.fr internautique.fr. IN DS 30790 15 2 BB8987B69144BFB6863D9F86983EE1FB465629CDB3EE8DC43EE6B7C11056598F
Et c'est ce DS qu'on va transmettre à la zone parente (peut-être via son BE). Les programmes de test disponibles en ligne montrent une configuration (presque) correcte : Zonemaster et DNSviz.
Avant-dernière chose à faire : faire tourner le démon en permanence sur la machine. Cascade vient avec un fichier de configuration pour systemd mais une bogue subtile l'empêche actuellement de fonctionner. J'ai donc utilisé un plus classique script rc.local :
% cat /etc/boot.d/cascade #!/bin/sh # You may change this one: DEBUG=1 set -e # Don't touch export PATH=${PATH}:/home/cascade/.cargo/bin if [ ${DEBUG} -ge 1 ]; then DAEMON_OPTS="--log-level debug" export RUST_BACKTRACE=full else DAEMON_OPTS="--log-level info" fi # Does not work with su (for unknown reasons): "Permission denied" sudo -E -u cascade /home/cascade/.cargo/bin/cascaded --daemonize ${DAEMON_OPTS} --state=/var/lib/cascade/state.db --config=/etc/cascade/config.toml # su -p cascade -c '/home/cascade/.cargo/bin/cascaded --state=/var/lib/cascade/state.db --config=/etc/cascade/config.toml'
Enfin, quand on sera en production, il faudra évidemment superviser Cascade. J'utilise un petit script qui suit l'API Nagios :
% cat /usr/local/lib/nagios/plugins/check_cascade #!/bin/sh # Simple Nagios/Icinga plugin for Cascade. USAGE="Usage: $0 -e string_to_expect" TEMP=$(getopt -o "e:h" -- "$@") if [ $? != 0 ]; then echo $USAGE exit 1 fi eval set -- "$TEMP" while true ; do case "$1" in -e) EXPECTED=$2; shift 2;; -h) echo $USAGE; exit 3;; --) shift ; break ;; *) echo "Internal error!"; exit 3 ;; esac done logger "Test Cascade, expect \"${EXPECTED}\"" E_OUT=$(/home/cascade/.cargo/bin/cascade zone list 2>/dev/null) if [ $? -ne 0 ]; then echo "Cascade did not respond" exit 2 fi if [ ! -z "${EXPECTED}" ]; then L_CNT=$(echo ${E_OUT} | grep "${EXPECTED}" 2>/dev/null | wc -l) if [ ${L_CNT} -eq 0 ]; then echo "Cascade zone list did not return a match on ${EXPECTED}" exit 2 fi fi echo "Cascade OK ${S_OUT}" exit 0
Et qui se configure comme cela dans Icinga :
object CheckCommand "cascade" { command = [ PluginContribDir + "/check_cascade" ] arguments = { "-e" = "$cascade_expect$", } } object Host "ayla-cascade" { import "generic-host" check_command = "remote_always_true" vars.cascade = true vars.cascade_expect="internautique\\.fr" }
Et en conclusion, un peu d'opéra, avec l'air « Cascader la vertu » de La Belle Hélène, interprété par Gaëlle Arquez.
Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)
Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)