• Ressources Astuces
  • Script Python - Ajout automatique fingerprint chez Gandi - NSD/DNSSEC via Docker

Je vous propose 2 scripts écrits en python3, l'un pour ajouter automatiquement un fingerprint chez Gandi, l'autre pour supprimer le précédent enregistrement après 24 heures. Les notifications slack permettent de recevoir les messages sur votre téléphone .. pratique pour être tenu informer.

Basés sur l'API de Gandi et sur le tuto de @Hardware
Déployer un serveur DNS autoritaire avec NSD/DNSSEC via Docker

  • Rotation des clés DNSSEC

  • Signature de la zone dns

  • Mise à jour du numéro de série de l'enregistrement SOA

  • Mise à jour automatique du fingerprint chez Gandi

  • Suppression après 24h du précédent fingerprint chez Gandi

Ce sont mes premiers scripts python, aussi n'hésitez pas à m'apporter vos critiques, vos conseils et vos résultats de tests pour les améliorer.

Ajout du fingerprint chez Gandi


import xmlrpc.client
import sys
import os
import subprocess
import fileinput
import requests
import json

api = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')

apikey = 'SSoxxxxxxxxxxxxxxx' ## cle de production gandi "https://v4.gandi.net/admin/api_key"
webhook_url = 'https://hooks.slack.com/services/TBFNA2Y5xxxxxxxxxx/F06eAOJKC1PxxxxxxxR' ## mettre votre webhook_url

# variable pour le nom de domaine
cmd = "grep name /mnt/docker/nsd/conf/nsd.conf | cut -d ':' -f2 | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
temp = process.communicate()[0]
domain = str(temp.decode())[1:-1]

# nouvelles cles
os.system('docker exec nsd keygen '+ domain)

# serial actuel de la zone
for line in open("/mnt/docker/nsd/zones/db."+ domain):
 if "Serial" in line:
  cmd = "grep Serial /mnt/docker/nsd/zones/db."+ domain
  process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  serial = process.communicate()[0]
  serial = str(serial.decode())[1:-10]
  serial = serial.strip ()
  serial = int(serial)

# nouveau serial
cmd = "date -d '+1 day' +'%Y%m%d%H' | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
newserial = process.communicate()[0]
newserial = int(newserial )

# modif serial dans la zone
if serial < newserial:
 for line in fileinput.input("/mnt/docker/nsd/zones/db."+ domain, inplace=True):
  print(line.replace(str(serial), str(newserial)), end='')

# date expiration pour la signature dnssec
cmd = "date -d '+6 months' +'%Y%m%d%H%M%S' | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
date_expire = process.communicate()[0]
date_expire = int(date_expire)
date_expire = str(date_expire)

# signature de la zone DNS
os.system('docker exec nsd nsd-checkzone '+ domain + ' /zones/db.'+ domain + ' >> zone.log')
for line in open("zone.log"):
 if "ok" in line:
  cmd = "grep ok zone.log"
  process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  result = process.communicate()[0]
  final = str(result.decode())[0:-1]
  if final == 'zone' + ' ' +domain +' ''is ok':
   os.system('docker exec nsd signzone '+domain+' '+date_expire)
   os.remove('zone.log')
  else:
   slack_data = {'text': "Verifiez votre zone, il semble y avoir un erreur"}
   response = requests.post(
      webhook_url, data=json.dumps(slack_data),
      headers={'Content-Type': 'application/json'}
      )
   os.remove('zone.log')

# recuperer le fingerprint       
os.system('docker exec nsd ds-records '+ domain + ' >> dnskey.log') 
for line in open("dnskey.log"):
 if "DNSKEY" in line:
   cmd = "grep DNSKEY dnskey.log | cut -d ' ' -f4 | tr '\n' ' '"
   process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
   fingerprint = process.communicate()[0]
   condensat = str(fingerprint.decode())[0:-1]

# Envoi du fingerprint chez gandi
op = api.domain.dnssec.create(apikey, domain, {
"flags": 257,
"algorithm": 14,
"public_key": condensat})
slack_data = {'text': 'La nouvelle cle est bien enregistre chez Gandi' + ' ' +condensat}
response = requests.post(
     webhook_url, data=json.dumps(slack_data),
     headers={'Content-Type': 'application/json'}
      )
os.remove('dnskey.log')

Suppression de l'ancien fingerprint chez gandi 24 heures après la nouvelle rotation des clés


import xmlrpc.client
import json
import requests
import time
import os
import subprocess
import fileinput
import sys

api = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')
webhook_url = 'https://hooks.slack.com/services/TBFxxxxxx7/Bxxxxxxx/FxxxxxxxNImR' # mettre votre webhook_url

apikey = 'xxxxxxxxxxxxxxxW8yrgGE1Qu' # cle de production gandi "https://v4.gandi.net/admin/api_key"

# nom du domaine
cmd = "grep name /mnt/docker/nsd/conf/nsd.conf | cut -d ':' -f2 | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
temp = process.communicate()[0]
domain = str(temp.decode())[1:-1]
extension=".zsk.key"

# recuperation du chemin de la cle KSK
file_path = "/mnt/docker/nsd/zones/K"+domain+extension

# Date du jour - date modification fichier par rapport  a 1970 
# (Nombre d'heures ecoulees depuis le dernier renouvellement des cles)
date_modif = (int(time.time()) - int(os.stat(file_path).st_mtime)) /3600
date = int(date_modif)

# on considere necessaire d'attendre 24 heures 
# pour la propagadion de l'enregistrement dnssec
# avant d effacer le plus ancien

if date > 24:
# Afficher liste fingerprint de gandi
 keys = api.domain.dnssec.list(apikey, domain)
 fichier = open("liste.txt", "w") 
 fichier.write(str(keys))
 fichier.close()

# Afficher les ID du(des) fingerprint(s)
 cmd = "grep id liste.txt | cut --delimiter=, -f7 | cut -d ',' -f1 | cut -d ':' -f2 | cut -d ' ' -f2 | tr '\n' ' '"
 process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
 id = process.communicate()[0]
 a = str(id.decode())[0:-1]
 a = int(a)
 cmd = "grep id liste.txt | cut --delimiter=, -f15 | cut -d ',' -f1 | cut -d ':' -f2 | cut -d ' ' -f2 | tr '\n' ' '"
 process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
 id = process.communicate()[0]
 b = str(id.decode())[0:-1]

# Si le deuxieme enregistrement n'existe pas, on quitte le programme
 if b == '':
  slack_data = {'text': "il n'y a qu'un seul enregistrement"}
  response = requests.post(
      webhook_url, data=json.dumps(slack_data),
      headers={'Content-Type': 'application/json'}
     )
  os.remove('liste.txt')
  sys.exit(0)
 else:
  b = int(b)

# s'il y a plus d'un enregistrement on efface le plus ancien 
# ( par default gandi le place en premier)
  if a > 0 and b > 0:
   slack_data = {'text': "Effacement en cours de l'enregistrement"}
   response = requests.post(
      webhook_url, data=json.dumps(slack_data),
      headers={'Content-Type': 'application/json'}
      )

   op = api.domain.dnssec.delete(apikey, a)
   os.remove('liste.txt')
else:
   slack_data = {'text': "Le delais des 24 heures n'est pas expire"}
   response = requests.post(
      webhook_url, data=json.dumps(slack_data),
      headers={'Content-Type': 'application/json'}
      )
   

J'ai testé le lancement du script avec certbot au renouvellement des certificats en ajoutant le script de la manière suivante

 --post-hook "update-mail-tlsa && python new_dnssec.py"

et ca fonctionne parfaitement, du coup les cles dnssec sont automatiquement renouvellées tous les 3 mois en meme temps que les certificats.
https://mondedie.fr/d/10326-tuto-certificat-wilcard-et-serveur-dns-autoritaire-nsd-dnssec-docker

Pour ceux qui utilisent acme (exemple)

 --reloadcmd      "docker-compose -f /docker/docker-compose.yml restart nginx && /usr/local/bin/update-mail-tlsa && python new_dnssec.py"

https://mondedie.fr/d/10307-certificat-classique-ou-wildcard/19

Pour le script de suppression, il suffit de le mettre en cron toutes les heures par exemple.

5 jours plus tard

Après une série de tests, je me suis aperçu que la rotation des clés se faisait en temps réel. Aucune raison donc d'attendre 24 h avant de supprimer l'ancien fingerprint.

Du coup cela facilite grandement la tache puisque toute la procédure s'effectue dorénavant dans un seul script au lieu de 2. Les commentaires insérés parlent d’eux mêmes quand à la chronologie des étapes.

Tous les tests ont été fait à partir de
http://dnsviz.net/
http://dnssec-debugger.verisignlabs.com/

Voici le script final :

import xmlrpc.client
import sys
import os
import subprocess
import fileinput
import requests
import json

api = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')

apikey = 'xxxxxxxxxxxxxxxxxx' ## cle de production gandi "https://v4.gandi.net/admin/api_key"
webhook_url = 'https://hooks.slack.com/services/xxxxxxxxxxx/xxxxxxxxx/xxxxxxxxxxxxx' ## mettre votre webhook_url

# variable pour le nom de domaine
cmd = "grep name /mnt/docker/nsd/conf/nsd.conf | cut -d ':' -f2 | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
temp = process.communicate()[0]
domain = str(temp.decode())[1:-1]

# Affiche le fingerprint actuel de gandi
keys = api.domain.dnssec.list(apikey, domain)
fichier = open("liste.txt", "w") 
fichier.write(str(keys))
fichier.close()

# Affiche l'ID du fingerprint
cmd = "grep id liste.txt | cut --delimiter=, -f7 | cut -d ',' -f1 | cut -d ':' -f2 | cut -d ' ' -f2 | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
id = process.communicate()[0]
a = str(id.decode())[0:-1]
a = int(a)

# Suppression du fingerprint
op = api.domain.dnssec.delete(apikey, a)
os.remove('liste.txt')

# nouvelles cles
os.system('docker exec nsd keygen '+ domain)

# serial actuel de la zone
for line in open("/mnt/docker/nsd/zones/db."+ domain):
 if "Serial" in line:
  cmd = "grep Serial /mnt/docker/nsd/zones/db."+ domain
  process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  serial = process.communicate()[0]
  serial = str(serial.decode())[1:-10]
  serial = serial.strip ()
  serial = int(serial)

# nouveau serial
cmd = "date -d '+1 day' +'%Y%m%d%H' | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
newserial = process.communicate()[0]
newserial = int(newserial)

# modif serial dans la zone
for line in fileinput.input("/mnt/docker/nsd/zones/db."+ domain, inplace=True):
 print(line.replace(str(serial), str(newserial)), end='')

# date expiration pour la signature dnssec
cmd = "date -d '+6 months' +'%Y%m%d%H%M%S' | tr '\n' ' '"
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
date_expire = process.communicate()[0]
date_expire = int(date_expire)
date_expire = str(date_expire)

# signature de la zone DNS
os.system('docker exec nsd nsd-checkzone '+ domain + ' /zones/db.'+ domain + ' >> zone.log')
for line in open("zone.log"):
 if "ok" in line:
  cmd = "grep ok zone.log"
  process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  result = process.communicate()[0]
  final = str(result.decode())[0:-1]
  if final == 'zone' + ' ' +domain +' ''is ok':
   os.system('docker exec nsd signzone '+domain+' '+date_expire)
   os.remove('zone.log')
  else:
   slack_data = {'text': 'Une erreur est survenue pendant la mise a jour de la zone DNS. Merci de verifier la conformite avec la commande suivante :docker exec nsd nsd-checkzone '+ domain + ' /zones/db.'+ domain}
   response = requests.post(
      webhook_url, data=json.dumps(slack_data),
      headers={'Content-Type': 'application/json'}
      )
   os.remove('zone.log')

# recuperer le nouveau fingerprint       
os.system('docker exec nsd ds-records '+ domain + ' >> dnskey.log') 
for line in open("dnskey.log"):
 if "DNSKEY" in line:
   cmd = "grep DNSKEY dnskey.log | cut -d ' ' -f4 | tr '\n' ' '"
   process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
   fingerprint = process.communicate()[0]
   condensat = str(fingerprint.decode())[0:-1]

# Envoi du fingerprint chez gandi
op = api.domain.dnssec.create(apikey, domain, {
"flags": 257,
"algorithm": 14,
"public_key": condensat})
slack_data = {'text': 'Le nouveau fingerprint est bien enregistre chez Gandi :' + '\n\n' +condensat + '\n\n' + 'La zone DNS a ete mise a jour et signee avec DNSSEC. Merci de verifier la conformite 10 minutes apres la notification avec : http://dnsviz.net/d/'+domain +'/analyze/ et https://dnssec-debugger.verisignlabs.com/'+domain}
response = requests.post(
     webhook_url, data=json.dumps(slack_data),
     headers={'Content-Type': 'application/json'}
      )
os.remove('dnskey.log')

Mise à jour du tuto
https://mondedie.fr/d/10326-tuto-certificat-wilcard-et-serveur-dns-autoritaire-nsd-dnssec-docker

Répondre…