Custom Python Crontab Script with DNS01 Challenge / Cloudflare DNS for Letsencrypt
This was an urgent issue fix - because these TLDs require an SSL Cert already in place
HTTP-01 Challenge is also working over HestiaCP, NOW!
Hint: crontab not required anymore
If u are the lazy admin and user of an "Hosting Control Panel" like HestiaCP you maybe already had the following problem:
- Issue
Error: Let's Encrypt finalize bad status 400
This happens, because HestiaCP supports only the HTTP ACME Challenge - but for Domains where SSL is enforced, the challenge can not be done! To get SSL Letsencrypt Certificates I have to build my own solution, which supports DNS01 at Cloudflare DNS.
Solution: Build up own python crontab script, which supports certbot and DNS01
- Requirements - Python3 with the following PIP Packages installed over distribution (or manual):
sudo apt-get install certbot python3-certbot-dns-cloudflare
-
Python SSL Certbot Task Script with HestiaCP Integration
/usr/local/bin/hestiacp_dns01_sslcert.py
#!/usr/bin/env python3
import os
import subprocess
from datetime import datetime, timedelta
from certbot.plugins import dns_common
# --- Configuration ---
domains = ["firstdomain.dev", "secondgoogledomain.blog"]
user = "username"
cloudflare_credentials_file = "/home/username/.secrets/certbot/certbot_cloudflare_key.ini"
log_file = "/var/log/hestiacp_DNS01_cert_renewal.log"
# --- Functions ---
def check_expiry(domain):
"""Checks the expiration date of the certificate."""
try:
result = subprocess.run(["sudo", "certbot", "certificates", "--cert-name", domain], capture_output=True, text=True)
for line in result.stdout.splitlines():
if line.startswith(" NOT AFTER :"):
expiry_date_str = line.split(": ")[1]
expiry_date = datetime.strptime(expiry_date_str, "%Y-%m-%d %H:%M:%S")
return (expiry_date - datetime.now()).days
except Exception as e: # Catch all exceptions here, including FileNotFoundError
log_error(f"Error checking expiry for {domain}: {e}") # Log the error
return None # Return None to indicate an error
def renew_certificate(domain):
"""Obtains/renews a certificate and updates HestiaCP configuration."""
# Remove the line: dns_common.Authenticator._setup_credentials = lambda self: None
try:
cert_path = f"/etc/letsencrypt/live/{domain}/cert.pem"
command = ["sudo", "certbot", "renew" if os.path.exists(cert_path) else "certonly",
"--dns-cloudflare", "--dns-cloudflare-credentials", cloudflare_credentials_file,
"-d", domain, "--non-interactive", "--keep-until-expiring"]
result = subprocess.run(command, capture_output=True, text=True)
if "SSL=yes already exists" in result.stderr:
update_hestia_config(domain, exists=True)
log_message(f"Certificate for {domain} already exists, updating HestiaCP configuration.")
else:
if "Certificate not yet due for renewal" in result.stdout:
log_message(f"Certificate for {domain} is not due for renewal yet.")
else:
log_message(f"Certificate for {domain} {'renewed' if os.path.exists(cert_path) else 'obtained'} successfully.")
update_hestia_config(domain, exists=False)
except subprocess.CalledProcessError as e:
error_message = e.stderr.decode('utf-8')
log_error(f"Error obtaining/renewing certificate for {domain}: {error_message}")
except Exception as e:
log_error(f"Error obtaining/renewing certificate for {domain}: {e}")
def update_hestia_config(domain, exists=False):
"""Updates or creates the SSL configuration for the domain in HestiaCP."""
cert_path = f"/etc/letsencrypt/live/{domain}"
ssl_dir = f"/usr/local/hestia/data/users/{user}/ssl"
if exists:
command = f"sudo v-update-web-domain-ssl{user} {domain} {ssl_dir}/"
else:
command = f"sudo v-add-web-domain-ssl {user} {domain} {ssl_dir}/"
os.system(command)
# Restart Nginx (or your web server) to apply the changes
os.system("sudo v-restart-web")
def log_message(message):
"""Logs a message to the log file."""
with open(log_file, "a") as f:
f.write(f"{datetime.now()} - {message}\n")
def log_error(message):
"""Logs an error message to the log file."""
log_message(f"ERROR: {message}")
# --- Main Script ---
if __name__ == "__main__":
for domain in domains:
days_remaining = check_expiry(domain)
if days_remaining is None or days_remaining < 30:
renew_certificate(domain)
else:
log_message(f"Certificate for {domain} is still valid (expires in {days_remaining} days).")
- Install as Cronjob of HestiaCP User
0 2 * * * /usr/bin/python3 /usr/local/bin/hestiacp_dns01_sslcert.py >> /var/log/hestiacp_DNS01_cert_renewal.log 2>&1
-
Set Permission for Certbot
Simply add the cloudflare credential data to the /home/username/.secrets/certbot/certbot_cloudflare_key.ini (chmod 600!) of the hestiacp User!
# Cloudflare API Token(!) credentials used by Certbot
dns_cloudflare_api_token = YOUR_API_TOKEN
Important: This can be done more safe - e.g. using a Vault for the secret data [ToBeDone]
Use visudo to add this sudo-permission:
username ALL=(ALL) NOPASSWD: /usr/bin/certbot
username ALL=(ALL) NOPASSWD: /usr/local/hestia/bin/v-update-web-domain-ssl
username ALL=(ALL) NOPASSWD: /usr/local/hestia/bin/v-add-web-domain-ssl
username ALL=(ALL) NOPASSWD: /usr/local/hestia/bin/v-restart-web
NOTE: Replace username with your hestiacp username
Thats iT! You have now a final solution for DNS01 Challenge Support at HestiaCP.