Let's encrypt certificate renewal with Gandi LiveDNS API
It's now one year I use Let's Encrypt TLS wildcard certificates. Until now, all was fine, but since the beginning of 2019, there is two domains on my certificate : soutade.fr and *.soutade.fr and (maybe due to my certificate generation) I need to perform two challenges for renewal : HTTP (http01) and DNS (dns01).
So, I wrote a Python script that performs both :
#!/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/>.
#
# Handle certificate renewal using HTTP and DNS challenges
# DNS challenge performed by Gandi Live v5 API
#
import requests
import os
import argparse
import shutil
# Config
API_KEY = "YOUR-KEY"
LIVEDNS_API = "https://dns.api.gandi.net/api/v5/"
ACME_RECORD = '_acme-challenge'
ACME_CHALLENGE_PATH = '/var/www/.well-known/acme-challenge'
headers = {
'X-Api-Key': API_KEY,
}
CERTBOT_TOKEN = os.environ.get('CERTBOT_TOKEN', None)
CERTBOT_VALIDATION = os.environ.get('CERTBOT_VALIDATION', None)
DOMAIN = os.environ.get('CERTBOT_DOMAIN', None)
optparser = argparse.ArgumentParser(description='Letsencrypt challenge for Gandi v5 API')
optparser.add_argument('-c', '--cleanup', dest='cleanup',
action="store_true", default=False,
help='Cleanup chanllenge')
options = optparser.parse_args()
if options.cleanup:
print('Cleanup')
if os.path.exists(ACME_CHALLENGE_PATH):
shutil.rmtree(ACME_CHALLENGE_PATH)
else:
if CERTBOT_TOKEN and CERTBOT_VALIDATION:
print('Build HTTP authentication')
# Create token file for web server
if not os.path.exists(ACME_CHALLENGE_PATH):
os.makedirs(ACME_CHALLENGE_PATH)
token_path = os.path.join(ACME_CHALLENGE_PATH, CERTBOT_TOKEN)
with open(token_path, 'w') as token:
token.write(CERTBOT_VALIDATION)
exit(0)
response = requests.get(LIVEDNS_API + "zones", headers=headers)
target_zone = None
if (response.ok):
zones = response.json()
for zone in zones:
if zone['name'] == DOMAIN:
target_zone = zone
break
else:
response.raise_for_status()
exit(1)
if not target_zone:
print('Any zone found for domain %s' % (DOMAIN))
exit(1)
domain_records_href = target_zone['zone_records_href']
# Get TXT record
response = requests.get(domain_records_href + "/" + ACME_RECORD, headers=headers)
# Delete record if it exists
if (response.ok):
requests.delete(domain_records_href + "/" + ACME_RECORD, headers=headers)
if options.cleanup:
exit(0)
print('Build DNS authentication')
record = {
"rrset_name": ACME_RECORD,
"rrset_type": "TXT",
"rrset_ttl": 300,
"rrset_values": [CERTBOT_VALIDATION],
}
response = requests.post(domain_records_href,
headers=headers, json=record)
if (response.ok):
print("DNS token created")
else:
print("Something went wrong")
response.raise_for_status()
exit(1)
A downloadable version is available here
Crontab
In /etc/crontab :
0 1 1 * * root certbot renew --manual -n --manual-public-ip-logging-ok --manual-auth-hook /root/gandi_letsencrypt.py --manual-cleanup-hook /root/letsencrypt_token_cleanup.sh
Aditionnals Scripts
Where /root/letsencrypt_token_cleanup.sh is
#!/bin/bash
/root/gandi_letsencrypt.py --cleanup
And in /etc/letsencrypt/renewal-hooks/post/ :
#!/bin/bash
service nginx restart
Errors
If you get a 404 error with nginx, you may add this line to ensure it will not delegate treatment in other part (or send it to another webserver) :
location /.well-known/acme-challenge/ {
}