certificat Let’s Encrypt subdomain DNS Gandi.net

Sécuriser son site web avec un certificat Let’s Encrypt c’est bien, mais lorsque l’on a des sous domaines, ou besoin de faire une activation par DNS, les choses peuvent se compliquer légèrement.

On part du principe que vous avez un nom-de-domaine (dans notre exemple, on utilisera domain.net) chez un gestionnaire de domaine (Gandi.net, OVH…).

De préférence, choisissez-en un avec une API pour faire les requêtes de manière automatiques.

La demande d’un nouveau certificat se fait via certbot comme d’habitude, ce sont les options nécessaires qui vont changer dans notre cas.

Notre fichier cert-req.sh va contenir

#!/usr/bin/env bash

certbot certonly \
    --agree-tos \
    --rsa-key-size 4096 \
    --email mymail@mydomain.net \
    --no-eff-email \
    --server https://acme-v02.api.letsencrypt.org/directory \
    --preferred-challenges dns \
    --manual-public-ip-logging-ok \
    --manual \
    --manual-auth-hook /path/addDomain.py \
    --manual-cleanup-hook /path/cleanDomain.py \
    -d '*.domain.net' \
    -d 'domain.net'

On va demander un certificat standard ET un certificat wildcard (incluant tous les sous domaines) pour le domaine domain.net

Ce type de certificat ne peut s’obtenir qu’en faisant une validation par DNS. Ce qui peut également fonctionner si le certificat doit être utilisé pour d’autres applications, difficilement accessibles via l’extérieur (NAS, hôte ESXi…)

On demande donc un certificat « manuellement » (via l’option –manual), et on donne 2 options avec des fichiers python :

  • manual-auth-hook
  • manual-cleanup-hook

Ces 2 fichiers vont créer les clés nécessaires chez votre prestataire de domaine afin que les serveurs de Let’s Encrypt puissent générer le certificat.

Le contenu du fichier addDomain.py

#!/usr/bin/env python3

import json
import os
import time

# Get CERTBOT informations
domain = os.environ['CERTBOT_DOMAIN']
dtoken = "\"" +  os.environ['CERTBOT_VALIDATION'] + "\""

# Get domain (array), baseDomain and subDomain
domain = domain.split(".")
bdomain = domain[len(domain)-2] + "." + domain[len(domain)-1]
sdomain = "_acme-challenge"

# We concat if we have some subdomains
if len(domain) > 2:
    sdomain += "."
    for i in range(0, len(domain)-2):
        if i == len(domain)-3:
            sdomain += domain[i]
        else:
            sdomain += domain[i] + "."

# Work only for Gandi.net
import gandi

# Get the current sub records
record = ""
r = getDomainSRecords(bdomain, sdomain)

try:
  record = r[0]['rrset_values'][0]
except:
  pass

# We need to clean the json
record = record.replace('"', '')

ttlTime = 300
data = {}
item = {}

item['rrset_type'] = "TXT"
item["rrset_ttl"] = ttlTime

# If the record doesn't exist, we create it
if record == "":
  item["rrset_values"] = [dtoken]
  data['items'] = [item]

# Else, we add the new informations
if record != "":
  item["rrset_values"] = [record, dtoken]
  data['items'] = [item]

# We write the informations on the Gandi.net API
w = putDomainRecord(bdomain, sdomain, "", data)

 # We wait TTL time
time.sleep(ttlTime)

Le contenu du fichier cleanDomain.py

#!/usr/bin/env python3

import json
import os
import time

# Get CERTBOT informations
domain = os.environ['CERTBOT_DOMAIN']
dtoken = "\"" +  os.environ['CERTBOT_VALIDATION'] + "\""

# Get domain (array), baseDomain and subDomain
domain = domain.split(".")
bdomain = domain[len(domain)-2] + "." + domain[len(domain)-1]
sdomain = "_acme-challenge"

# We concat if we have some subdomains
if len(domain) > 2:
    sdomain += "."
    for i in range(0, len(domain)-2):
        if i == len(domain)-3:
            sdomain += domain[i]
        else:
            sdomain += domain[i] + "."

# Work only for Gandi.net
import gandi

try:
  d = deleteDomainRecords(bdomain, sdomain)
except:
  pass

Le contenu du fichier gandi.py (n’oubliez pas de changer la partie X-Api-Key)

#!/usr/bin/env python3

import json
import requests

def reqGandi(method, url, payload = None):
  headers = {}
  headers['X-Api-Key'] = 'abcDEFghiJKLmnoPQRstuVWXyz'
  headers['Content-Type'] = "application/json"

  if method == "get":
    return requests.get(url, headers=headers).json()

  if method == "put":
    return requests.put(url, headers=headers, data=json.dumps(payload)).json()

def getDomainRecords(domain):
  url = "https://dns.api.gandi.net/api/v5/domains/" + domain + "/records"
  return reqGandi("get", url)

def getDomainSRecords(domain, subdomain):
  url = "https://dns.api.gandi.net/api/v5/domains/" + domain + "/records/" + subdomain
  return reqGandi("get", url)

def putDomainRecord(domain, record, type, data):
  url = "https://dns.api.gandi.net/api/v5/domains/" + domain + "/records/" + record
  return reqGandi("put", url, data)

Il ne reste plus qu’à exécuter le script cert-req.sh et normalement vous obtenez un certificat validé par DNS.

Note :

Certbot va générer et contrôler autant de clé qu’il n’y a de domaines / sous domaines intégrés au certificats. Il faudra donc être patient.

Par exemple, pour Gandi.net, avec un temps de propagation de 5 minutes (300s) sur le LiveDNS, une demande de certificat wildcard dure 10 minutes avant l’obtention du certificat.