#!/bin/bash # GENERE par Set-OPS (role serveur_powerdns). Ne pas editer a la main. # # setops-dnssec-zone ZONE signer|nu — met une zone publique dans l'etat DNSSEC du plan. # # signer : la cle arrive sur l'ENTREE STANDARD (la cle CSK de la voute du locataire, en # base64). La zone doit porter CETTE cle, et elle seule. # nu : la zone ne doit porter aucune cle. # # Codes : 0 deja conforme, 10 change, 3 refus (la raison sur la sortie), autre = erreur. # LA CLE N'EST JAMAIS ECRITE SUR LA SORTIE : Ansible la journaliserait. # # LE REFUS QUI COMPTE. Changer la cle — ou retirer la signature — pendant que le registre # publie un DS rend le domaine BOGUS pour tout resolveur validant : site et courriel # injoignables, et aucune machine en panne. On mesure donc le DS publie avant, et une mesure # qui echoue vaut refus : l'absence de preuve n'est pas une preuve d'absence. set -euo pipefail ZONE="${1:?zone}" MODE="${2:?signer|nu}" P=(runuser -u pdns -- pdnsutil --config-name=public) ids() { "${P[@]}" list-keys "$ZONE" 2>/dev/null | awk -v z="$ZONE" '$1 == z { print $7 }' } ds_publie() { # Rend 0 si le registre ne publie AUCUN DS, 1 s'il en publie, 2 si on ne sait pas. local sortie statut sortie=$(dig +time=4 +tries=2 +noall +comments +answer "$ZONE" DS 2>/dev/null) || return 2 statut=$(sed -n 's/.*status: \([A-Z]*\),.*/\1/p' <<<"$sortie" | head -1) [[ "$statut" == "NOERROR" || "$statut" == "NXDOMAIN" ]] || return 2 grep -qE "^[^;].*[[:space:]]DS[[:space:]]" <<<"$sortie" && return 1 return 0 } refuser_si_ds() { local code=0 ds_publie || code=$? case "$code" in 0) return 0 ;; 1) echo "REFUS : le registre publie un DS pour $ZONE. $1 rendrait le domaine BOGUS. Retirer d'abord le DS chez le registraire, attendre son TTL, puis relancer." ;; *) echo "REFUS : impossible de lire le DS publie pour $ZONE (resolveur injoignable ?). $1 sans cette mesure pourrait rendre le domaine BOGUS." ;; esac exit 3 } actuelles=$(ids) if [[ "$MODE" == "nu" ]]; then [[ -z "$actuelles" ]] && exit 0 refuser_si_ds "Retirer la signature" "${P[@]}" disable-dnssec "$ZONE" >/dev/null for id in $(ids); do "${P[@]}" remove-zone-key "$ZONE" "$id" >/dev/null; done # LIMITE CONNUE. Sans SOA-EDIT, le serial servi redescend a celui du contenu — plus petit # que l'heure que le site a vue en dernier. Le site garde donc la version signee jusqu'au # prochain changement de contenu ; sans DS publie (verifie plus haut), c'est sans effet # pour les resolveurs. Pour l'aligner tout de suite : `pdns_control retrieve ZONE` au site. "${P[@]}" set-meta "$ZONE" SOA-EDIT >/dev/null echo "$ZONE : signature retiree." exit 10 fi [[ "$MODE" == "signer" ]] || { echo "mode inconnu : $MODE"; exit 2; } IFS= read -r CLE || true [[ "$CLE" =~ ^[A-Za-z0-9+/]{43}=$ ]] || { echo "REFUS : cle absente ou malformee sur l'entree standard."; exit 3; } # SOA-EDIT EPOCH : LE SERIAL SERVI EST L'HEURE (calcule le 2026-09-16, apres une premiere # version fausse). # # PowerDNS signe avec une inception au debut de la semaine PRECEDENTE et une expiration deux # semaines apres le debut de la semaine COURANTE (mesure : inception 2026-09-10, expiration # 2026-10-01, un 2026-09-17). Le secondaire du site garde les signatures qu'il a tirees : il # faut qu'il retire la zone au moins chaque semaine. # # La premiere version posait INCEPTION-EPOCH : serial = max(serial, inception). Or notre # serial suit deja l'heure du dernier changement — il DEPASSE l'inception pendant deux # semaines. Le serial servi ne bougeait donc qu'au moment ou les signatures du site # expiraient : une heure de BOGUS, au mieux, a chaque cycle. # # EPOCH rend l'heure : le site voit un serial neuf a chaque rafraichissement (une heure) et # retire la zone. Deux zones de quelques Ko par heure — le prix d'une signature jamais # perimee de plus d'une heure. Les signatures servies gardent entre 7 et 14 jours. soa_edit=$("${P[@]}" get-meta "$ZONE" SOA-EDIT 2>/dev/null | grep -cx "SOA-EDIT = EPOCH" || true) if [[ $(wc -w <<<"$actuelles") -eq 1 ]] \ && "${P[@]}" export-zone-key "$ZONE" "$actuelles" 2>/dev/null | grep -qxF "PrivateKey: $CLE" \ && [[ "$soa_edit" -ge 1 ]]; then exit 0 fi # UNE AUTRE CLE EST EN PLACE : la remplacer n'est sur que si aucun DS n'est publie. if [[ -n "$actuelles" ]] && ! { [[ $(wc -w <<<"$actuelles") -eq 1 ]] \ && "${P[@]}" export-zone-key "$ZONE" "$actuelles" 2>/dev/null | grep -qxF "PrivateKey: $CLE"; }; then refuser_si_ds "Remplacer la cle" for id in $actuelles; do "${P[@]}" remove-zone-key "$ZONE" "$id" >/dev/null; done fi if [[ -z "$(ids)" ]]; then f=$(mktemp /run/setops-dnssec.XXXXXX) trap 'shred -u -z "$f" 2>/dev/null || rm -f "$f"' EXIT chown pdns "$f" chmod 0600 "$f" printf 'Private-key-format: v1.2\nAlgorithm: 13 (ECDSAP256SHA256)\nPrivateKey: %s\n' "$CLE" > "$f" "${P[@]}" import-zone-key "$ZONE" "$f" ksk active published >/dev/null fi "${P[@]}" set-meta "$ZONE" SOA-EDIT EPOCH >/dev/null echo "$ZONE : signee." exit 10