La toute récente sortie de Pannous a été l'occasion de créer un nouveau sous-domaine pour héberger le service. Service qui comporte une partie d'authentification, donc obligation de passer par une communication sécurisée (SSL/TLS). Autrefois, la chose était plus aisée, puisque je pouvais générer mes propres certificats et notamment des certificats "wildcards", donc valides pour tous les sous-domaines.
Sauf que je suis passé à Let's Encrypt. Il a donc fallu attendre la sortie de la version 2 du protocole (qui a eu du retard) afin de bénéficier de cette fonctionnalité. Surtout, qu'au passage, le paquet Debian (backport) de certbot a été cassé, ce qui m'a forcé à revenir à une version encore plus ancienne.
Bref, les choses sont maintenants stables et déployées sur les serveurs respectifs. Petit problème néanmoins, la génération d'un certificat wildcard par Let's Encrypt requiert l'ajout d'une entrée DNS (comme challenge). Fatalité, le DNS de Gandi a lui aussi évolué pour passer en version 5. Avec pour principal avantage une mise à jour immédiate des entrées DNS (là où il fallait plusieurs minutes/heures auparavant). Autre nouveauté : l'API Gandi change de format. On oublie l'ancien XML-RPC (ce qui était pratique avec des bindings Python déjà tout faits), pour passer au REST (un peu moins formel).
Mélangeons tout ça pour obtenir un joli cocktail, dont la recette nous est donnée par Sébastien Blaisot qui, pour nous simplifier la vie, a créé des scripts de génération de certificats wildcards via Let's Encrypt. Le code est disponible sur GitHub et supporte le logiciel bind (en local), l'API OVH et la nouvelle API Gandi. Il ne reste plus qu'à cloner le dépôt et lancer la commande magique :
cd certbot-dns-01-authenticators/gandi-livedns
certbot certonly --manual --server https://acme-v02.api.letsencrypt.org/directory\
--manual-auth-hook $PWD/auth.py --manual-cleanup-hook $PWD/cleanup.py -d '*.soutade.fr'
Et voilà un joli certificat tout frais !
Du coup, je me suis grandement inspiré de son code pour mettre à jour mon script de DNS fallback (serveur de secours via redirection DNS). Avec, en prime, un passage en Python 3 ! À terme, il faudra que j'ajoute le support IPv6.
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
import json
import re
# Config
domain="soutade.fr"
API_KEY = "YOUR-KEY"
livedns_api = "https://dns.api.gandi.net/api/v5/"
dyndns_url = 'http://checkip.dyndns.com/'
headers = {
'X-Api-Key': API_KEY,
}
A_RECORD_NAME="@" # Record of A type
# Get current IP
current_ip = ''
response = requests.get(dyndns_url)
if response.ok:
pattern = re.compile('[^:]*(\d+\.\d+\.\d+\.\d+)')
result = pattern.search(response.text, 0)
if result == None:
print("No IP found")
exit(1)
else:
current_ip = result.group(0).strip()
else:
print("Connexion error")
response.raise_for_status()
exit(1)
print('Your Current IP is %s' % (current_ip))
# Retrieve domains address
response = requests.get(livedns_api + "domains", headers=headers)
if (response.ok):
domains = response.json()
else:
response.raise_for_status()
exit(1)
domain_index = next((index for (index, d) in enumerate(domains) if d["fqdn"] == domain), None)
if domain_index == None:
# domain not found
print("The requested domain " + domain + " was not found in this gandi account")
exit(1)
domain_records_href = domains[domain_index]["domain_records_href"]
# Get recorded IP
response = requests.get(domain_records_href + "/" + A_RECORD_NAME + "/A", headers=headers)
if (response.ok):
record = response.json()
else:
print("Failed to look for recorded IP")
response.raise_for_status()
exit(1)
print('Old IP : %s' % (record['rrset_values'][0]))
if current_ip != record['rrset_values'][0]:
record['rrset_values'][0] = current_ip
# Put updated IP
response = requests.put(domain_records_href + "/" + A_RECORD_NAME + "/A", headers=headers, json=record)
if (response.ok):
print("IP updated")
else:
print("something went wrong")
response.raise_for_status()
exit(1)
exit(34) # IP updated return !!
exit(0)
Une version téléchargable est disponible ici.