# How to install SSLMonitor on Linux

> Install ssl_monitor.sh in about ten minutes: download it, set your email settings, list your domains, test it and add a daily cron job.

Source: https://sslmonitor.com/install/

## 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](https://forwardemail.net/en/faq). The sender address must be an alias on a domain in your account, and the [Email API docs](https://forwardemail.net/en/email-api) 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:

```bash
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](#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:

```bash
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](https://forwardemail.net/en/my-account/security) 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:

```bash
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](/certificate-lifetimes/#what-thresholds-should-you-use) 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.

```bash
sudo mkdir -p /etc/ssl_monitor
sudo nano /etc/ssl_monitor/domains.txt
```

```text
# 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:

```bash
    # 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](https://forwardemail.net/en/faq). [Email alerts](/ssl-certificate-expiry-alerts/) explains both methods in detail.

## 5. Test the script

Run it once by hand and read the log:

```bash
sudo /usr/local/bin/ssl_monitor.sh
sudo tail -n 20 /var/log/ssl_monitor.log
```

A healthy run logs lines like these:

```text
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:

```bash
sudo crontab -e
```

Add a line to run the check every day at 08:00 server time:

```text
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:

```bash
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:

```bash
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](/check-ssl-certificate-expiry-command-line/) 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](https://github.com/forwardemail/sslmonitor.com/blob/main/ssl_monitor.sh).

```bash
#!/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
```
