Set-OPS-Public/roles/serveur_powerdns/templates/setops-dnssec-zone.sh.j2

110 lines
4.9 KiB
Text
Raw Normal View History

#!/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