mikrotik checks

This commit is contained in:
Mateusz Gruszczyński
2025-10-27 07:12:15 +01:00
parent 7fc8b3894c
commit 5a37e451a5
2 changed files with 348 additions and 205 deletions

View File

@@ -11,7 +11,7 @@ import sys
import os import os
import ssl import ssl
import socket import socket
import tempfile import re
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
@@ -120,31 +120,6 @@ Certificate Info:
""" """
except Exception as e: except Exception as e:
return f"Unable to extract certificate info: {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: class SSHManager:
@@ -267,17 +242,86 @@ class MikroTikManager(SSHManager):
self.cert_name = "ssl-cert" self.cert_name = "ssl-cert"
self.key_name = "ssl-key" self.key_name = "ssl-key"
def upload_certificate(self, cert_path: str, key_path: str = None) -> bool: def check_certificate_expiry(self, source_cert: x509.Certificate) -> bool:
"""
Check if certificate on MikroTik needs update
Returns True if upload needed, False if current cert is OK
"""
try:
logger.info("Checking certificate on MikroTik")
# Get certificate details from MikroTik
success, stdout, stderr = self.execute_command(
f'/certificate print detail where name~"{self.cert_name}"',
ignore_error=True
)
if not success or not stdout:
logger.info("No certificate found on MikroTik or cannot read it. Upload needed.")
return True
logger.debug(f"MikroTik certificate info:\n{stdout}")
# Parse expiry date from output
# Looking for "invalid-after" field
invalid_after_match = re.search(r'invalid-after:\s+([a-zA-Z]{3}/\d{2}/\d{4}\s+\d{2}:\d{2}:\d{2})', stdout)
if not invalid_after_match:
logger.warning("Could not parse certificate expiry date from MikroTik. Proceeding with upload.")
return True
mikrotik_expiry_str = invalid_after_match.group(1)
logger.debug(f"MikroTik certificate expires: {mikrotik_expiry_str}")
# Parse MikroTik date format (e.g., "jan/24/2026 08:34:12")
try:
mikrotik_expiry = datetime.strptime(mikrotik_expiry_str, '%b/%d/%Y %H:%M:%S')
mikrotik_expiry = mikrotik_expiry.replace(tzinfo=timezone.utc)
except Exception as e:
logger.warning(f"Could not parse MikroTik date: {e}. Proceeding with upload.")
return True
# Compare with source certificate
source_expiry = source_cert.not_valid_after_utc
# Also check fingerprint/serial if available
fingerprint_match = re.search(r'fingerprint:\s+([a-f0-9]+)', stdout)
if fingerprint_match:
mikrotik_fingerprint = fingerprint_match.group(1)
logger.debug(f"MikroTik certificate fingerprint: {mikrotik_fingerprint}")
# Compare expiry dates (allowing 1 day tolerance for timezone differences)
time_diff = abs((source_expiry - mikrotik_expiry).total_seconds())
if time_diff < 86400: # Less than 24 hours difference
logger.info("✓ Certificate on MikroTik is current. Skipping upload.")
return False
else:
logger.info(f"Certificate on MikroTik differs. Source expires: {source_expiry}, MikroTik expires: {mikrotik_expiry}")
return True
except Exception as e:
logger.warning(f"Error checking MikroTik certificate: {e}. Proceeding with upload.")
return True
def upload_certificate(self, cert_path: str, key_path: str = None, check_first: bool = True, source_cert: x509.Certificate = None) -> bool:
""" """
Upload and import certificate to MikroTik RouterOS Upload and import certificate to MikroTik RouterOS
Args: Args:
cert_path: Path to certificate file (PEM format, can be fullchain) cert_path: Path to certificate file (PEM format, can be fullchain)
key_path: Optional path to private key file key_path: Optional path to private key file
check_first: If True, check existing certificate before uploading
source_cert: Source certificate object for comparison
""" """
try: try:
logger.info(f"Starting MikroTik certificate deployment to {self.hostname}") logger.info(f"Starting MikroTik certificate deployment to {self.hostname}")
# Check if upload is needed
if check_first and source_cert:
if not self.check_certificate_expiry(source_cert):
return True # Certificate is current, no upload needed
# Step 1: Disable www-ssl service # Step 1: Disable www-ssl service
logger.debug("Disabling www-ssl service") logger.debug("Disabling www-ssl service")
self.execute_command('/ip service disable www-ssl', ignore_error=True) self.execute_command('/ip service disable www-ssl', ignore_error=True)
@@ -470,20 +514,45 @@ class CertPusher:
logger.error(f"Failed to load configuration: {e}") logger.error(f"Failed to load configuration: {e}")
return False return False
def get_key_path(self, section: str, cert_path: str) -> str:
"""
Get private key path for certificate
Priority: section-specific > global > derived from cert path
"""
# Check section-specific key
if self.config.has_option(section, 'source_key_path'):
return self.config.get(section, 'source_key_path')
# Check global key
if self.config.has_option('global', 'source_key_path'):
return self.config.get('global', 'source_key_path')
# Derive from certificate path
key_path = cert_path.replace('fullchain.pem', 'privkey.pem').replace('cert.pem', 'privkey.pem')
logger.debug(f"Derived key path: {key_path}")
return key_path
def process_mikrotik(self, section: str, hostname: str, port: int, def process_mikrotik(self, section: str, hostname: str, port: int,
username: str, ssh_key: str, source_cert_path: str) -> bool: username: str, ssh_key: str, source_cert_path: str) -> bool:
"""Process MikroTik device specifically""" """Process MikroTik device specifically"""
try: try:
logger.info("Using MikroTik-specific deployment method") logger.info("Using MikroTik-specific deployment method")
# Get optional private key path # Get private key path
source_key_path = self.config.get(section, 'source_key_path', fallback=None) source_key_path = self.get_key_path(section, source_cert_path)
if source_key_path: logger.info(f"Certificate: {source_cert_path}")
logger.info(f"Private key: {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}") if not os.path.exists(source_key_path):
return False logger.error(f"Private key file not found: {source_key_path}")
return False
# Load source certificate for comparison
source_cert = self.cert_manager.get_cert_from_file(source_cert_path)
# Check if certificate check is enabled
check_first = self.config.getboolean(section, 'check_before_upload', fallback=True)
# Connect # Connect
mikrotik = MikroTikManager(hostname, port, username, ssh_key) mikrotik = MikroTikManager(hostname, port, username, ssh_key)
@@ -492,17 +561,29 @@ class CertPusher:
self.stats['failed'] += 1 self.stats['failed'] += 1
return False return False
# Upload and import certificate # Upload and import certificate (with optional check)
if not mikrotik.upload_certificate(source_cert_path, source_key_path): if not mikrotik.upload_certificate(source_cert_path, source_key_path, check_first, source_cert):
mikrotik.disconnect() mikrotik.disconnect()
self.stats['failed'] += 1 self.stats['failed'] += 1
return False return False
# If we got here and check was enabled, certificate was actually uploaded
if check_first and source_cert:
# Check if it was actually uploaded or skipped
success, stdout, stderr = mikrotik.execute_command(
f'/certificate print where name~"{mikrotik.cert_name}"'
)
if success and stdout and mikrotik.cert_name in stdout:
self.stats['uploaded'] += 1
else:
self.stats['skipped'] += 1
else:
self.stats['uploaded'] += 1
# Verify installation # Verify installation
mikrotik.verify_certificate() mikrotik.verify_certificate()
mikrotik.disconnect() mikrotik.disconnect()
self.stats['uploaded'] += 1
logger.info(f"✓ Successfully processed MikroTik {section}") logger.info(f"✓ Successfully processed MikroTik {section}")
return True return True
@@ -518,11 +599,7 @@ class CertPusher:
logger.info("Using Proxmox-specific deployment method") logger.info("Using Proxmox-specific deployment method")
# Get private key path # Get private key path
if self.config.has_option(section, 'source_key_path'): source_key_path = self.get_key_path(section, source_cert_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"Certificate: {source_cert_path}")
logger.info(f"Private key: {source_key_path}") logger.info(f"Private key: {source_key_path}")
@@ -555,117 +632,136 @@ class CertPusher:
return False return False
def process_host(self, section: str) -> bool: def process_host(self, section: str) -> bool:
"""Process certificate deployment for a single host""" """Process certificate deployment for a single host"""
try: try:
logger.info(f"\n{'='*60}") logger.info(f"\n{'='*60}")
logger.info(f"Processing host: {section}") logger.info(f"Processing host: {section}")
logger.info(f"{'='*60}") logger.info(f"{'='*60}")
self.stats['total'] += 1
# Get configuration
hostname = self.config.get(section, 'hostname')
port = self.config.getint(section, 'port', fallback=22)
username = self.config.get(section, 'username', fallback='root')
device_type = self.config.get(section, 'type', fallback='standard')
# Determine SSH key to use
if self.config.has_option(section, 'ssh_key_path'):
ssh_key = self.config.get(section, 'ssh_key_path')
else:
ssh_key = self.config.get('global', 'default_ssh_key')
# 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 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')
post_upload_command = self.config.get(section, 'post_upload_command', fallback='')
check_url = self.config.get(section, 'check_url', fallback='')
logger.info(f"Remote certificate path: {remote_cert_path}")
# Check if upload is needed
if check_url:
logger.info(f"Checking current certificate at: {check_url}")
source_cert = self.cert_manager.get_cert_from_file(source_cert_path)
remote_cert = self.cert_manager.get_cert_from_url(check_url)
self.stats['total'] += 1 if source_cert and remote_cert:
if self.cert_manager.compare_certificates(source_cert, remote_cert):
# Get configuration logger.info(f"✓ Certificate on {hostname} is already up to date. Skipping upload.")
hostname = self.config.get(section, 'hostname') self.stats['skipped'] += 1
port = self.config.getint(section, 'port', fallback=22) return True
username = self.config.get(section, 'username', fallback='root')
device_type = self.config.get(section, 'type', fallback='standard')
# Determine SSH key to use
if self.config.has_option(section, 'ssh_key_path'):
ssh_key = self.config.get(section, 'ssh_key_path')
else:
ssh_key = self.config.get('global', 'default_ssh_key')
# 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 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')
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
if check_url:
logger.info(f"Checking current certificate at: {check_url}")
source_cert = self.cert_manager.get_cert_from_file(source_cert_path)
remote_cert = self.cert_manager.get_cert_from_url(check_url)
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.")
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: else:
logger.warning(f"Could not compare certificates. Proceeding with upload.") 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.")
# Connect and upload
ssh = SSHManager(hostname, port, username, ssh_key)
if not ssh.connect():
self.stats['failed'] += 1
return False
# Upload certificate
if not ssh.upload_file(source_cert_path, remote_cert_path):
ssh.disconnect()
self.stats['failed'] += 1
return False
# Upload private key if remote_key_path is specified
if self.config.has_option(section, 'remote_key_path'):
remote_key_path = self.config.get(section, 'remote_key_path')
source_key_path = self.get_key_path(section, source_cert_path)
# Connect and upload logger.info(f"Remote key path: {remote_key_path}")
ssh = SSHManager(hostname, port, username, ssh_key) logger.info(f"Uploading private key: {source_key_path} -> {remote_key_path}")
if not ssh.connect(): if not os.path.exists(source_key_path):
self.stats['failed'] += 1 logger.error(f"Private key file not found: {source_key_path}")
return False
if not ssh.upload_file(source_cert_path, remote_cert_path):
ssh.disconnect() ssh.disconnect()
self.stats['failed'] += 1 self.stats['failed'] += 1
return False return False
# Upload additional files if specified if not ssh.upload_file(source_key_path, remote_key_path):
if self.config.has_option(section, 'additional_files'): logger.warning(f"Failed to upload private key to {remote_key_path}")
additional_files = self.config.get(section, 'additional_files') # Continue anyway - key might be uploaded via additional_files
# Format: local_path:remote_path,local_path:remote_path
for file_pair in additional_files.split(','): # Upload additional files if specified
if ':' in file_pair: if self.config.has_option(section, 'additional_files'):
local, remote = file_pair.strip().split(':', 1) additional_files = self.config.get(section, 'additional_files')
logger.info(f"Uploading additional file: {local} -> {remote}") # Format: local_path:remote_path,local_path:remote_path
if not ssh.upload_file(local, remote): for file_pair in additional_files.split(','):
logger.warning(f"Failed to upload additional file: {local}") 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}")
success, stdout, stderr = ssh.execute_command(post_upload_command)
# Execute post-upload command if not success:
if post_upload_command: logger.warning(f"Post-upload command failed, but file was uploaded successfully")
logger.info(f"Executing post-upload command: {post_upload_command}") else:
success, stdout, stderr = ssh.execute_command(post_upload_command) logger.info(f"✓ Post-upload command completed successfully")
if not success: ssh.disconnect()
logger.warning(f"Post-upload command failed, but file was uploaded successfully") self.stats['uploaded'] += 1
else: logger.info(f"✓ Successfully processed {section}")
logger.info(f"✓ Post-upload command completed successfully") return True
ssh.disconnect() except Exception as e:
self.stats['uploaded'] += 1 logger.error(f"Failed to process host {section}: {e}", exc_info=True)
logger.info(f"✓ Successfully processed {section}") self.stats['failed'] += 1
return True return False
except Exception as e:
logger.error(f"Failed to process host {section}: {e}", exc_info=True)
self.stats['failed'] += 1
return False
def run(self): def run(self):
"""Main execution method""" """Main execution method"""

View File

@@ -1,11 +1,19 @@
# ╔═══════════════════════════════════════════════════════════╗
# ║ CertPusher Configuration File ║
# ║ SSL Certificate Distribution Configuration ║
# ╚═══════════════════════════════════════════════════════════╝
[global] [global]
# Path to source SSL certificate (fullchain recommended) # Global source certificate and private key
source_cert_path = /etc/letsencrypt/live/example.com/fullchain.pem source_cert_path = /etc/letsencrypt/live/example.com/fullchain.pem
source_key_path = /etc/letsencrypt/live/example.com/privkey.pem
# Default SSH key for all hosts (can be overridden per host) # Default SSH key for all hosts (can be overridden per host)
default_ssh_key = /root/.ssh/id_rsa default_ssh_key = /root/.ssh/id_rsa
# ==================== MIKROTIK DEVICES ==================== # ═══════════════════════════════════════════════════════════
# MIKROTIK DEVICES
# ═══════════════════════════════════════════════════════════
[mikrotik_router] [mikrotik_router]
type = mikrotik type = mikrotik
@@ -13,26 +21,26 @@ hostname = 172.16.0.1
port = 51022 port = 51022
username = admin username = admin
ssh_key_path = /root/.ssh/id_rsa_proxy ssh_key_path = /root/.ssh/id_rsa_proxy
# For MikroTik, you need to provide the private key separately # Check certificate before upload (default: true)
source_key_path = /etc/letsencrypt/live/example.com/privkey.pem check_before_upload = true
# Note: check_url not used for MikroTik
[mikrotik_switch] [mikrotik_switch]
type = mikrotik type = mikrotik
hostname = 192.168.1.50 hostname = 192.168.1.50
port = 22 port = 22
username = admin username = admin
source_key_path = /etc/letsencrypt/live/example.com/privkey.pem # Always upload without checking
check_before_upload = false
# ==================== PROXMOX SERVERS ==================== # ═══════════════════════════════════════════════════════════
# PROXMOX VE SERVERS
# ═══════════════════════════════════════════════════════════
[proxmox1] [proxmox1]
type = proxmox type = proxmox
hostname = 10.87.2.150 hostname = 10.87.2.150
port = 11922 port = 11922
username = root username = root
# 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 check_url = https://10.87.2.150:8006
[proxmox2] [proxmox2]
@@ -40,20 +48,19 @@ type = proxmox
hostname = 10.87.2.151 hostname = 10.87.2.151
port = 11922 port = 11922
username = root username = root
source_key_path = /etc/letsencrypt/live/npm-3/privkey.pem
check_url = https://10.87.2.151:8006 check_url = https://10.87.2.151:8006
# ==================== HOME ASSISTANT ==================== # ═══════════════════════════════════════════════════════════
# HOME ASSISTANT INSTALLATIONS
# ═══════════════════════════════════════════════════════════
[homeassistant_supervised] [homeassistant_supervised]
type = standard type = standard
hostname = 192.168.1.100 hostname = 192.168.1.100
port = 22 port = 22
username = root username = root
# Home Assistant Supervised stores SSL in /ssl/ directory
remote_cert_path = /usr/share/hassio/ssl/fullchain.pem remote_cert_path = /usr/share/hassio/ssl/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/usr/share/hassio/ssl/privkey.pem remote_key_path = /usr/share/hassio/ssl/privkey.pem
# Home Assistant needs to be restarted via ha command
post_upload_command = ha core restart post_upload_command = ha core restart
check_url = https://homeassistant.local:8123 check_url = https://homeassistant.local:8123
@@ -63,9 +70,8 @@ hostname = 192.168.1.101
port = 22 port = 22
username = homeassistant username = homeassistant
ssh_key_path = /root/.ssh/homeassistant_key ssh_key_path = /root/.ssh/homeassistant_key
# Home Assistant Core uses the config directory
remote_cert_path = /home/homeassistant/.homeassistant/fullchain.pem remote_cert_path = /home/homeassistant/.homeassistant/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/home/homeassistant/.homeassistant/privkey.pem remote_key_path = /home/homeassistant/.homeassistant/privkey.pem
post_upload_command = sudo systemctl restart home-assistant@homeassistant post_upload_command = sudo systemctl restart home-assistant@homeassistant
check_url = https://192.168.1.101:8123 check_url = https://192.168.1.101:8123
@@ -74,10 +80,8 @@ type = standard
hostname = 192.168.1.102 hostname = 192.168.1.102
port = 22 port = 22
username = root username = root
# Home Assistant in Docker - certificate goes to mounted config volume
remote_cert_path = /opt/homeassistant/config/fullchain.pem remote_cert_path = /opt/homeassistant/config/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/opt/homeassistant/config/privkey.pem remote_key_path = /opt/homeassistant/config/privkey.pem
# Restart Docker container
post_upload_command = docker restart homeassistant post_upload_command = docker restart homeassistant
check_url = https://ha.example.com:8123 check_url = https://ha.example.com:8123
@@ -86,26 +90,24 @@ type = standard
hostname = 192.168.1.103 hostname = 192.168.1.103
port = 22 port = 22
username = root username = root
# Home Assistant OS (HassOS) - using SSH add-on
remote_cert_path = /ssl/fullchain.pem remote_cert_path = /ssl/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/ssl/privkey.pem remote_key_path = /ssl/privkey.pem
post_upload_command = ha core restart post_upload_command = ha core restart
check_url = https://192.168.1.103:8123 check_url = https://192.168.1.103:8123
# ==================== HOME ASSISTANT WITH NGINX PROXY ====================
[homeassistant_nginx_proxy] [homeassistant_nginx_proxy]
type = standard type = standard
hostname = 192.168.1.104 hostname = 192.168.1.104
port = 22 port = 22
username = root username = root
# When using nginx as reverse proxy for Home Assistant
remote_cert_path = /etc/nginx/ssl/homeassistant/fullchain.pem remote_cert_path = /etc/nginx/ssl/homeassistant/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/nginx/ssl/homeassistant/privkey.pem remote_key_path = /etc/nginx/ssl/homeassistant/privkey.pem
post_upload_command = nginx -t && systemctl reload nginx post_upload_command = nginx -t && systemctl reload nginx
check_url = https://ha.example.com check_url = https://ha.example.com
# ==================== STANDARD WEB SERVERS ==================== # ═══════════════════════════════════════════════════════════
# WEB SERVERS (NGINX & APACHE)
# ═══════════════════════════════════════════════════════════
[webserver_nginx] [webserver_nginx]
type = standard type = standard
@@ -113,7 +115,7 @@ hostname = 192.168.1.110
port = 22 port = 22
username = root username = root
remote_cert_path = /etc/nginx/ssl/fullchain.pem remote_cert_path = /etc/nginx/ssl/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/nginx/ssl/privkey.pem remote_key_path = /etc/nginx/ssl/privkey.pem
post_upload_command = nginx -t && systemctl reload nginx post_upload_command = nginx -t && systemctl reload nginx
check_url = https://example.com check_url = https://example.com
@@ -124,11 +126,13 @@ port = 2222
username = admin username = admin
ssh_key_path = /root/.ssh/webserver_key ssh_key_path = /root/.ssh/webserver_key
remote_cert_path = /etc/apache2/ssl/fullchain.pem remote_cert_path = /etc/apache2/ssl/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/apache2/ssl/privkey.pem remote_key_path = /etc/apache2/ssl/privkey.pem
post_upload_command = apachectl configtest && systemctl reload apache2 post_upload_command = apachectl configtest && systemctl reload apache2
check_url = https://subdomain.example.com check_url = https://subdomain.example.com
# ==================== MAIL SERVERS ==================== # ═══════════════════════════════════════════════════════════
# MAIL SERVERS (POSTFIX/DOVECOT)
# ═══════════════════════════════════════════════════════════
[mailserver_postfix] [mailserver_postfix]
type = standard type = standard
@@ -136,18 +140,21 @@ hostname = mail.example.com
port = 22 port = 22
username = root username = root
remote_cert_path = /etc/postfix/ssl/cert.pem remote_cert_path = /etc/postfix/ssl/cert.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/postfix/ssl/privkey.pem remote_key_path = /etc/postfix/ssl/privkey.pem
post_upload_command = systemctl restart postfix && systemctl restart dovecot post_upload_command = systemctl restart postfix && systemctl restart dovecot
check_url = https://mail.example.com:587
# ==================== DOCKER / CONTAINER HOSTS ==================== # ═══════════════════════════════════════════════════════════
# DOCKER / CONTAINER PLATFORMS
# ═══════════════════════════════════════════════════════════
[docker_traefik] [docker_traefik]
type = standard type = standard
hostname = 10.0.0.60 hostname = 10.0.0.60
port = 22 port = 22
username = root username = root
remote_cert_path = /opt/docker/traefik/certs/cert.pem remote_cert_path = /opt/docker/traefik/certs/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/opt/docker/traefik/certs/key.pem remote_key_path = /opt/docker/traefik/certs/privkey.pem
post_upload_command = docker restart traefik post_upload_command = docker restart traefik
check_url = https://traefik.example.com check_url = https://traefik.example.com
@@ -156,12 +163,14 @@ type = standard
hostname = 10.0.0.61 hostname = 10.0.0.61
port = 22 port = 22
username = root username = root
remote_cert_path = /opt/docker/nginx-proxy-manager/letsencrypt/live/npm-1/fullchain.pem remote_cert_path = /opt/docker/npm/ssl/fullchain.pem
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/opt/docker/nginx-proxy-manager/letsencrypt/live/npm-1/privkey.pem remote_key_path = /opt/docker/npm/ssl/privkey.pem
post_upload_command = docker exec nginx-proxy-manager nginx -s reload post_upload_command = docker exec nginx-proxy-manager nginx -s reload
check_url = https://npm.example.com check_url = https://npm.example.com
# ==================== STORAGE / NAS ==================== # ═══════════════════════════════════════════════════════════
# STORAGE / NAS DEVICES
# ═══════════════════════════════════════════════════════════
[truenas_scale] [truenas_scale]
type = standard type = standard
@@ -169,7 +178,7 @@ hostname = 10.0.0.70
port = 22 port = 22
username = root username = root
remote_cert_path = /etc/certificates/truenas_cert.crt remote_cert_path = /etc/certificates/truenas_cert.crt
additional_files = /etc/letsencrypt/live/example.com/privkey.pem:/etc/certificates/truenas_cert.key remote_key_path = /etc/certificates/truenas_cert.key
post_upload_command = midclt call system.general.ui_restart post_upload_command = midclt call system.general.ui_restart
check_url = https://truenas.local check_url = https://truenas.local
@@ -179,54 +188,51 @@ hostname = 10.0.0.71
port = 22 port = 22
username = root username = root
remote_cert_path = /usr/syno/etc/certificate/system/default/fullchain.pem 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 remote_key_path = /usr/syno/etc/certificate/system/default/privkey.pem
post_upload_command = /usr/syno/sbin/synoservicectl --reload nginx post_upload_command = /usr/syno/sbin/synoservicectl --reload nginx
check_url = https://synology.local:5001 check_url = https://synology.local:5001
# ═══════════════════════════════════════════════════════════
# HOSTS WITH CUSTOM CERTIFICATES
# ═══════════════════════════════════════════════════════════
# ==================== MAIL SERVER WITH CUSTOM CERTIFICATE ==================== [mailserver_custom_domain]
# This server uses mail.company.com certificate
[mailserver]
type = standard type = standard
hostname = mail.company.com hostname = mail.company.com
port = 22 port = 22
username = root username = root
# Override: use mail-specific certificate # Override global certificate with domain-specific cert
source_cert_path = /etc/letsencrypt/live/mail.company.com/fullchain.pem source_cert_path = /etc/letsencrypt/live/mail.company.com/fullchain.pem
source_key_path = /etc/letsencrypt/live/mail.company.com/privkey.pem
remote_cert_path = /etc/postfix/ssl/cert.pem remote_cert_path = /etc/postfix/ssl/cert.pem
additional_files = /etc/letsencrypt/live/mail.company.com/privkey.pem:/etc/postfix/ssl/privkey.pem remote_key_path = /etc/postfix/ssl/privkey.pem
post_upload_command = systemctl restart postfix && systemctl restart dovecot post_upload_command = systemctl restart postfix && systemctl restart dovecot
check_url = https://mail.company.com:465 check_url = https://mail.company.com:465
# ==================== SUBDOMAIN WITH CUSTOM CERTIFICATE ==================== [api_server_subdomain]
# This server uses subdomain.org certificate
[api_server]
type = standard type = standard
hostname = 192.168.1.200 hostname = 192.168.1.200
port = 22 port = 22
username = ubuntu username = ubuntu
ssh_key_path = /root/.ssh/api_key ssh_key_path = /root/.ssh/api_key
# Override: use api-specific certificate # API server with its own certificate
source_cert_path = /etc/letsencrypt/live/api.subdomain.org/fullchain.pem source_cert_path = /etc/letsencrypt/live/api.subdomain.org/fullchain.pem
source_key_path = /etc/letsencrypt/live/api.subdomain.org/privkey.pem
remote_cert_path = /etc/nginx/ssl/api/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 remote_key_path = /etc/nginx/ssl/api/privkey.pem
post_upload_command = systemctl reload nginx post_upload_command = systemctl reload nginx
check_url = https://api.subdomain.org check_url = https://api.subdomain.org
# ==================== CLIENT SITE WITH CUSTOM CERTIFICATE ====================
# Client's own domain and certificate
[client_website] [client_website]
type = standard type = standard
hostname = 203.0.113.50 hostname = 203.0.113.50
port = 2222 port = 2222
username = admin username = admin
# Override: use client-specific certificate # Client's own domain and certificate
source_cert_path = /etc/letsencrypt/live/client-domain.com/fullchain.pem source_cert_path = /etc/letsencrypt/live/client-domain.com/fullchain.pem
source_key_path = /etc/letsencrypt/live/client-domain.com/privkey.pem
remote_cert_path = /var/www/ssl/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 remote_key_path = /var/www/ssl/privkey.pem
post_upload_command = systemctl reload apache2 post_upload_command = systemctl reload apache2
check_url = https://www.client-domain.com check_url = https://www.client-domain.com
@@ -236,21 +242,62 @@ hostname = 203.0.113.51
port = 22 port = 22
username = admin username = admin
ssh_key_path = /root/.ssh/client_key ssh_key_path = /root/.ssh/client_key
# Override: use client-specific certificate # Client's MikroTik with custom certificate
source_cert_path = /etc/letsencrypt/live/client-domain.com/fullchain.pem source_cert_path = /etc/letsencrypt/live/client-domain.com/fullchain.pem
source_key_path = /etc/letsencrypt/live/client-domain.com/privkey.pem source_key_path = /etc/letsencrypt/live/client-domain.com/privkey.pem
check_before_upload = true
# ==================== DEVELOPMENT SERVER ==================== # ═══════════════════════════════════════════════════════════
# Dev server with staging certificate # DEVELOPMENT / STAGING SERVERS
# ═══════════════════════════════════════════════════════════
[dev_server] [dev_server]
type = standard type = standard
hostname = dev.local hostname = dev.local
port = 22 port = 22
username = developer username = developer
# Override: use staging certificate for testing # Dev server with staging certificate
source_cert_path = /etc/letsencrypt-staging/live/dev.example.com/fullchain.pem source_cert_path = /etc/letsencrypt-staging/live/dev.example.com/fullchain.pem
source_key_path = /etc/letsencrypt-staging/live/dev.example.com/privkey.pem
remote_cert_path = /opt/app/ssl/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 remote_key_path = /opt/app/ssl/privkey.pem
post_upload_command = docker-compose restart nginx post_upload_command = docker-compose restart nginx
# No check_url - always upload to dev # No check_url - always upload to dev environment
# ═══════════════════════════════════════════════════════════
# ADVANCED EXAMPLES
# ═══════════════════════════════════════════════════════════
[nginx_with_chain]
type = standard
hostname = 192.168.1.120
port = 22
username = root
remote_cert_path = /etc/nginx/ssl/cert.pem
remote_key_path = /etc/nginx/ssl/private/key.pem
# Upload additional chain file
additional_files = /etc/letsencrypt/live/example.com/chain.pem:/etc/nginx/ssl/chain.pem
post_upload_command = nginx -t && systemctl reload nginx
check_url = https://example.com
[apache_separate_dirs]
type = standard
hostname = 192.168.1.121
port = 22
username = root
# Certificate and key in completely different directories
remote_cert_path = /etc/ssl/certs/apache-cert.pem
remote_key_path = /etc/ssl/private/apache-key.pem
post_upload_command = apachectl configtest && systemctl reload apache2
[multi_service_host]
type = standard
hostname = 192.168.1.130
port = 22
username = root
remote_cert_path = /etc/ssl/certs/fullchain.pem
remote_key_path = /etc/ssl/private/privkey.pem
# Deploy cert to multiple services
additional_files = /etc/letsencrypt/live/example.com/fullchain.pem:/opt/app1/ssl/cert.pem,/etc/letsencrypt/live/example.com/privkey.pem:/opt/app1/ssl/key.pem,/etc/letsencrypt/live/example.com/fullchain.pem:/opt/app2/ssl/cert.pem
post_upload_command = systemctl reload nginx && systemctl restart app1 && systemctl restart app2
check_url = https://example.com