Menü schliessen
Created: November 25th 2025
Categories: Linux
Author: LEXO

Virtualmin | Automated SSL Certificate Monitoring and Renewal Bash Script

Introduction: Automating SSL Certificate Management on Virtualmin

Managing SSL certificates across multiple domains can quickly become overwhelming for Linux server administrators. Expired certificates lead to browser warnings, lost visitor trust, and potential downtime for critical services. For administrators running Virtualmin on Linux servers, maintaining dozens or even hundreds of SSL certificates manually is simply not scalable.

This comprehensive bash script provides a complete solution for automated SSL certificate monitoring and renewal specifically designed for Virtualmin environments. It continuously monitors all SSL-enabled domains, automatically renews certificates approaching expiration using Let's Encrypt, performs DNS resolution checks, and sends detailed email notifications for every issue detected. The script supports modern ECDSA (Elliptic Curve) certificates for enhanced security and performance.

Whether you're managing a small web hosting environment or enterprise-scale infrastructure, this script eliminates the manual overhead of certificate management while ensuring your domains remain secure and accessible.

Who This Script Is For

This automation solution is specifically designed for:

  • Linux Server Administrators managing Virtualmin-based hosting environments
  • Web Hosting Professionals responsible for multiple client domains
  • DevOps Engineers maintaining infrastructure with SSL requirements
  • IT Departments running internal web services on Virtualmin
  • Managed Service Providers offering hosting solutions

Prerequisites: This script requires a Linux server (Debian/Ubuntu recommended) with Virtualmin installed and configured. It is specifically designed to work with Virtualmin's Let's Encrypt integration and will not function on systems without Virtualmin.

Key Features and Benefits

This SSL monitoring script offers comprehensive functionality that goes beyond simple certificate checking:

Automated Certificate Monitoring

  • Scans all SSL-enabled domains managed by Virtualmin
  • Configurable warning threshold (default: 10 days before expiration)
  • Supports manual certificate file checks for non-Virtualmin services
  • Handles both active and expired certificates gracefully

Intelligent DNS Validation

  • Performs DNS resolution checks before certificate validation
  • Detects and reports DNS configuration issues
  • Prevents false positives from unreachable domains

Automatic Certificate Renewal

  • Attempts automatic Let's Encrypt renewal for expiring certificates
  • Uses modern ECDSA certificates for better performance
  • Validates DNS records before renewal attempts
  • Verifies successful renewal with post-renewal certificate checks

Comprehensive Email Notifications

  • Individual email reports for each domain with issues
  • Detailed renewal attempt logs included in notifications
  • Daily summary email with all domain statuses
  • Clear categorization: SUCCESS, WARNING, ERROR, RENEWED

Domain Exclusion Management

  • Exclude specific domains from monitoring (test domains, parked domains, etc.)
  • Configurable exclusion list in the script header

How the Script Works: Technical Overview

The script follows a systematic workflow for each SSL-enabled domain in your Virtualmin installation:

  1. Domain Discovery: Queries Virtualmin for all domains with SSL features enabled
  2. DNS Resolution Check: Verifies each domain resolves correctly before attempting certificate checks
  3. Certificate Retrieval: Connects to each domain via OpenSSL to retrieve certificate details
  4. Expiry Calculation: Calculates days remaining until certificate expiration
  5. Threshold Evaluation: Compares remaining days against the configured warning threshold
  6. Automatic Renewal: For certificates below threshold, attempts Let's Encrypt renewal via Virtualmin
  7. Individual Notifications: Sends detailed email reports for domains with issues (DNS failures, retrieval errors, expiring certificates)
  8. Summary Generation: Compiles a final summary email with statistics for all checked domains

The script maintains detailed logs and provides actionable information in every notification, allowing administrators to quickly identify and resolve issues.

Script Dependencies and Installation

Before deploying this script, ensure your Linux server has the required components installed and configured.

Required Software Components

The script depends on the following tools, most of which are standard on Debian/Ubuntu systems:

  • bash - Shell interpreter (pre-installed)
  • openssl - For SSL certificate operations
  • mail (mailutils package) - For sending email notifications
  • host (bind9-host package) - For DNS resolution checks
  • virtualmin - Control panel (must be already installed)
  • timeout (coreutils) - For connection timeouts

Installing Dependencies on Debian/Ubuntu

Install the required packages using apt:

sudo apt update
sudo apt install -y openssl mailutils bind9-host coreutils

Verify Virtualmin is installed and accessible:

which virtualmin
# Should output: /usr/sbin/virtualmin

virtualmin list-domains --with-feature ssl --name-only
# Should list your SSL-enabled domains

Configuring Email Delivery with Postfix

The script sends notifications via the system's mail command, which typically uses Postfix. For reliable email delivery, especially from cloud servers where port 25 is often blocked, configure Postfix to relay through an SMTP server.

Basic Postfix Configuration (/etc/postfix/main.cf):

# Basic SMTP relay configuration
relayhost = [smtp.yourmailprovider.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
smtp_use_tls = yes

Create SMTP credentials file (/etc/postfix/sasl_passwd):

[smtp.yourmailprovider.com]:587 your-email@domain.tld:your-smtp-password

Generate the password database and restart Postfix:

sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
sudo systemctl restart postfix

# Test email delivery
echo "Test email from SSL monitoring script" | mail -s "Test" your-email@domain.tld

Script Configuration Guide

The script includes a comprehensive configuration section at the top of the file. All customizable values are clearly documented and must be adjusted for your environment.

Email Configuration

Configure the sender and recipient addresses for notifications:

# Email sender address for notifications
MAIL_SENDER="serveradmin@yourdomain.com"

# Recipients who will receive SSL certificate status notifications
MAIL_RECIPIENTS="serveradmin@yourdomain.com"

You can specify multiple recipients by separating email addresses with spaces or commas, depending on your mail command configuration.

Warning Threshold Configuration

Set how many days before expiration warnings should be triggered:

# Number of days before certificate expiration when warnings should be sent
DAYS_BEFORE_WARNING=10

A value of 10 days is recommended, providing sufficient time for automatic renewal and manual intervention if renewal fails. Let's Encrypt certificates are valid for 90 days, and standard practice is to renew them after 60 days.

Domain Exclusion List

Exclude specific domains from monitoring (test domains, placeholder domains, etc.):

# List of domains to exclude from SSL certificate checks
EXCLUDED_DOMAINS="test.example.com placeholder.example.net"

Manual Certificate Checks

For services outside Virtualmin (mail servers, custom applications, etc.), configure manual certificate file checks:

MANUAL_CERT_CHECKS=(
    "path=/etc/ssl/mail-server/cert.pem;name=Mail Server Certificate"
    "path=/etc/ssl/custom-app/cert.pem;name=Custom Application Certificate"
)

These certificates will be monitored but cannot be automatically renewed. The script will send notifications when they approach expiration, requiring manual renewal.

File Paths Configuration

Verify or adjust system paths if your distribution uses different locations:

# Log file where SSL check results and errors will be stored
MAILLOG="/var/log/ssl-check-all-hosts.log"

# Path to the mail binary for sending notifications
MAIL_PATH="/usr/bin/mail"

# Path to the Virtualmin binary
VIRTUALMIN_BIN="/usr/sbin/virtualmin"

Email Notification Examples

The script generates two types of email notifications: individual domain reports and a daily summary.

Individual Domain Report Example

When a certificate is approaching expiration, the script sends a detailed report for that specific domain. Here's what administrators receive:

SSL Certificate Check Report for domain.tld
===========================================

WARNING: Certificate expires in 8 days

Current Certificate Details:
----------------------------
notBefore=Sep  4 15:21:49 2025 GMT
notAfter=Dec  3 15:21:48 2025 GMT
subject=CN = domain.tld
issuer=C = US, O = Let's Encrypt, CN = E8

=== Attempting Certificate Renewal ===

Executing: virtualmin generate-letsencrypt-cert --renew --validate-first --dns-check --ec --domain domain.tld

Checking hostnames for resolvability ..
.. all hostnames can be resolved

Requesting SSL certificate for domain.tld www.domain.tld mail.domain.tld admin.domain.tld from Let's Encrypt ..
.. done for all hostnames

Copying to server configuration ..
.. done

Certificate renewal SUCCESSFUL for domain.tld

=== Post-Renewal Certificate Check ===
New certificate expires in 89 days

notBefore=Nov 25 09:10:55 2025 GMT
notAfter=Feb 23 09:10:54 2026 GMT
subject=CN = domain.tld
issuer=C = US, O = Let's Encrypt, CN = E8

The email subject line clearly indicates the status: [RENEWED], [WARNING], or [ERROR], allowing administrators to prioritize their response.

Daily Summary Email

At the end of each run, the script sends a comprehensive summary showing the status of all domains:

SSL Certificate Check Summary
=============================
Date: 2025-11-25 09:15:32
Warning threshold: 10 days

[OK] domain1.tld - Expires in 45 days
[OK] domain2.tld - Expires in 67 days
[RENEWED] domain3.tld - Was 8 days, now 89 days
[OK] domain4.tld - Expires in 23 days
[WARNING] domain5.tld - Expires in 5 days, renewal FAILED
[ERROR] domain6.tld - DNS resolution failed
[OK] Mail Server Certificate - Expires in 34 days

=============================
Summary Statistics
=============================
Total domains checked: 7
Healthy domains: 5
Domains with issues: 2

Report generated: 2025-11-25 09:15:45

The summary email subject line indicates overall status: [SUCCESS] if all domains are healthy, [WARNING] if any certificates are expiring but renewals succeeded, or [ERROR] if critical issues were detected.

Complete Script Code

Below is the complete, production-ready bash script. Copy this code, customize the configuration section at the top, and deploy it on your Virtualmin server:

#! /bin/bash

# This script checks SSL certificate validity for all Virtualmin-managed domains
# It performs DNS resolution checks and sends email notifications for expiring certificates
# and any DNS resolution failures
#
# Workflow per domain:
#   1. Verify DNS resolution (send individual email if it fails)
#   2. Check SSL certificate expiry
#   3. If certificate has any issue (DNS error, retrieval error, or expiring soon):
#      a. Send individual email notification for that domain
#      b. For expiring certificates, attempt automatic renewal first
#   4. At the end, send a summary email with all domain statuses

# File paths and system configurations
# ===================================

# Manual certificate checks configuration
# This array allows you to specify local certificate files that should be checked directly
# Each entry should contain:
#   - path: The full path to the certificate file
#   - name: A descriptive name for the certificate (used in logs)
# Example entry: ("path=/path/to/cert.pem;name=Description of cert")
# Leave the array empty () if you don't have any manual checks to perform
MANUAL_CERT_CHECKS=(
    # Add certificates as needed, one per line:
    # "path=/path/to/cert.pem;name=Mail Server Certificate"
    # "path=/etc/ssl/custom/cert.pem;name=Custom Service Certificate"
)

# Log file where SSL check results and errors will be stored (summary log)
MAILLOG="/var/log/ssl-check-all-hosts.log"

# Path to the mail binary for sending notifications
MAIL_PATH="/usr/bin/mail"

# Email sender address for notifications
# This can be modified using the 'chfn' command to set the user's full name
# Example: sudo chfn -f "SSL Monitor" serveradmin
MAIL_SENDER="serveradmin@yourdomain.com"

# Recipients who will receive SSL certificate status notifications
MAIL_RECIPIENTS="serveradmin@yourdomain.com"

# Path to the Virtualmin binary for managing virtual servers
VIRTUALMIN_BIN="/usr/sbin/virtualmin"

# Number of days before certificate expiration when warnings should be sent
# Certificates expiring within this window will trigger notification emails
DAYS_BEFORE_WARNING=10

# List of domains to exclude from SSL certificate checks
# Add domain names separated by spaces
# Example: "domain1.com domain2.com domain3.net"
EXCLUDED_DOMAINS="test.example.com placeholder.example.net"

# Status tracking variables
# =======================

# Tracks overall script status - changes to [ERROR] if any issues are found
SUMMARY_STATUS="[SUCCESS]"

# Boolean flag to track if any DNS resolution errors occurred
HAS_DNS_ERROR=false

# Counter for domains with issues
DOMAINS_WITH_ISSUES=0

# Functions
# =========

# Checks if a domain is in the exclusion list
# Parameters:
#   $1: Domain name to check
# Returns:
#   0 if domain should be excluded
#   1 if domain should be included
is_excluded() {
    local domain="$1"
    for excluded in $EXCLUDED_DOMAINS; do
        if [ "$domain" = "$excluded" ]; then
            return 0
        fi
    done
    return 1
}

# Check SSL certificate expiry for a domain
# Parameters:
#   $1: Domain name or IP
#   $2: Port (default 443)
# Returns:
#   Exit code: 0 on success, 1 on error
# Outputs:
#   Days until expiry (negative if expired), or "ERROR" on failure
get_cert_expiry_days() {
    local host="$1"
    local port="${2:-443}"
    local cert_info
    local expiry_date
    local expiry_epoch
    local now_epoch
    local days_left

    # Get certificate info
    cert_info=$(echo | timeout 10 openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null | openssl x509 -noout -dates -subject 2>/dev/null)

    if [ -z "$cert_info" ]; then
        echo "ERROR"
        return 1
    fi

    # Extract expiry date
    expiry_date=$(echo "$cert_info" | grep "notAfter" | cut -d= -f2)

    if [ -z "$expiry_date" ]; then
        echo "ERROR"
        return 1
    fi

    # Convert to epoch and calculate days
    expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    echo "$days_left"
    return 0
}

# Get certificate details for reporting
# Parameters:
#   $1: Domain name
#   $2: Port (default 443)
get_cert_details() {
    local host="$1"
    local port="${2:-443}"

    echo | timeout 10 openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null | openssl x509 -noout -dates -subject -issuer 2>/dev/null
}

# Check a local certificate file
# Parameters:
#   $1: Path to certificate file
# Returns:
#   Exit code: 0 on success, 1 on error
# Outputs:
#   Days until expiry (negative if expired), or "ERROR" on failure
get_local_cert_expiry_days() {
    local cert_path="$1"
    local expiry_date
    local expiry_epoch
    local now_epoch
    local days_left

    if [ ! -f "$cert_path" ]; then
        echo "ERROR"
        return 1
    fi

    expiry_date=$(openssl x509 -noout -enddate -in "$cert_path" 2>/dev/null | cut -d= -f2)

    if [ -z "$expiry_date" ]; then
        echo "ERROR"
        return 1
    fi

    expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    echo "$days_left"
    return 0
}

# Get local certificate details
# Parameters:
#   $1: Path to certificate file
get_local_cert_details() {
    local cert_path="$1"
    openssl x509 -noout -dates -subject -issuer -in "$cert_path" 2>/dev/null
}

# Attempt to renew SSL certificate for a domain using Virtualmin
# Parameters:
#   $1: Domain name to renew certificate for
# Outputs:
#   The full output from the virtualmin command
renew_certificate() {
    local hostname="$1"
    local exit_code

    echo "Executing: virtualmin generate-letsencrypt-cert --renew --validate-first --dns-check --ec --domain ${hostname}"
    echo ""

    ${VIRTUALMIN_BIN} generate-letsencrypt-cert --renew --validate-first --dns-check --ec --domain "${hostname}" 2>&1
    exit_code=$?

    echo ""
    if [ $exit_code -eq 0 ]; then
        echo "Certificate renewal SUCCESSFUL for ${hostname}"
        return 0
    else
        echo "Certificate renewal FAILED for ${hostname} (exit code: ${exit_code})"
        return 1
    fi
}

# Send email for a specific domain
# Parameters:
#   $1: Domain name
#   $2: Status (SUCCESS/WARNING/ERROR)
#   $3: Log file path
send_domain_email() {
    local domain="$1"
    local status="$2"
    local log_file="$3"

    sudo -u "$MAIL_SENDER" "$MAIL_PATH" -s "[${status}] SSL Certificate Report: ${domain}" \
        "$MAIL_RECIPIENTS" < "$log_file" } # Main Script Logic # ================ # Verify required utilities are available if [ ! -x "$MAIL_PATH" ]; then echo "Cannot find [ mail ]. Is it installed?" >&2
    exit 1
fi

if ! command -v openssl &> /dev/null; then
    echo "Cannot find [ openssl ]. Is it installed?" >&2
    exit 1
fi

# Create temporary files for processing
TEMP_DOMAIN_LIST=$(mktemp)
DNS_ERROR_LOG=$(mktemp)

# Initialize the summary log
rm -f "$MAILLOG"
touch "$MAILLOG"

echo "SSL Certificate Check Summary" >> "$MAILLOG"
echo "=============================" >> "$MAILLOG"
echo "Date: $(date '+%Y-%m-%d %H:%M:%S')" >> "$MAILLOG"
echo "Warning threshold: ${DAYS_BEFORE_WARNING} days" >> "$MAILLOG"
echo "" >> "$MAILLOG"

# Retrieve list of SSL-enabled domains from Virtualmin
"${VIRTUALMIN_BIN}" list-domains --with-feature ssl --name-only > "${TEMP_DOMAIN_LIST}"

TOTAL_DOMAINS=0
HEALTHY_DOMAINS=0

# Process each domain individually
while IFS= read -r domain; do
    if is_excluded "$domain"; then
        continue
    fi

    ((TOTAL_DOMAINS++))

    # Verify DNS resolution
    if ! host "$domain" >/dev/null 2>&1; then
        echo "DNS resolution failed for ${domain}" >> "$DNS_ERROR_LOG"
        HAS_DNS_ERROR=true
        SUMMARY_STATUS="[ERROR]"
        ((DOMAINS_WITH_ISSUES++))

        # Send email for DNS failure
        DOMAIN_LOG=$(mktemp)
        echo "SSL Certificate Check Report for ${domain}" > "$DOMAIN_LOG"
        echo "===========================================" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"
        echo "ERROR: DNS resolution failed for ${domain}" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"
        echo "The domain could not be resolved. Please check:" >> "$DOMAIN_LOG"
        echo "  - DNS configuration" >> "$DOMAIN_LOG"
        echo "  - Domain registration status" >> "$DOMAIN_LOG"

        send_domain_email "$domain" "ERROR" "$DOMAIN_LOG"
        rm -f "$DOMAIN_LOG"

        echo "[ERROR] ${domain} - DNS resolution failed" >> "$MAILLOG"
        continue
    fi

    # Check certificate expiry
    days_left=$(get_cert_expiry_days "$domain" 443)

    if [ "$days_left" = "ERROR" ]; then
        # Could not retrieve certificate
        SUMMARY_STATUS="[ERROR]"
        ((DOMAINS_WITH_ISSUES++))

        DOMAIN_LOG=$(mktemp)
        echo "SSL Certificate Check Report for ${domain}" > "$DOMAIN_LOG"
        echo "===========================================" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"
        echo "ERROR: Could not retrieve SSL certificate for ${domain}" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"
        echo "Possible causes:" >> "$DOMAIN_LOG"
        echo "  - No SSL certificate installed" >> "$DOMAIN_LOG"
        echo "  - Connection timeout" >> "$DOMAIN_LOG"
        echo "  - Firewall blocking port 443" >> "$DOMAIN_LOG"

        send_domain_email "$domain" "ERROR" "$DOMAIN_LOG"
        rm -f "$DOMAIN_LOG"

        echo "[ERROR] ${domain} - Could not retrieve certificate" >> "$MAILLOG"
        continue
    fi

    if [ "$days_left" -le "$DAYS_BEFORE_WARNING" ]; then
        # Certificate is expiring soon or already expired
        SUMMARY_STATUS="[WARNING]"
        ((DOMAINS_WITH_ISSUES++))

        # Create per-domain log for email
        DOMAIN_LOG=$(mktemp)

        echo "SSL Certificate Check Report for ${domain}" > "$DOMAIN_LOG"
        echo "===========================================" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"

        if [ "$days_left" -lt 0 ]; then
            echo "CRITICAL: Certificate has EXPIRED (${days_left} days ago)" >> "$DOMAIN_LOG"
        else
            echo "WARNING: Certificate expires in ${days_left} days" >> "$DOMAIN_LOG"
        fi

        echo "" >> "$DOMAIN_LOG"
        echo "Current Certificate Details:" >> "$DOMAIN_LOG"
        echo "----------------------------" >> "$DOMAIN_LOG"
        get_cert_details "$domain" 443 >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"

        echo "=== Attempting Certificate Renewal ===" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"

        # Attempt renewal and capture output
        renewal_output=$(renew_certificate "$domain")
        renewal_exit=$?

        echo "$renewal_output" >> "$DOMAIN_LOG"
        echo "" >> "$DOMAIN_LOG"

        # Check new certificate status after renewal
        if [ $renewal_exit -eq 0 ]; then
            echo "=== Post-Renewal Certificate Check ===" >> "$DOMAIN_LOG"
            new_days=$(get_cert_expiry_days "$domain" 443)
            echo "New certificate expires in ${new_days} days" >> "$DOMAIN_LOG"
            echo "" >> "$DOMAIN_LOG"
            get_cert_details "$domain" 443 >> "$DOMAIN_LOG"

            send_domain_email "$domain" "RENEWED" "$DOMAIN_LOG"
            echo "[RENEWED] ${domain} - Was ${days_left} days, now ${new_days} days" >> "$MAILLOG"
        else
            send_domain_email "$domain" "WARNING" "$DOMAIN_LOG"
            echo "[WARNING] ${domain} - Expires in ${days_left} days, renewal FAILED" >> "$MAILLOG"
        fi

        rm -f "$DOMAIN_LOG"
    else
        # Certificate is healthy
        ((HEALTHY_DOMAINS++))
        echo "[OK] ${domain} - Expires in ${days_left} days" >> "$MAILLOG"
    fi

done < "${TEMP_DOMAIN_LIST}" # Process manual certificate checks if [ ${#MANUAL_CERT_CHECKS[@]} -gt 0 ]; then echo "" >> "$MAILLOG"
    echo "=== Manual Certificate Checks ===" >> "$MAILLOG"

    for cert_entry in "${MANUAL_CERT_CHECKS[@]}"; do
        cert_path=$(echo "$cert_entry" | sed 's/^path=\([^;]*\);.*$/\1/')
        cert_name=$(echo "$cert_entry" | sed 's/^.*;name=\(.*\)$/\1/')

        if [ ! -f "$cert_path" ]; then
            echo "[ERROR] ${cert_name} - File not found: ${cert_path}" >> "$MAILLOG"
            SUMMARY_STATUS="[ERROR]"
            ((DOMAINS_WITH_ISSUES++))
            continue
        fi

        days_left=$(get_local_cert_expiry_days "$cert_path")

        if [ "$days_left" = "ERROR" ]; then
            echo "[ERROR] ${cert_name} - Could not parse certificate" >> "$MAILLOG"
            SUMMARY_STATUS="[ERROR]"
            ((DOMAINS_WITH_ISSUES++))
        elif [ "$days_left" -le "$DAYS_BEFORE_WARNING" ]; then
            SUMMARY_STATUS="[WARNING]"
            ((DOMAINS_WITH_ISSUES++))

            # Send email for manual cert (cannot auto-renew)
            DOMAIN_LOG=$(mktemp)
            echo "SSL Certificate Check Report: ${cert_name}" > "$DOMAIN_LOG"
            echo "===========================================" >> "$DOMAIN_LOG"
            echo "" >> "$DOMAIN_LOG"
            echo "Certificate file: ${cert_path}" >> "$DOMAIN_LOG"
            echo "" >> "$DOMAIN_LOG"

            if [ "$days_left" -lt 0 ]; then
                echo "CRITICAL: Certificate has EXPIRED (${days_left} days ago)" >> "$DOMAIN_LOG"
            else
                echo "WARNING: Certificate expires in ${days_left} days" >> "$DOMAIN_LOG"
            fi

            echo "" >> "$DOMAIN_LOG"
            echo "Certificate Details:" >> "$DOMAIN_LOG"
            echo "-------------------" >> "$DOMAIN_LOG"
            get_local_cert_details "$cert_path" >> "$DOMAIN_LOG"
            echo "" >> "$DOMAIN_LOG"
            echo "NOTE: This is a manually managed certificate. Automatic renewal is not available." >> "$DOMAIN_LOG"
            echo "Please renew this certificate manually." >> "$DOMAIN_LOG"

            send_domain_email "$cert_name" "WARNING" "$DOMAIN_LOG"
            rm -f "$DOMAIN_LOG"

            echo "[WARNING] ${cert_name} - Expires in ${days_left} days (manual renewal required)" >> "$MAILLOG"
        else
            echo "[OK] ${cert_name} - Expires in ${days_left} days" >> "$MAILLOG"
            ((HEALTHY_DOMAINS++))
        fi
    done
fi

# Add DNS errors to summary if any occurred
if [ "$HAS_DNS_ERROR" = true ]; then
    echo "" >> "$MAILLOG"
    echo "=== DNS Resolution Errors ===" >> "$MAILLOG"
    cat "$DNS_ERROR_LOG" >> "$MAILLOG"
fi

# Add summary statistics
echo "" >> "$MAILLOG"
echo "=============================" >> "$MAILLOG"
echo "Summary Statistics" >> "$MAILLOG"
echo "=============================" >> "$MAILLOG"
echo "Total domains checked: ${TOTAL_DOMAINS}" >> "$MAILLOG"
echo "Healthy domains: ${HEALTHY_DOMAINS}" >> "$MAILLOG"
echo "Domains with issues: ${DOMAINS_WITH_ISSUES}" >> "$MAILLOG"
echo "" >> "$MAILLOG"
echo "Report generated: $(date '+%Y-%m-%d %H:%M:%S')" >> "$MAILLOG"

# Send summary email
sudo -u "$MAIL_SENDER" "$MAIL_PATH" -s "${SUMMARY_STATUS} Daily SSL Certificate Check Summary" \
    "$MAIL_RECIPIENTS" < "$MAILLOG"

# Clean up temporary files
rm -f "$TEMP_DOMAIN_LIST" "$DNS_ERROR_LOG"

Deployment and Scheduling

After customizing the script configuration, follow these steps to deploy it on your Virtualmin server:

Script Installation

# Create the script file
sudo nano /usr/local/bin/ssl-check-all-hosts

# Paste the complete script code (after customizing configuration)
# Save and exit (Ctrl+X, Y, Enter)

# Make the script executable
sudo chmod +x /usr/local/bin/ssl-check-all-hosts

# Test the script manually
sudo /usr/local/bin/ssl-check-all-hosts

The first run will check all SSL-enabled domains in Virtualmin and send email notifications. Review the emails to ensure formatting and delivery work correctly.

Automated Daily Execution via Cron

Schedule the script to run daily using cron. This ensures continuous monitoring without manual intervention:

# Edit root's crontab
sudo crontab -e

# Add this line to run daily at 6:00 AM
0 6 * * * /usr/local/bin/ssl-check-all-hosts

# Or run at 3:00 AM for less active hours
0 3 * * * /usr/local/bin/ssl-check-all-hosts

Choose a time during low-traffic hours to minimize any potential impact from certificate renewals requiring service restarts.

Log File Management

The script writes logs to /var/log/ssl-check-all-hosts.log. Configure logrotate to prevent excessive disk usage:

# Create logrotate configuration
sudo nano /etc/logrotate.d/ssl-check-all-hosts

Add this configuration:

/var/log/ssl-check-all-hosts.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
}

This keeps 30 days of log history with compression, providing sufficient audit trail while managing disk space efficiently.

Security Considerations

When deploying automation scripts with system-level access, security must be a primary concern:

Script Permissions

  • The script file should be owned by root with 755 permissions (readable/executable by all, writable only by root)
  • Prevent unauthorized modification: sudo chown root:root /usr/local/bin/ssl-check-all-hosts
  • Log files should have restricted permissions: sudo chmod 640 /var/log/ssl-check-all-hosts.log

Email Security

  • Protect SMTP credentials in /etc/postfix/sasl_passwd with 600 permissions
  • Use TLS/SSL encryption for SMTP connections (port 587 with STARTTLS or port 465 with SSL)
  • Consider using application-specific passwords for SMTP authentication
  • Review email recipients regularly to ensure notifications reach appropriate administrators

Execution Context

  • The script runs as root (required for Virtualmin operations and sending mail as configured user)
  • Review script changes carefully before deployment
  • Test modifications in a staging environment when possible

Troubleshooting Common Issues

No Emails Received

If you're not receiving email notifications:

  1. Verify Postfix is running: sudo systemctl status postfix
  2. Check mail logs: sudo tail -f /var/log/mail.log
  3. Test manual email: echo "test" | mail -s "test" your-email@domain.tld
  4. Verify SMTP credentials in /etc/postfix/sasl_passwd
  5. Check spam/junk folders for script notifications

DNS Resolution Failures

If domains fail DNS checks unexpectedly:

  1. Verify DNS server configuration: cat /etc/resolv.conf
  2. Test manual resolution: host domain.tld
  3. Check network connectivity to DNS servers
  4. Consider adding specific domains to the exclusion list if they're intentionally offline

Certificate Renewal Failures

If automatic renewals fail:

  1. Review detailed renewal output in the email notification
  2. Verify Let's Encrypt rate limits haven't been hit (5 renewals per domain per week)
  3. Check DNS records point to the correct server
  4. Ensure port 80 is accessible for Let's Encrypt validation
  5. Manually attempt renewal: sudo virtualmin generate-letsencrypt-cert --renew --domain domain.tld

Script Not Executing via Cron

If the cron job doesn't run:

  1. Verify cron service is running: sudo systemctl status cron
  2. Check crontab syntax: sudo crontab -l
  3. Review cron logs: sudo grep ssl-check /var/log/syslog
  4. Ensure the script path is absolute in crontab
  5. Verify script permissions allow execution

Best Practices for Production Use

To maximize the effectiveness of this SSL monitoring solution:

  • Set appropriate warning thresholds: 10 days provides adequate time for troubleshooting failed renewals. Shorter windows risk expired certificates during weekends or holidays.
  • Monitor email delivery: Regularly verify you're receiving daily summary emails. Silent failures leave you unaware of certificate issues.
  • Review renewal failures promptly: When renewals fail, investigate immediately. Common causes include DNS issues, rate limits, or firewall changes.
  • Test in staging first: Before deploying to production, test the script with a few domains to verify email formatting and renewal behavior.
  • Document customizations: Maintain notes about configuration changes, especially exclusion lists and manual certificate entries.
  • Keep Virtualmin updated: SSL renewal commands may change between versions. Test script compatibility after major updates.
  • Backup before renewals: While Let's Encrypt renewals are generally safe, consider taking Virtualmin backups before mass renewal events.

Conclusion

SSL certificate management is a critical but often overlooked aspect of server administration. Expired certificates cause immediate user-facing problems, damage trust, and can impact search engine rankings. For Linux administrators managing multiple domains on Virtualmin, manual certificate monitoring simply doesn't scale.

This comprehensive bash script provides enterprise-grade SSL automation specifically designed for Virtualmin environments. By combining certificate expiry monitoring, automatic Let's Encrypt renewal with modern ECDSA support, DNS validation, and detailed email notifications, it eliminates the manual overhead of SSL management while ensuring your domains remain secure and accessible.

The script runs silently in the background via cron, only alerting administrators when action is required. Successful renewals happen automatically, while failures receive detailed diagnostics to enable rapid resolution. The daily summary provides visibility into your entire SSL certificate infrastructure at a glance.

Deploy this solution today to transform SSL certificate management from a recurring manual task into a fully automated, monitored system. Your future self will thank you when dozens of certificates renew seamlessly without intervention, and you're immediately notified of the rare exceptions that need attention.

For questions, improvements, or to share your deployment experiences, feel free to reach out or contribute enhancements to the script. Effective automation benefits the entire systems administration community.