SSL certificates are one of those systems that work quietly in the background until something goes wrong. When a certificate expires, browsers immediately display warnings that can discourage visitors from continuing. Even if your server is running perfectly, an expired certificate creates the impression that your site is unsafe or broken. Preventing that situation requires visibility into certificate expiration before it becomes a problem.
Most of my servers use Let’s Encrypt with automatic renewal, which works reliably most of the time. However, renewal still depends on DNS configuration, firewall access, server availability, and correct certificate validation. If one of those pieces fails, renewal may not occur even though everything appears normal. Rather than relying entirely on automation, I prefer to actively monitor expiration dates.
This Python script connects to each domain, retrieves its SSL certificate, and reports how many days remain before expiration. This provides early warning if a certificate is approaching expiration or if something is misconfigured. It allows problems to be addressed proactively instead of reactively.
The Complete Script
Save this file as: check_ssl_expiration.py
#!/usr/bin/env python3
# Import modules required for SSL connections, networking,
# command-line arguments, and working with dates and time.
import ssl
import socket
import sys
from datetime import datetime, timezone
# Default HTTPS port used when none is specified.
DEFAULT_PORT = 443
# Number of days before expiration to trigger a warning.
DEFAULT_WARN_DAYS = 30
# Timeout in seconds when attempting to connect to a server.
DEFAULT_TIMEOUT = 5
def load_domains(filename):
"""
Load domain names from a text file.
Each line should contain either:
- domain.com
- domain.com:port
Blank lines and lines starting with '#' are ignored.
"""
domains = []
try:
# Open the domain list file for reading.
with open(filename, "r") as file:
# Read each line from the file.
for line in file:
# Remove whitespace and newline characters.
line = line.strip()
# Skip empty lines.
if not line:
continue
# Skip comment lines.
if line.startswith("#"):
continue
# Check if a custom port is specified.
if ":" in line:
# Split domain and port.
host, port = line.split(":", 1)
# Add domain and port to the list.
domains.append(
(host.strip(), int(port.strip()))
)
else:
# Use default HTTPS port.
domains.append(
(line, DEFAULT_PORT)
)
except FileNotFoundError:
# Exit if the domain file does not exist.
print(f"Domain file not found: {filename}")
sys.exit(1)
return domains
def get_certificate_expiration(host, port):
"""
Connect to the server and retrieve the SSL certificate
expiration date.
"""
# Create a secure SSL context using system defaults.
context = ssl.create_default_context()
# Establish a TCP connection to the server.
with socket.create_connection(
(host, port),
timeout=DEFAULT_TIMEOUT
) as sock:
# Wrap the socket in SSL to perform the TLS handshake.
with context.wrap_socket(
sock,
server_hostname=host
) as secure_sock:
# Retrieve certificate information.
cert = secure_sock.getpeercert()
# Extract expiration date from certificate.
expiration_str = cert["notAfter"]
# Convert expiration string into a datetime object.
expiration_date = datetime.strptime(
expiration_str,
"%b %d %H:%M:%S %Y %Z"
)
# Assign UTC timezone to expiration date.
expiration_date = expiration_date.replace(
tzinfo=timezone.utc
)
return expiration_date
def get_days_remaining(expiration_date):
"""
Calculate the number of days remaining until expiration.
"""
# Get current UTC time.
now = datetime.now(timezone.utc)
# Calculate time difference.
remaining = expiration_date - now
# Convert seconds to whole days.
return int(remaining.total_seconds() / 86400)
def main():
"""
Main program execution.
Loads domains, checks expiration dates,
and displays results.
"""
# Default domain file.
domain_file = "domains.txt"
# Default warning threshold.
warn_days = DEFAULT_WARN_DAYS
# Allow domain file to be specified via command-line.
if len(sys.argv) >= 2:
domain_file = sys.argv[1]
# Allow warning threshold to be specified.
if len(sys.argv) >= 3:
warn_days = int(sys.argv[2])
# Load domain list.
domains = load_domains(domain_file)
print(
f"Checking SSL expiration for {len(domains)} domain(s)"
)
print()
# Check each domain individually.
for host, port in domains:
# Format display label.
label = (
host if port == 443 else f"{host}:{port}"
)
try:
# Retrieve certificate expiration date.
expiration_date = get_certificate_expiration(
host,
port
)
# Calculate days remaining.
days_remaining = get_days_remaining(
expiration_date
)
# Format expiration date for display.
expiration_str = expiration_date.strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
# Display warning if expiration is near.
if days_remaining <= warn_days:
print(f"[WARN] {label}")
print(
f" Expires: {expiration_str}"
)
print(
f" Days remaining: {days_remaining}"
)
print()
else:
# Display normal status.
print(f"[OK] {label}")
print(
f" Expires: {expiration_str}"
)
print(
f" Days remaining: {days_remaining}"
)
print()
except Exception as e:
# Handle connection or certificate errors.
print(f"[ERROR] {label}")
print(f" {e}")
print()
# Run main function when script is executed directly.
if __name__ == "__main__":
main()
Creating the Domains File
This script reads domains from a simple text file in the same folder as the script. Each domain goes on its own line, which keeps the script clean and makes it easy to add or remove sites without editing Python code. I also like this approach because I can reuse the same script on different servers by swapping out the domain list. The script ignores blank lines and any line that starts with #, so you can leave notes in the file without breaking anything.
Create a file named domains.txt and add your domains like this:
google.com godaddy.com netflix.com amazon.com
If you need to check a service running on a non-standard port, you can add :port after the hostname. This is useful for internal dashboards, alternate HTTPS ports, or staging environments that are not on 443. Here’s an example:
example.com:8443 staging.example.com:9443
If you want to keep notes in the file, add them as comments:
# Production google.com amazon.com # Internal tools example.com:8443
Example Output
When the script runs, it connects to each domain and prints the certificate status. This output shows whether the certificate is safe, approaching expiration, or failed to retrieve.
Example:
Checking SSL expiration for 4 domain(s)
[OK] google.com
Expires: 2026-04-20 08:39:19 UTC
Days remaining: 65
[OK] godaddy.com
Expires: 2027-02-13 23:13:55 UTC
Days remaining: 365
[OK] netflix.com
Expires: 2026-09-04 09:49:22 UTC
Days remaining: 203
[OK] amazon.com
Expires: 2027-01-23 23:59:59 UTC
Days remaining: 344
This format makes it easy to quickly identify domains that require attention. The warning label provides early notice, while error messages indicate connection or configuration problems. This immediate visibility helps prevent unexpected certificate expiration.
Importing Required Modules
The script begins by importing several built-in Python modules that provide the necessary functionality. These modules allow the script to establish secure connections, communicate over the network, and perform time calculations.
import ssl import socket import sys from datetime import datetime, timezone
The ssl module handles encrypted TLS connections and allows the script to retrieve certificate information. The socket module creates the underlying network connection that SSL operates on top of. The sys module allows command-line arguments to be passed to the script, which makes it easier to automate and customize. The datetime module allows the script to convert expiration dates into usable values and calculate how much time remains.
Together, these modules provide everything needed without requiring external dependencies.
Defining Configuration Values
The script defines default values that control how it behaves.
DEFAULT_PORT = 443 DEFAULT_WARN_DAYS = 30 DEFAULT_TIMEOUT = 5
Port 443 is the standard HTTPS port, so it is used by default when connecting to domains. The warning threshold is set to 30 days, which provides enough time to correct renewal problems before expiration occurs. The timeout prevents the script from hanging indefinitely if a server does not respond.
Using constants makes the script easier to maintain. These values can be changed without modifying the core logic.
Loading the Domain List
The load_domains function reads the domain list from a file.
def load_domains(filename):
domains = []
The function opens the file and reads each line individually. It ignores empty lines and comment lines to keep the input clean and readable.
with open(filename, "r") as file:
for line in file:
line = line.strip()
Each domain is added to a list, along with its port number if specified.
domains.append((line, DEFAULT_PORT))
This separation between input and logic makes the script easier to maintain and expand.
Retrieving the Certificate
The script retrieves certificate information by connecting securely to the server.
context = ssl.create_default_context()
with socket.create_connection((host, port), timeout=DEFAULT_TIMEOUT) as sock:
with context.wrap_socket(sock, server_hostname=host) as secure_sock:
cert = secure_sock.getpeercert()
This process establishes a secure connection and performs a TLS handshake. During this handshake, the server provides its certificate. The script retrieves the certificate and extracts the expiration date.
This approach ensures the script retrieves the actual certificate presented to visitors.
Converting and Calculating Expiration Time
The expiration date must be converted into a usable format before calculations can be performed.
expiration_date = datetime.strptime(
expiration_str,
"%b %d %H:%M:%S %Y %Z"
)
Once converted, the script calculates how much time remains.
remaining = expiration_date - now
This allows the script to determine whether the certificate is approaching expiration.
Processing and Displaying Results
The main loop processes each domain individually.
for host, port in domains:
The script calculates remaining time and displays the results clearly.
print(f"[OK] {label}")
Error handling ensures the script continues even if a domain fails.
Final Thoughts
Certificate expiration is predictable, but it is easy to overlook without monitoring. This script provides a simple and reliable way to verify certificate validity across multiple domains. It retrieves real certificate data and presents it clearly.
Monitoring infrastructure does not require complex systems. Sometimes a simple script provides the most effective solution.





















