Before you start
You need a Linux server with shell access and these tools. Most distributions ship all of them.
| Requirement | Why | Check with |
|---|---|---|
| Bash | The script uses Bash syntax ([[ ]], +=) | bash --version |
| OpenSSL | Connects to each domain and reads the certificate | openssl version |
GNU date | Converts the expiry date to seconds (date -d) | date --version |
curl | Sends the alert through the Forward Email API | curl --version |
sendmail (optional) | Sends the alert through your local mail server instead of the API | command -v sendmail |
| Root access | Default paths are /etc/ssl_monitor and /var/log | sudo -v |
macOS ships BSD date, which does not support date -d, so the script reports wrong day counts there unless you install GNU coreutils and change date to gdate.
To send alerts with the Forward Email API you need a Forward Email account on a paid plan, because outbound SMTP comes with all paid plans. The sender address must be an alias on a domain in your account, and the Email API docs ask you to set up DKIM, Return-Path and DMARC for that domain first.
1. Download the script
Save the script to /usr/local/bin and make it executable:
sudo curl -fsSL -o /usr/local/bin/ssl_monitor.sh \
https://raw.githubusercontent.com/forwardemail/sslmonitor.com/main/ssl_monitor.sh
sudo chmod +x /usr/local/bin/ssl_monitor.sh
You can also copy the full script from the bottom of this page. Read it before you run it as root.
2. Set your email settings
Open the script and edit the configuration block at the top:
sudo nano /usr/local/bin/ssl_monitor.sh
| Variable | Default | What to set |
|---|---|---|
EMAIL_TO | admin@yourdomain.com | Where alerts go. Separate several addresses with commas. |
EMAIL_FROM | ssl-alerts@yourdomain.com | Sender address. With the API it must be an alias on your Forward Email domain. |
FORWARD_EMAIL_API_KEY | your_api_key_here | Your API token from the Security page of your Forward Email account. |
DOMAINS_FILE | /etc/ssl_monitor/domains.txt | The list of domains to check. |
WARNING_DAYS | 30 | A certificate with fewer days left than this is WARNING. |
CRITICAL_DAYS | 7 | A certificate with fewer days left than this is CRITICAL. |
LOG_FILE | /var/log/ssl_monitor.log | Where results are appended. |
The API token lets anyone who holds it use the Forward Email API as you, so keep the script readable by root only:
sudo chmod 700 /usr/local/bin/ssl_monitor.sh
If your certificates renew automatically, set WARNING_DAYS lower than the number of days left when your client renews, or you may get a warning on a normal renewal day. Certificate lifetimes has suggested values.
3. List your domains
Create the domains file with one hostname per line. Use the exact name the certificate is served for, such as www.example.com, without https:// or a port.
sudo mkdir -p /etc/ssl_monitor
sudo nano /etc/ssl_monitor/domains.txt
# Websites
example.com
www.example.com
# Mail server (must answer TLS on port 443)
mail.example.com
If the file does not exist, the script creates it on its first run with example.com and mail.example.com as samples and then checks those samples. Create your own file first to avoid a confusing first report.
The script always connects to port 443. A line such as example.com:8443 does not work.
4. Choose how alerts are sent
The script sends alerts through the Forward Email API by default. To use your server's sendmail command instead, swap which line is commented near the end of the script:
# Uncomment one of these methods based on your preference:
# send_email_api "$EMAIL_SUBJECT" "$HTML_REPORT"
send_email_smtp "$EMAIL_SUBJECT" "$HTML_REPORT"
The sendmail method needs a local mail transfer agent (for example Postfix or msmtp) that can deliver mail. To relay it through Forward Email, point that agent at smtp.forwardemail.net on port 465 (SSL/TLS) or 587 (STARTTLS) with the full alias address as the username and its generated password, as listed in the Forward Email FAQ. Email alerts explains both methods in detail.
5. Test the script
Run it once by hand and read the log:
sudo /usr/local/bin/ssl_monitor.sh
sudo tail -n 20 /var/log/ssl_monitor.log
A healthy run logs lines like these:
Checking certificate for example.com...
example.com: OK - Expires: Jan 14 23:59:59 2027 GMT (87 days left)
No alerts needed, all certificates are valid
No email is sent when every domain is OK. To test email delivery, set WARNING_DAYS=400 for one run: every publicly trusted certificate has fewer than 400 days left, so each domain becomes WARNING and the script sends a report. Set it back afterwards.
With the API method the log says Alert email sent via API when Forward Email accepts the request and Failed to send alert email via API when it does not. curl prints the reason to the terminal.
6. Schedule it with cron
Open root's crontab:
sudo crontab -e
Add a line to run the check every day at 08:00 server time:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 8 * * * /usr/local/bin/ssl_monitor.sh
The PATH line matters for the sendmail method: cron starts jobs with a short default PATH that often leaves out /usr/sbin, where sendmail lives. Once a day is enough for expiry checks. Each run with a problem sends a new report, so a certificate that stays in WARNING sends one email per day until you fix it.
7. Rotate the log
The script appends to its log on every run and never trims it. A small logrotate rule keeps it in check:
sudo tee /etc/logrotate.d/ssl_monitor > /dev/null <<'EOF'
/var/log/ssl_monitor.log {
monthly
rotate 6
compress
missingok
notifempty
}
EOF
Run it without root
To run the script as a normal user, change DOMAINS_FILE and LOG_FILE to paths that user can write, create the directory for the domains file yourself, and change the mkdir -p /etc/ssl_monitor line to that directory. Then add the cron line with crontab -e as that user.
Update or remove
To update, download the script again and copy your settings back into the configuration block. To remove it, delete the cron line, then run:
sudo rm /usr/local/bin/ssl_monitor.sh /var/log/ssl_monitor.log
sudo rm -r /etc/ssl_monitor
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ERROR with an empty expiry date | The name does not resolve, port 443 is closed or blocked, or the server does not speak TLS | Run the openssl s_client command from the command line guide for that domain and read the error |
| Days left look wrong | BSD date (macOS) or a wrong system clock | Use GNU date and check the clock with timedatectl |
Failed to send alert email via API | Wrong API token, no paid plan, or EMAIL_FROM is not an alias on your domain | Check the token on your account Security page and the sender alias |
No email with the sendmail method | No working mail transfer agent, or sendmail not on cron's PATH | Run echo test | sendmail you@example.com by hand and add the PATH line to the crontab |
| The script hangs on one domain | A firewall drops packets to port 443, so openssl waits for the TCP timeout | Remove the domain or fix the firewall |
Full script
This is the current ssl_monitor.sh from the GitHub repository.
#!/bin/bash
# SSL Certificate Monitor Script
# This script monitors SSL certificate expiration dates and sends alerts via Forward Email
# Usage: Place in /usr/local/bin/ and add to crontab to run daily
# Example crontab entry: 0 8 * * * /usr/local/bin/ssl_monitor.sh
# Configuration
EMAIL_TO="admin@yourdomain.com"
EMAIL_FROM="ssl-alerts@yourdomain.com"
FORWARD_EMAIL_API_KEY="your_api_key_here" # Get from https://forwardemail.net/my-account/security
DOMAINS_FILE="/etc/ssl_monitor/domains.txt" # One domain per line
WARNING_DAYS=30 # Send warning when certificate expires in less than this many days
CRITICAL_DAYS=7 # Send critical alert when certificate expires in less than this many days
LOG_FILE="/var/log/ssl_monitor.log"
# Create config directory and domains file if they don't exist
mkdir -p /etc/ssl_monitor
if [ ! -f "$DOMAINS_FILE" ]; then
echo "# Add domains to monitor, one per line" > "$DOMAINS_FILE"
echo "example.com" >> "$DOMAINS_FILE"
echo "mail.example.com" >> "$DOMAINS_FILE"
fi
# Function to send email via Forward Email HTTP API
send_email_api() {
local subject="$1"
local body="$2"
local text
# Plain text version of the report (HTML tags and blank lines removed)
text=$(echo "$body" | sed -e 's/<[^>]*>//g' -e '/^[[:space:]]*$/d')
# Form-encode every field so newlines and quotes in the report are sent safely
if curl -sS --fail -X POST "https://api.forwardemail.net/v1/emails" \
-u "$FORWARD_EMAIL_API_KEY:" \
--data-urlencode "from=$EMAIL_FROM" \
--data-urlencode "to=$EMAIL_TO" \
--data-urlencode "subject=$subject" \
--data-urlencode "html=$body" \
--data-urlencode "text=$text" \
-o /dev/null; then
echo "Alert email sent via API at $(date)" >> "$LOG_FILE"
else
echo "Failed to send alert email via API at $(date)" >> "$LOG_FILE"
fi
}
# Function to send email via sendmail (SMTP)
send_email_smtp() {
local subject="$1"
local body="$2"
echo -e "Subject: $subject\nFrom: $EMAIL_FROM\nTo: $EMAIL_TO\nContent-Type: text/html\n\n$body" | \
sendmail -t
echo "Alert email sent via SMTP at $(date)" >> "$LOG_FILE"
}
# Function to check SSL certificate expiration
check_certificate() {
local domain="$1"
local expiry_date=""
local days_left=0
local status="OK"
local error_message=""
# Get certificate expiration date
expiry_date=$(echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | \
sed -e 's/notAfter=//')
if [ -z "$expiry_date" ]; then
status="ERROR"
error_message="Could not retrieve certificate for $domain"
days_left=0
else
# Convert expiry date to seconds since epoch
expiry_seconds=$(date -d "$expiry_date" +%s)
current_seconds=$(date +%s)
# Calculate days left
seconds_left=$((expiry_seconds - current_seconds))
days_left=$((seconds_left / 86400))
# Determine status
if [ $days_left -lt $CRITICAL_DAYS ]; then
status="CRITICAL"
elif [ $days_left -lt $WARNING_DAYS ]; then
status="WARNING"
fi
fi
# Return results
echo "$domain|$expiry_date|$days_left|$status|$error_message"
}
# Initialize HTML report
HTML_REPORT="<h2>SSL Certificate Monitoring Report</h2>
<p><strong>Date:</strong> $(date)</p>
<p><strong>Server:</strong> $(hostname)</p>
<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse;'>
<tr style='background-color: #f2f2f2;'>
<th>Domain</th>
<th>Expiry Date</th>
<th>Days Left</th>
<th>Status</th>
</tr>"
# Check each domain
ALERT_NEEDED=false
while read -r domain || [ -n "$domain" ]; do
# Skip comments and empty lines
[[ "$domain" =~ ^#.*$ || -z "$domain" ]] && continue
echo "Checking certificate for $domain..." >> "$LOG_FILE"
# Get certificate info
cert_info=$(check_certificate "$domain")
# Parse results
domain_name=$(echo "$cert_info" | cut -d'|' -f1)
expiry_date=$(echo "$cert_info" | cut -d'|' -f2)
days_left=$(echo "$cert_info" | cut -d'|' -f3)
status=$(echo "$cert_info" | cut -d'|' -f4)
error_message=$(echo "$cert_info" | cut -d'|' -f5)
# Set row color based on status
row_color="#ffffff" # Default white
if [ "$status" = "WARNING" ]; then
row_color="#fff3cd" # Light yellow
ALERT_NEEDED=true
elif [ "$status" = "CRITICAL" ]; then
row_color="#f8d7da" # Light red
ALERT_NEEDED=true
elif [ "$status" = "ERROR" ]; then
row_color="#f8d7da" # Light red
ALERT_NEEDED=true
fi
# Add to HTML report
HTML_REPORT+="<tr style='background-color: $row_color;'>
<td>$domain_name</td>
<td>$expiry_date</td>
<td>$days_left</td>
<td>$status</td>
</tr>"
# Log results
echo "$domain: $status - Expires: $expiry_date ($days_left days left)" >> "$LOG_FILE"
if [ -n "$error_message" ]; then
echo " Error: $error_message" >> "$LOG_FILE"
fi
done < "$DOMAINS_FILE"
# Complete HTML report
HTML_REPORT+="</table>
<p style='margin-top: 20px;'><em>This is an automated alert from your SSL certificate monitoring system.</em></p>
<p><strong>Note:</strong> It is recommended to renew certificates at least 14 days before expiration.</p>
<p><strong>Renewal Instructions:</strong></p>
<ol>
<li>For Let's Encrypt certificates: Run <code>certbot renew</code></li>
<li>For other certificates: Contact your certificate provider or generate a new CSR</li>
</ol>"
# Send alert if needed
if [ "$ALERT_NEEDED" = true ]; then
EMAIL_SUBJECT="SSL Certificate Alert: $(hostname) - $(date +%Y-%m-%d)"
# Uncomment one of these methods based on your preference:
send_email_api "$EMAIL_SUBJECT" "$HTML_REPORT"
# send_email_smtp "$EMAIL_SUBJECT" "$HTML_REPORT"
echo "Alert sent due to expiring certificates" >> "$LOG_FILE"
else
echo "No alerts needed, all certificates are valid" >> "$LOG_FILE"
fi
exit 0