diff --git a/certpusher.py b/certpusher.py index 34c5ed9..fb752f2 100644 --- a/certpusher.py +++ b/certpusher.py @@ -2,7 +2,7 @@ """ CertPusher - Automated SSL Certificate Distribution Tool Distributes SSL certificates to remote servers via SSH/SCP -Supports standard Linux servers and MikroTik RouterOS devices +Supports standard Linux servers, MikroTik RouterOS, and Proxmox VE """ import configparser @@ -11,6 +11,7 @@ import sys import os import ssl import socket +import tempfile from datetime import datetime, timezone from pathlib import Path from typing import Dict, Optional, Tuple @@ -119,6 +120,31 @@ Certificate Info: """ except Exception as e: return f"Unable to extract certificate info: {e}" + + @staticmethod + def create_combined_cert(cert_path: str, key_path: str, output_path: str) -> bool: + """Create combined certificate file (cert + key) - used by Proxmox""" + try: + logger.debug(f"Creating combined certificate: {cert_path} + {key_path} -> {output_path}") + + with open(cert_path, 'r') as cert_file: + cert_content = cert_file.read() + + with open(key_path, 'r') as key_file: + key_content = key_file.read() + + # Combined format: certificate + private key + combined_content = cert_content.strip() + "\n" + key_content.strip() + "\n" + + with open(output_path, 'w') as combined_file: + combined_file.write(combined_content) + + logger.info(f"✓ Combined certificate created at {output_path}") + return True + + except Exception as e: + logger.error(f"Failed to create combined certificate: {e}") + return False class SSHManager: @@ -304,10 +330,8 @@ class MikroTikManager(SSHManager): logger.debug(f"Certificates after import:\n{stdout}") # Step 7: Configure services to use new certificate - # RouterOS typically names imported certs as certname_0, certname_1, etc. logger.info("Configuring www-ssl service to use new certificate") - # Find the certificate name (usually ssl-cert_0 for cert, ssl-cert_1 for key) config_commands = [ f'/ip service set www-ssl certificate={self.cert_name}_0', '/ip service enable www-ssl', @@ -343,6 +367,71 @@ class MikroTikManager(SSHManager): return False +class ProxmoxManager(SSHManager): + """Specialized manager for Proxmox VE servers""" + + def upload_certificate(self, cert_path: str, key_path: str) -> bool: + """ + Upload certificate to Proxmox VE + + Proxmox uses two separate files: + - /etc/pve/local/pveproxy-ssl.pem (certificate) + - /etc/pve/local/pveproxy-ssl.key (private key) + + Args: + cert_path: Path to certificate file (fullchain) + key_path: Path to private key file + """ + try: + logger.info(f"Starting Proxmox certificate deployment to {self.hostname}") + + # Step 1: Upload certificate + logger.debug("Uploading certificate to Proxmox") + if not self.upload_file(cert_path, '/etc/pve/local/pveproxy-ssl.pem'): + return False + + # Step 2: Upload private key + logger.debug("Uploading private key to Proxmox") + if not self.upload_file(key_path, '/etc/pve/local/pveproxy-ssl.key'): + return False + + # Step 3: Set correct permissions + logger.debug("Setting file permissions") + commands = [ + 'chmod 640 /etc/pve/local/pveproxy-ssl.key', + 'chown root:www-data /etc/pve/local/pveproxy-ssl.key', + ] + + for cmd in commands: + self.execute_command(cmd, ignore_error=False) + + # Step 4: Restart pveproxy + logger.info("Restarting pveproxy service") + success, stdout, stderr = self.execute_command('systemctl restart pveproxy', timeout=30) + + if not success: + logger.error(f"Failed to restart pveproxy: {stderr}") + return False + + # Step 5: Verify service is running + import time + time.sleep(3) + + success, stdout, stderr = self.execute_command('systemctl is-active pveproxy') + if success and 'active' in stdout: + logger.info(f"✓ Successfully deployed certificate to Proxmox {self.hostname}") + return True + else: + logger.error("pveproxy service is not active after restart") + # Show journal logs for debugging + self.execute_command('journalctl -u pveproxy -n 20 --no-pager') + return False + + except Exception as e: + logger.error(f"Proxmox certificate deployment failed: {e}", exc_info=True) + return False + + class CertPusher: """Main application class""" @@ -422,6 +511,49 @@ class CertPusher: self.stats['failed'] += 1 return False + def process_proxmox(self, section: str, hostname: str, port: int, + username: str, ssh_key: str, source_cert_path: str) -> bool: + """Process Proxmox VE server specifically""" + try: + logger.info("Using Proxmox-specific deployment method") + + # Get private key path + if self.config.has_option(section, 'source_key_path'): + source_key_path = self.config.get(section, 'source_key_path') + else: + # Try to derive from cert path + source_key_path = source_cert_path.replace('fullchain.pem', 'privkey.pem') + + logger.info(f"Certificate: {source_cert_path}") + logger.info(f"Private key: {source_key_path}") + + if not os.path.exists(source_key_path): + logger.error(f"Private key file not found: {source_key_path}") + return False + + # Connect + proxmox = ProxmoxManager(hostname, port, username, ssh_key) + + if not proxmox.connect(): + self.stats['failed'] += 1 + return False + + # Upload certificate + if not proxmox.upload_certificate(source_cert_path, source_key_path): + proxmox.disconnect() + self.stats['failed'] += 1 + return False + + proxmox.disconnect() + self.stats['uploaded'] += 1 + logger.info(f"✓ Successfully processed Proxmox {section}") + return True + + except Exception as e: + logger.error(f"Proxmox processing failed: {e}", exc_info=True) + self.stats['failed'] += 1 + return False + def process_host(self, section: str) -> bool: """Process certificate deployment for a single host""" try: @@ -443,16 +575,30 @@ class CertPusher: else: ssh_key = self.config.get('global', 'default_ssh_key') - source_cert_path = self.config.get('global', 'source_cert_path') + # Allow per-host certificate override + if self.config.has_option(section, 'source_cert_path'): + source_cert_path = self.config.get(section, 'source_cert_path') + logger.info(f"Using host-specific certificate: {source_cert_path}") + else: + source_cert_path = self.config.get('global', 'source_cert_path') + logger.debug(f"Using global certificate: {source_cert_path}") + + # Verify certificate exists + if not os.path.exists(source_cert_path): + logger.error(f"Certificate file not found: {source_cert_path}") + self.stats['failed'] += 1 + return False logger.info(f"Host: {hostname}:{port}") logger.info(f"Type: {device_type}") logger.info(f"Username: {username}") logger.info(f"SSH Key: {ssh_key}") - # Handle MikroTik devices specially + # Handle device-specific deployments if device_type.lower() == 'mikrotik': return self.process_mikrotik(section, hostname, port, username, ssh_key, source_cert_path) + elif device_type.lower() == 'proxmox': + return self.process_proxmox(section, hostname, port, username, ssh_key, source_cert_path) # Standard processing for other devices remote_cert_path = self.config.get(section, 'remote_cert_path') diff --git a/config.ini.example b/config.ini.example index dae5d95..2951b55 100644 --- a/config.ini.example +++ b/config.ini.example @@ -24,39 +24,24 @@ port = 22 username = admin source_key_path = /etc/letsencrypt/live/example.com/privkey.pem -# ==================== PROXMOX HOSTS ==================== +# ==================== PROXMOX SERVERS ==================== -[proxmox_node1] -type = standard -hostname = pve1.example.com -port = 22 +[proxmox1] +type = proxmox +hostname = 10.87.2.150 +port = 11922 username = root -# Proxmox uses /etc/pve/local/ which is a symlink to /etc/pve/nodes/NODENAME/ -remote_cert_path = /etc/pve/local/pveproxy-ssl.pem -additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/pve/local/pveproxy-ssl.key -post_upload_command = systemctl restart pveproxy -check_url = https://pve1.example.com:8006 +# For Proxmox, source_key_path can be auto-derived or specified +source_key_path = /etc/letsencrypt/live/npm-3/privkey.pem +check_url = https://10.87.2.150:8006 -[proxmox_node2] -type = standard -hostname = 10.0.0.51 -port = 22 +[proxmox2] +type = proxmox +hostname = 10.87.2.151 +port = 11922 username = root -remote_cert_path = /etc/pve/local/pveproxy-ssl.pem -additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/pve/local/pveproxy-ssl.key -post_upload_command = systemctl restart pveproxy -check_url = https://10.0.0.51:8006 - -[proxmox_cluster_node] -type = standard -hostname = pve-cluster.local -port = 22 -username = root -# For clustered Proxmox, certificate is stored per-node -remote_cert_path = /etc/pve/nodes/pve-cluster/pveproxy-ssl.pem -additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/pve/nodes/pve-cluster/pveproxy-ssl.key -post_upload_command = chmod 600 /etc/pve/nodes/pve-cluster/pveproxy-ssl.key && systemctl restart pveproxy -check_url = https://pve-cluster.local:8006 +source_key_path = /etc/letsencrypt/live/npm-3/privkey.pem +check_url = https://10.87.2.151:8006 # ==================== HOME ASSISTANT ==================== @@ -197,3 +182,75 @@ remote_cert_path = /usr/syno/etc/certificate/system/default/fullchain.pem additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/usr/syno/etc/certificate/system/default/privkey.pem post_upload_command = /usr/syno/sbin/synoservicectl --reload nginx check_url = https://synology.local:5001 + + +# ==================== MAIL SERVER WITH CUSTOM CERTIFICATE ==================== +# This server uses mail.company.com certificate + +[mailserver] +type = standard +hostname = mail.company.com +port = 22 +username = root +# Override: use mail-specific certificate +source_cert_path = /etc/letsencrypt/live/mail.company.com/fullchain.pem +remote_cert_path = /etc/postfix/ssl/cert.pem +additional_files = /etc/letsencrypt/live/mail.company.com/privkey.pem:/etc/postfix/ssl/privkey.pem +post_upload_command = systemctl restart postfix && systemctl restart dovecot +check_url = https://mail.company.com:465 + +# ==================== SUBDOMAIN WITH CUSTOM CERTIFICATE ==================== +# This server uses subdomain.org certificate + +[api_server] +type = standard +hostname = 192.168.1.200 +port = 22 +username = ubuntu +ssh_key_path = /root/.ssh/api_key +# Override: use api-specific certificate +source_cert_path = /etc/letsencrypt/live/api.subdomain.org/fullchain.pem +remote_cert_path = /etc/nginx/ssl/api/fullchain.pem +additional_files = /etc/letsencrypt/live/api.subdomain.org/privkey.pem:/etc/nginx/ssl/api/privkey.pem +post_upload_command = systemctl reload nginx +check_url = https://api.subdomain.org + +# ==================== CLIENT SITE WITH CUSTOM CERTIFICATE ==================== +# Client's own domain and certificate + +[client_website] +type = standard +hostname = 203.0.113.50 +port = 2222 +username = admin +# Override: use client-specific certificate +source_cert_path = /etc/letsencrypt/live/client-domain.com/fullchain.pem +remote_cert_path = /var/www/ssl/fullchain.pem +additional_files = /etc/letsencrypt/live/client-domain.com/privkey.pem:/var/www/ssl/privkey.pem +post_upload_command = systemctl reload apache2 +check_url = https://www.client-domain.com + +[client_mikrotik] +type = mikrotik +hostname = 203.0.113.51 +port = 22 +username = admin +ssh_key_path = /root/.ssh/client_key +# Override: use client-specific certificate +source_cert_path = /etc/letsencrypt/live/client-domain.com/fullchain.pem +source_key_path = /etc/letsencrypt/live/client-domain.com/privkey.pem + +# ==================== DEVELOPMENT SERVER ==================== +# Dev server with staging certificate + +[dev_server] +type = standard +hostname = dev.local +port = 22 +username = developer +# Override: use staging certificate for testing +source_cert_path = /etc/letsencrypt-staging/live/dev.example.com/fullchain.pem +remote_cert_path = /opt/app/ssl/fullchain.pem +additional_files = /etc/letsencrypt-staging/live/dev.example.com/privkey.pem:/opt/app/ssl/privkey.pem +post_upload_command = docker-compose restart nginx +# No check_url - always upload to dev \ No newline at end of file