Skip to content

Instantly share code, notes, and snippets.

@kpirnie
Created September 5, 2025 13:10
Show Gist options
  • Save kpirnie/129c8e6905724f696fbe71d2433ea743 to your computer and use it in GitHub Desktop.
Save kpirnie/129c8e6905724f696fbe71d2433ea743 to your computer and use it in GitHub Desktop.
CloudPanel.io Renew LE Certificates if the lets-encrypt:renew:certificates does not exist for you.
#!/usr/bin/env python3
import os
import re
import subprocess
import glob
from datetime import datetime, timedelta
from pathlib import Path
VHOSTS_DIR = "/etc/nginx/sites-enabled"
RENEW_DAYS_BEFORE_EXPIRATION = 7
def extract_server_names(config_file):
"""Extract server_name directive from nginx config file."""
try:
with open(config_file, 'r') as f:
content = f.read()
# Find server blocks and extract server_name
server_block_pattern = r'server\s*\{([^}]*)\}'
server_blocks = re.findall(server_block_pattern, content, re.DOTALL)
for block in server_blocks:
# Look for server_name directive
server_name_match = re.search(r'^\s*server_name\s+([^;]+);', block, re.MULTILINE)
if server_name_match:
# Split server names and clean them up
server_names = server_name_match.group(1).split()
return [name.strip() for name in server_names if name.strip()]
return []
except Exception as e:
print(f" [Error] Failed to parse config file: {e}")
return []
def extract_ssl_cert_path(config_file):
"""Extract ssl_certificate path from nginx config file."""
try:
with open(config_file, 'r') as f:
content = f.read()
# Find server blocks and extract ssl_certificate
server_block_pattern = r'server\s*\{([^}]*)\}'
server_blocks = re.findall(server_block_pattern, content, re.DOTALL)
for block in server_blocks:
# Look for ssl_certificate directive (but not template variables)
ssl_cert_match = re.search(r'^\s*ssl_certificate\s+([^;]+);', block, re.MULTILINE)
if ssl_cert_match and '{{ssl_certificate}}' not in ssl_cert_match.group(1):
return ssl_cert_match.group(1).strip()
return None
except Exception as e:
print(f" [Error] Failed to parse SSL cert path: {e}")
return None
def get_certificate_issuer(cert_path):
"""Get certificate issuer for debugging."""
try:
result = subprocess.run(
['openssl', 'x509', '-in', cert_path, '-noout', '-issuer'],
capture_output=True, text=True, check=True
)
return result.stdout.strip()
except:
return "Unknown"
def is_lets_encrypt_cert(cert_path):
"""Check if certificate is from Let's Encrypt."""
issuer = get_certificate_issuer(cert_path)
return "Let's Encrypt" in issuer
def get_cert_expiration_info(cert_path):
"""Get certificate expiration information."""
try:
result = subprocess.run(
['openssl', 'x509', '-in', cert_path, '-noout', '-enddate'],
capture_output=True, text=True, check=True
)
# Extract date from output (format: notAfter=MMM DD HH:MM:SS YYYY GMT)
enddate_line = result.stdout.strip()
date_str = enddate_line.split('=')[1]
# Parse the date
expiration_date = datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')
# Calculate days until expiration
days_until_expiration = (expiration_date - datetime.now()).days
return date_str, days_until_expiration
except Exception as e:
raise Exception(f"Error parsing certificate: {e}")
def check_certificate_needs_renewal(cert_path):
"""Check if certificate needs renewal - returns (needs_renewal, reason)."""
if not cert_path or not os.path.isfile(cert_path):
return True, "Certificate file does not exist. Needs initial installation."
try:
# Get certificate info
date_str, days_until_expiration = get_cert_expiration_info(cert_path)
print(f" Certificate expires on: {date_str} ({days_until_expiration} days from now)")
# Check if this is a Let's Encrypt certificate
if not is_lets_encrypt_cert(cert_path):
return True, "Certificate is self-signed or not from Let's Encrypt. Needs Let's Encrypt installation."
# For Let's Encrypt certs, check expiration
if days_until_expiration <= RENEW_DAYS_BEFORE_EXPIRATION:
return True, f"Certificate expires in {days_until_expiration} days (<= {RENEW_DAYS_BEFORE_EXPIRATION})."
else:
print(f" [Skip] Certificate does not need renewal (expires in more than {RENEW_DAYS_BEFORE_EXPIRATION} days).")
return False, ""
except Exception as e:
return True, f"Error checking certificate: {e}"
def main():
print(f"Starting SSL renewal process by parsing NGINX vhosts in {VHOSTS_DIR}")
print("------------------------------------------------------------------")
# Find all .conf files, ignore default.conf and custom-domain.conf
config_files = glob.glob(os.path.join(VHOSTS_DIR, "*.conf"))
config_files = [f for f in config_files if not re.search(r'(default|custom-domain)\.conf$', f)]
for config_file in config_files:
# Extract primary domain from filename (e.g., domain.ext.conf -> domain.ext)
primary_domain = Path(config_file).stem
print(f"[Processing] Domain: {primary_domain}")
# Extract server names from config
server_names = extract_server_names(config_file)
if not server_names:
print(" [Warning] Could not find a valid server_name directive. Skipping.")
continue
# Extract SSL certificate path
ssl_cert_path = extract_ssl_cert_path(config_file)
# Show certificate info if it exists
if ssl_cert_path and os.path.isfile(ssl_cert_path):
print(f" Certificate found at: {ssl_cert_path}")
issuer = get_certificate_issuer(ssl_cert_path)
print(f" Certificate issuer: {issuer}")
# Check if certificate needs renewal
needs_renewal, renewal_reason = check_certificate_needs_renewal(ssl_cert_path)
if not needs_renewal:
print("------------------------------------------------------------------")
continue
if renewal_reason:
print(f" [Action] {renewal_reason}")
# Build command: primary domain is always the filename
# Remove primary domain from server_names if it exists
san_domains = [name for name in server_names if name != primary_domain]
if san_domains:
sans_string = ','.join(san_domains)
command = f"sudo clpctl lets-encrypt:install:certificate --domainName={primary_domain} --subjectAlternativeName={sans_string}"
else:
command = f"sudo clpctl lets-encrypt:install:certificate --domainName={primary_domain}"
# Execute the renewal/installation command (echo only for now)
print(f" [Executing] {command}")
print(" --- Output Start ---")
# print(command) # Commented out for debugging
result = subprocess.run(command, shell=True, capture_output=True, text=True)
print(result.stdout)
if result.stderr:
print(result.stderr)
exit_code = result.returncode
print(" --- Output End ---")
print(f" Command exit code: {exit_code}")
print("------------------------------------------------------------------")
print("Renewal process complete.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment