110 lines
4.9 KiB
Text
110 lines
4.9 KiB
Text
|
|
#!/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
|