From 4fc21ff6357968047d4cf537fd37c62f8d928e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 26 Oct 2025 23:07:10 +0100 Subject: [PATCH] options --- certpusher.py | 321 +++++++++++++++++++++++++++++++++++++++++---- config.ini.example | 191 ++++++++++++++++++++++++--- 2 files changed, 469 insertions(+), 43 deletions(-) diff --git a/certpusher.py b/certpusher.py index 69e2f90..1bf41d0 100644 --- a/certpusher.py +++ b/certpusher.py @@ -2,6 +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 """ import configparser @@ -43,6 +44,7 @@ class CertificateManager: cert_data = f.read() cert = x509.load_pem_x509_certificate(cert_data, default_backend()) logger.debug(f"Loaded certificate from {cert_path}") + logger.debug(f"Certificate subject: {cert.subject}") logger.debug(f"Certificate expires: {cert.not_valid_after}") return cert except Exception as e: @@ -62,6 +64,9 @@ class CertificateManager: logger.debug(f"Connecting to {hostname}:{port} to retrieve certificate") context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket.create_connection((hostname, port), timeout=timeout) as sock: with context.wrap_socket(sock, server_hostname=hostname) as ssock: der_cert = ssock.getpeercert(binary_form=True) @@ -78,14 +83,38 @@ class CertificateManager: """Compare two certificates by serial number and fingerprint""" try: same_serial = cert1.serial_number == cert2.serial_number - same_fingerprint = cert1.fingerprint(cert1.signature_hash_algorithm) == \ - cert2.fingerprint(cert2.signature_hash_algorithm) + + # Compare fingerprints + from cryptography.hazmat.primitives import hashes + fingerprint1 = cert1.fingerprint(hashes.SHA256()) + fingerprint2 = cert2.fingerprint(hashes.SHA256()) + same_fingerprint = fingerprint1 == fingerprint2 logger.debug(f"Certificate comparison - Serial match: {same_serial}, Fingerprint match: {same_fingerprint}") return same_serial and same_fingerprint except Exception as e: logger.error(f"Failed to compare certificates: {e}") return False + + @staticmethod + def get_certificate_info(cert: x509.Certificate) -> str: + """Get human-readable certificate information""" + try: + subject = cert.subject.rfc4514_string() + issuer = cert.issuer.rfc4514_string() + valid_from = cert.not_valid_before + valid_to = cert.not_valid_after + + return f""" +Certificate Info: + Subject: {subject} + Issuer: {issuer} + Valid From: {valid_from} + Valid To: {valid_to} + Days Until Expiry: {(valid_to - datetime.now()).days} +""" + except Exception as e: + return f"Unable to extract certificate info: {e}" class SSHManager: @@ -107,7 +136,27 @@ class SSHManager: logger.debug(f"Connecting to {self.username}@{self.hostname}:{self.port}") logger.debug(f"Using SSH key: {self.key_path}") - private_key = paramiko.RSAKey.from_private_key_file(self.key_path) + # Try to load different key types + private_key = None + key_types = [ + ('RSA', paramiko.RSAKey), + ('Ed25519', paramiko.Ed25519Key), + ('ECDSA', paramiko.ECDSAKey), + ('DSS', paramiko.DSSKey), + ] + + for key_name, key_class in key_types: + try: + private_key = key_class.from_private_key_file(self.key_path) + logger.debug(f"Successfully loaded {key_name} key") + break + except Exception as e: + logger.debug(f"Not a {key_name} key: {e}") + continue + + if not private_key: + logger.error(f"Could not load SSH key from {self.key_path}") + return False self.ssh_client.connect( hostname=self.hostname, @@ -115,10 +164,11 @@ class SSHManager: username=self.username, pkey=private_key, timeout=30, - banner_timeout=30 + banner_timeout=30, + auth_timeout=30 ) - logger.info(f"Successfully connected to {self.hostname}:{self.port}") + logger.info(f"✓ Successfully connected to {self.hostname}:{self.port}") return True except Exception as e: @@ -130,33 +180,42 @@ class SSHManager: try: logger.debug(f"Uploading {local_path} to {self.hostname}:{remote_path}") + # Ensure remote directory exists + remote_dir = os.path.dirname(remote_path) + if remote_dir: + self.execute_command(f"mkdir -p {remote_dir}", ignore_error=True) + with SCPClient(self.ssh_client.get_transport()) as scp: scp.put(local_path, remote_path) - logger.info(f"Successfully uploaded {local_path} to {self.hostname}:{remote_path}") + logger.info(f"✓ Successfully uploaded {local_path} to {self.hostname}:{remote_path}") return True except Exception as e: logger.error(f"File upload failed: {e}") return False - def execute_command(self, command: str) -> Tuple[bool, str, str]: + def execute_command(self, command: str, timeout: int = 60, ignore_error: bool = False) -> Tuple[bool, str, str]: """Execute command on remote server""" try: logger.debug(f"Executing command on {self.hostname}: {command}") - stdin, stdout, stderr = self.ssh_client.exec_command(command, timeout=60) + stdin, stdout, stderr = self.ssh_client.exec_command(command, timeout=timeout) exit_status = stdout.channel.recv_exit_status() - stdout_text = stdout.read().decode('utf-8') - stderr_text = stderr.read().decode('utf-8') + stdout_text = stdout.read().decode('utf-8', errors='ignore') + stderr_text = stderr.read().decode('utf-8', errors='ignore') if exit_status == 0: - logger.info(f"Command executed successfully on {self.hostname}") - logger.debug(f"STDOUT: {stdout_text}") + logger.info(f"✓ Command executed successfully on {self.hostname}") + if stdout_text: + logger.debug(f"STDOUT: {stdout_text}") else: - logger.error(f"Command failed with exit code {exit_status}") - logger.error(f"STDERR: {stderr_text}") + if not ignore_error: + logger.error(f"Command failed with exit code {exit_status}") + logger.error(f"STDERR: {stderr_text}") + else: + logger.debug(f"Command failed (ignored): {stderr_text}") return exit_status == 0, stdout_text, stderr_text @@ -171,6 +230,116 @@ class SSHManager: logger.debug(f"Disconnected from {self.hostname}") +class MikroTikManager(SSHManager): + """Specialized manager for MikroTik RouterOS devices""" + + def __init__(self, hostname: str, port: int, username: str, key_path: str): + super().__init__(hostname, port, username, key_path) + self.cert_name = "ssl-cert" + self.key_name = "ssl-key" + + def upload_certificate(self, cert_path: str, key_path: str = None) -> bool: + """ + Upload and import certificate to MikroTik RouterOS + + Args: + cert_path: Path to certificate file (PEM format, can be fullchain) + key_path: Optional path to private key file + """ + try: + logger.info(f"Starting MikroTik certificate deployment to {self.hostname}") + + # Step 1: Disable www-ssl service + logger.debug("Disabling www-ssl service") + self.execute_command('/ip service disable www-ssl', ignore_error=True) + + # Step 2: Remove old certificates + logger.debug("Removing old certificates") + cleanup_commands = [ + f'/certificate remove [find name~"{self.cert_name}"]', + f'/file remove "{self.cert_name}.pem"', + ] + + if key_path: + cleanup_commands.append(f'/file remove "{self.key_name}.pem"') + + for cmd in cleanup_commands: + self.execute_command(cmd, ignore_error=True) + + # Step 3: Upload certificate file + logger.debug(f"Uploading certificate to MikroTik") + with SCPClient(self.ssh_client.get_transport()) as scp: + scp.put(cert_path, f'{self.cert_name}.pem') + logger.info(f"✓ Certificate file uploaded") + + # Step 4: Upload private key if provided + if key_path: + logger.debug(f"Uploading private key to MikroTik") + with SCPClient(self.ssh_client.get_transport()) as scp: + scp.put(key_path, f'{self.key_name}.pem') + logger.info(f"✓ Private key file uploaded") + + # Step 5: Import certificate + logger.debug("Importing certificate into RouterOS") + import_cmd = f'/certificate import file-name={self.cert_name}.pem passphrase=""' + success, stdout, stderr = self.execute_command(import_cmd, timeout=30) + + if not success and "failure" in stderr.lower(): + logger.error(f"Certificate import failed: {stderr}") + return False + + # Give RouterOS time to process + import time + time.sleep(2) + + # Step 6: Verify certificate was imported + success, stdout, stderr = self.execute_command( + f'/certificate print where name~"{self.cert_name}"' + ) + + if success and stdout: + 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', + ] + + for cmd in config_commands: + success, stdout, stderr = self.execute_command(cmd, ignore_error=True) + + logger.info(f"✓ Successfully deployed certificate to MikroTik {self.hostname}") + return True + + except Exception as e: + logger.error(f"MikroTik certificate deployment failed: {e}", exc_info=True) + return False + + def verify_certificate(self) -> bool: + """Verify certificate is properly installed""" + try: + logger.debug("Verifying certificate installation") + success, stdout, stderr = self.execute_command( + '/certificate print detail where name~"ssl-cert"' + ) + + if success and stdout: + logger.info(f"Certificate verification:\n{stdout}") + return True + + logger.warning("Could not verify certificate installation") + return False + + except Exception as e: + logger.error(f"Certificate verification failed: {e}") + return False + + class CertPusher: """Main application class""" @@ -195,7 +364,13 @@ class CertPusher: logger.error("Missing [global] section in config file") return False - logger.info(f"Configuration loaded successfully") + required_global = ['source_cert_path', 'default_ssh_key'] + for key in required_global: + if not self.config.has_option('global', key): + logger.error(f"Missing required global option: {key}") + return False + + logger.info(f"✓ Configuration loaded successfully") logger.debug(f"Found {len(self.config.sections()) - 1} host(s) in configuration") return True @@ -203,6 +378,47 @@ class CertPusher: logger.error(f"Failed to load configuration: {e}") return False + def process_mikrotik(self, section: str, hostname: str, port: int, + username: str, ssh_key: str, source_cert_path: str) -> bool: + """Process MikroTik device specifically""" + try: + logger.info("Using MikroTik-specific deployment method") + + # Get optional private key path + source_key_path = self.config.get(section, 'source_key_path', fallback=None) + + if source_key_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 + mikrotik = MikroTikManager(hostname, port, username, ssh_key) + + if not mikrotik.connect(): + self.stats['failed'] += 1 + return False + + # Upload and import certificate + if not mikrotik.upload_certificate(source_cert_path, source_key_path): + mikrotik.disconnect() + self.stats['failed'] += 1 + return False + + # Verify installation + mikrotik.verify_certificate() + + mikrotik.disconnect() + self.stats['uploaded'] += 1 + logger.info(f"✓ Successfully processed MikroTik {section}") + return True + + except Exception as e: + logger.error(f"MikroTik 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: @@ -216,9 +432,7 @@ class CertPusher: hostname = self.config.get(section, 'hostname') port = self.config.getint(section, 'port', fallback=22) username = self.config.get(section, 'username', fallback='root') - remote_cert_path = self.config.get(section, 'remote_cert_path') - post_upload_command = self.config.get(section, 'post_upload_command', fallback='') - check_url = self.config.get(section, 'check_url', fallback='') + device_type = self.config.get(section, 'type', fallback='standard') # Determine SSH key to use if self.config.has_option(section, 'ssh_key_path'): @@ -229,8 +443,19 @@ class CertPusher: source_cert_path = self.config.get('global', 'source_cert_path') 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 + if device_type.lower() == 'mikrotik': + return self.process_mikrotik(section, hostname, port, username, ssh_key, source_cert_path) + + # Standard processing for other devices + remote_cert_path = self.config.get(section, 'remote_cert_path') + post_upload_command = self.config.get(section, 'post_upload_command', fallback='') + check_url = self.config.get(section, 'check_url', fallback='') + logger.info(f"Remote path: {remote_cert_path}") # Check if upload is needed @@ -241,11 +466,12 @@ class CertPusher: if source_cert and remote_cert: if self.cert_manager.compare_certificates(source_cert, remote_cert): - logger.info(f"Certificate on {hostname} is already up to date. Skipping upload.") + logger.info(f"✓ Certificate on {hostname} is already up to date. Skipping upload.") self.stats['skipped'] += 1 return True else: logger.info(f"Certificate on {hostname} is outdated. Upload needed.") + logger.debug(self.cert_manager.get_certificate_info(source_cert)) else: logger.warning(f"Could not compare certificates. Proceeding with upload.") @@ -261,6 +487,17 @@ class CertPusher: self.stats['failed'] += 1 return False + # Upload additional files if specified + if self.config.has_option(section, 'additional_files'): + additional_files = self.config.get(section, 'additional_files') + # Format: local_path:remote_path,local_path:remote_path + for file_pair in additional_files.split(','): + if ':' in file_pair: + local, remote = file_pair.strip().split(':', 1) + logger.info(f"Uploading additional file: {local} -> {remote}") + if not ssh.upload_file(local, remote): + logger.warning(f"Failed to upload additional file: {local}") + # Execute post-upload command if post_upload_command: logger.info(f"Executing post-upload command: {post_upload_command}") @@ -268,6 +505,8 @@ class CertPusher: if not success: logger.warning(f"Post-upload command failed, but file was uploaded successfully") + else: + logger.info(f"✓ Post-upload command completed successfully") ssh.disconnect() self.stats['uploaded'] += 1 @@ -282,8 +521,10 @@ class CertPusher: def run(self): """Main execution method""" logger.info("="*60) - logger.info("CertPusher - SSL Certificate Distribution Tool") + logger.info(" CertPusher - SSL Certificate Distribution Tool") logger.info("="*60) + logger.info(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + logger.info("") if not self.load_config(): logger.error("Configuration loading failed. Exiting.") @@ -297,6 +538,11 @@ class CertPusher: logger.info(f"Source certificate: {source_cert}") + # Display certificate info + cert = self.cert_manager.get_cert_from_file(source_cert) + if cert: + logger.info(self.cert_manager.get_certificate_info(cert)) + # Process each host for section in self.config.sections(): if section == 'global': @@ -310,22 +556,32 @@ class CertPusher: # Print summary logger.info("\n" + "="*60) - logger.info("DEPLOYMENT SUMMARY") + logger.info(" DEPLOYMENT SUMMARY") logger.info("="*60) - logger.info(f"Total hosts: {self.stats['total']}") - logger.info(f"✓ Uploaded: {self.stats['uploaded']}") + logger.info(f"Total hosts: {self.stats['total']}") + logger.info(f"✓ Uploaded: {self.stats['uploaded']}") logger.info(f"○ Skipped (up to date): {self.stats['skipped']}") - logger.info(f"✗ Failed: {self.stats['failed']}") + logger.info(f"✗ Failed: {self.stats['failed']}") logger.info("="*60) + logger.info(f"Finished at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") if self.stats['failed'] > 0: sys.exit(1) -if __name__ == '__main__': +def main(): + """Entry point""" + print(""" +╔═══════════════════════════════════════════════════════════╗ +║ CertPusher v1.0 ║ +║ Automated SSL Certificate Distribution Tool ║ +╚═══════════════════════════════════════════════════════════╝ +""") + if len(sys.argv) < 2: print("Usage: python certpusher.py ") print("Example: python certpusher.py config.ini") + print("") sys.exit(1) config_file = sys.argv[1] @@ -334,5 +590,16 @@ if __name__ == '__main__': print(f"Error: Configuration file '{config_file}' not found") sys.exit(1) - pusher = CertPusher(config_file) - pusher.run() + try: + pusher = CertPusher(config_file) + pusher.run() + except KeyboardInterrupt: + print("\n\nInterrupted by user. Exiting...") + sys.exit(130) + except Exception as e: + logger.error(f"Fatal error: {e}", exc_info=True) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/config.ini.example b/config.ini.example index 06d4522..dae5d95 100644 --- a/config.ini.example +++ b/config.ini.example @@ -1,40 +1,199 @@ [global] -# Path to source SSL certificate (can be fullchain or single cert) +# Path to source SSL certificate (fullchain recommended) source_cert_path = /etc/letsencrypt/live/example.com/fullchain.pem # Default SSH key for all hosts (can be overridden per host) default_ssh_key = /root/.ssh/id_rsa -[webserver1] +# ==================== MIKROTIK DEVICES ==================== + +[mikrotik_router] +type = mikrotik +hostname = 172.16.0.1 +port = 51022 +username = admin +ssh_key_path = /root/.ssh/id_rsa_proxy +# For MikroTik, you need to provide the private key separately +source_key_path = /etc/letsencrypt/live/example.com/privkey.pem +# Note: check_url not used for MikroTik + +[mikrotik_switch] +type = mikrotik +hostname = 192.168.1.50 +port = 22 +username = admin +source_key_path = /etc/letsencrypt/live/example.com/privkey.pem + +# ==================== PROXMOX HOSTS ==================== + +[proxmox_node1] +type = standard +hostname = pve1.example.com +port = 22 +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 + +[proxmox_node2] +type = standard +hostname = 10.0.0.51 +port = 22 +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 + +# ==================== HOME ASSISTANT ==================== + +[homeassistant_supervised] +type = standard hostname = 192.168.1.100 port = 22 username = root -remote_cert_path = /etc/nginx/ssl/certificate.pem -post_upload_command = systemctl reload nginx +# Home Assistant Supervised stores SSL in /ssl/ directory +remote_cert_path = /usr/share/hassio/ssl/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/usr/share/hassio/ssl/privkey.pem +# Home Assistant needs to be restarted via ha command +post_upload_command = ha core restart +check_url = https://homeassistant.local:8123 + +[homeassistant_core] +type = standard +hostname = 192.168.1.101 +port = 22 +username = homeassistant +ssh_key_path = /root/.ssh/homeassistant_key +# Home Assistant Core uses the config directory +remote_cert_path = /home/homeassistant/.homeassistant/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/home/homeassistant/.homeassistant/privkey.pem +post_upload_command = sudo systemctl restart home-assistant@homeassistant +check_url = https://192.168.1.101:8123 + +[homeassistant_docker] +type = standard +hostname = 192.168.1.102 +port = 22 +username = root +# Home Assistant in Docker - certificate goes to mounted config volume +remote_cert_path = /opt/homeassistant/config/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/opt/homeassistant/config/privkey.pem +# Restart Docker container +post_upload_command = docker restart homeassistant +check_url = https://ha.example.com:8123 + +[homeassistant_haos] +type = standard +hostname = 192.168.1.103 +port = 22 +username = root +# Home Assistant OS (HassOS) - using SSH add-on +remote_cert_path = /ssl/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/ssl/privkey.pem +post_upload_command = ha core restart +check_url = https://192.168.1.103:8123 + +# ==================== HOME ASSISTANT WITH NGINX PROXY ==================== + +[homeassistant_nginx_proxy] +type = standard +hostname = 192.168.1.104 +port = 22 +username = root +# When using nginx as reverse proxy for Home Assistant +remote_cert_path = /etc/nginx/ssl/homeassistant/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/nginx/ssl/homeassistant/privkey.pem +post_upload_command = nginx -t && systemctl reload nginx +check_url = https://ha.example.com + +# ==================== STANDARD WEB SERVERS ==================== + +[webserver_nginx] +type = standard +hostname = 192.168.1.110 +port = 22 +username = root +remote_cert_path = /etc/nginx/ssl/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/nginx/ssl/privkey.pem +post_upload_command = nginx -t && systemctl reload nginx check_url = https://example.com -[webserver2] -hostname = 192.168.1.101 +[webserver_apache] +type = standard +hostname = 192.168.1.111 port = 2222 username = admin -# Override SSH key for this host -ssh_key_path = /root/.ssh/webserver2_key +ssh_key_path = /root/.ssh/webserver_key remote_cert_path = /etc/apache2/ssl/fullchain.pem -post_upload_command = systemctl reload apache2 +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/apache2/ssl/privkey.pem +post_upload_command = apachectl configtest && systemctl reload apache2 check_url = https://subdomain.example.com -[mailserver] +# ==================== MAIL SERVERS ==================== + +[mailserver_postfix] +type = standard hostname = mail.example.com port = 22 username = root remote_cert_path = /etc/postfix/ssl/cert.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/postfix/ssl/privkey.pem post_upload_command = systemctl restart postfix && systemctl restart dovecot -# No check_url - always upload -[proxmox] -hostname = 10.0.0.50 +# ==================== DOCKER / CONTAINER HOSTS ==================== + +[docker_traefik] +type = standard +hostname = 10.0.0.60 port = 22 username = root -remote_cert_path = /etc/pve/local/pveproxy-ssl.pem -post_upload_command = systemctl restart pveproxy -check_url = https://10.0.0.50:8006 +remote_cert_path = /opt/docker/traefik/certs/cert.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/opt/docker/traefik/certs/key.pem +post_upload_command = docker restart traefik +check_url = https://traefik.example.com + +[docker_nginx_proxy_manager] +type = standard +hostname = 10.0.0.61 +port = 22 +username = root +remote_cert_path = /opt/docker/nginx-proxy-manager/letsencrypt/live/npm-1/fullchain.pem +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/opt/docker/nginx-proxy-manager/letsencrypt/live/npm-1/privkey.pem +post_upload_command = docker exec nginx-proxy-manager nginx -s reload +check_url = https://npm.example.com + +# ==================== STORAGE / NAS ==================== + +[truenas_scale] +type = standard +hostname = 10.0.0.70 +port = 22 +username = root +remote_cert_path = /etc/certificates/truenas_cert.crt +additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/certificates/truenas_cert.key +post_upload_command = midclt call system.general.ui_restart +check_url = https://truenas.local + +[synology_nas] +type = standard +hostname = 10.0.0.71 +port = 22 +username = root +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