Created
September 5, 2025 13:10
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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