Automated SSL Certificate eeping track of SSL/TLS certificates across an organization's infrastructure is crucial. Expired or misconfigured certificates can lead to service disruptions, security vulnerabilities, and compliance issues. This post introduces a comprehensive bash script that automates the discovery and auditing of SSL certificates across networks and then, in the second part, crates a very easy to use dashboard to show you this data.
Let’s first begin with the NMAP scan, I have chosen to use Kali For this section of the script because I find it’s more accurate and reliable, The different sections will be divided by magenta headers.
Why NMAP?
That is a very good question, and the answer for that is quite simple, If you query the certificate server, you will be able to get information like certificate, template, and common names, subject, alternate nameAnd the person that requested it - But that does not help you solve the conundrum as to where the certificate is used.
The local certificate commands will tell you a certificate has been issued and you can gauge certain aspects of the certificate details, but what you can’t do is see where it’s actively bound - if the certificate has been issued by the Certificate authority and it’s not actually been bound to any active services then reporting on it seems to be counterproductive.
This is why I always think it’s a lot more productive to do a network scan that actively goes off, looking for live services and reports on what’s actually live on your network, i’ve also found when running this script that it finds other services you didn’t even know what on the server that are not technically using a certificate authority certificates - that means it will also pick up self sign certificates and certificates outside of your internal certificate authority remit.
I then thought about caveat with this approach, would the tool have access to discover the open ports - well, yes if this is a live service that people need to use if the port is shut, then the service is not live by definition, this means you will only discover ports that are available to the general network - which means you will pick up all the critical ports.
Part 1 : SSL Certificate Scanner
The SSL Certificate Scanner is a flexible tool that can:
- Scan a single IP address
- Scan an entire network range using CIDR notation
- Process a list of servers from a text file
- Discover open ports on target hosts
- Identify SSL/TLS certificates on any port
- Extract detailed certificate information
- Generate a comprehensive CSV report
Flexible Input Options
The script accepts three different input methods:
# Scan a single IP address
./scan_certificates.sh 192.168.1.100
# Scan a network range
./scan_certificates.sh 10.0.0.0/24
# Use a server list file
./scan_certificates.sh -f servers.txt
Intelligent Certificate Discovery
The script doesn't just check standard HTTPS ports (443). It:
- Scans all ports on target hosts
- Identifies services running SSL/TLS
- Extracts certificates from any SSL-enabled port
Comprehensive Data Collection
For each discovered certificate, the script collects:
- Server hostname/IP
- Port number
- Protocol (HTTP/HTTPS)
- Server signature
- Certificate Common Name (CN)
- Subject Alternative Names (SANs)
- Expiration date
Next, let’s dive right into how the script works with examples of code snippet to illustrate the point.
Target Discovery
When given a network range, the script first discovers live hosts:
# Discover live hosts in the network range
echo "Discovering live hosts in $NETWORK_RANGE ..."
SERVER_LIST=$(nmap -sn "$NETWORK_RANGE" | grep "Nmap scan report" | awk '{print $5}')
This uses nmap's ping scan (-sn
) to quickly identify responsive hosts before attempting more intensive port scanning.
Port Scanning
For each discovered host, the script performs a comprehensive port scan:
# Run nmap to discover open ports
echo "Finding open ports on $SERVER..."
OPEN_PORTS=$(nmap -p- --min-rate=1000 -T4 --max-rtt-timeout=10s
--initial-rtt-timeout=10s --min-rtt-timeout=10s "$SERVER" | grep ^[0-9]
| grep "open" | cut -d '/' -f 1)
The scan parameters are optimized for speed while maintaining reliability:
-p-
: Scan all 65,535 ports--min-rate=1000
: Send at least 1000 packets per second-T4
: Aggressive timing template- RTT timeouts: Prevent hanging on unresponsive hosts
Service Detection
The script intelligently determines which protocol to use for each port:
# Determine protocol based on port number
case $PORT in
80|8080|8000)
PROTOCOL="http"
;;
443|8443)
PROTOCOL="https"
;;
*)
# Try to get service info
SERVICE=$(nmap -p $PORT -sV "$SERVER" | grep "^$PORT" | awk '{print $4}')
if [[ "$SERVICE" == *"ssl"* || "$SERVICE" == *"tls"* || "$SERVICE" == *"https"* ]];
then
PROTOCOL="https"
elif [[ "$SERVICE" == *"http"* ]]; then
PROTOCOL="http"
else
PROTOCOL="unknown"
fi
;;
esac
Certificate Extraction
For each port potentially running SSL/TLS, the script attempts to extract certificate information:
# Check for SSL certificate
CERT_OUTPUT=$(timeout 10 openssl s_client -connect ${SERVER}:${PORT} -servername ${SERVER}
</dev/null 2>/dev/null)
# Process certificate if found
if echo "$CERT_OUTPUT" | grep -q "BEGIN CERTIFICATE"; then
# Extract certificate info
CERT_INFO=$(echo "$CERT_OUTPUT" | openssl x509 -noout -subject -ext
subjectAltName -enddate 2>/dev/null)
# Extract CN
CN=$(echo "$CERT_INFO" | grep "subject" | sed -n 's/.*CN = \([^,]*\).*/\1/p')
# Extract SAN
SAN=$(echo "$CERT_INFO" | grep -A1 "X509v3 Subject Alternative Name" | tail -n1
| sed 's/DNS://g; s/, /|/g' | tr -d ' ')
# Extract expiry date
EXPIRY=$(echo "$CERT_INFO" | grep "notAfter=" | sed 's/notAfter=//g')
fi
Server Signature Collection
The script also collects server signatures when available:
# Get server signature for HTTP/HTTPS
if [[ "$PROTOCOL" == "http" || "$PROTOCOL" == "https" ]]; then
SERVER_SIG=$(timeout 10 curl -s -I -m 10 ${PROTOCOL}://${SERVER}:${PORT} 2>/dev/null
| grep -i "Server:" | sed 's/Server: //i' | tr -d '\r')
if [ -z "$SERVER_SIG" ]; then
SERVER_SIGNATURE="Unknown"
else
SERVER_SIGNATURE="$SERVER_SIG"
fi
else
SERVER_SIGNATURE="N/A"
fi
Output Format
The script generates a CSV file with the following columns:
- Hostname
- Port
- Protocol
- Server_Signature
- Certificate_CN
- Certificate_SAN
- Expiry_Date
Excluded Ports
The script supports port exclusion via an excluded_ports.txt
file:
# Load excluded ports from file if it exists
if [ -f "$EXCLUDED_PORTS_FILE" ]; then
EXCLUDED_PORTS=$(cat "$EXCLUDED_PORTS_FILE" | tr '\n' ' ')
echo "Loaded excluded ports from ${EXCLUDED_PORTS_FILE}: ${EXCLUDED_PORTS}"
else
echo "Using default excluded ports: ${EXCLUDED_PORTS}"
fi
Timeout Settings
All network operations have timeouts to prevent the script from hanging:
- OpenSSL connections: 10 seconds
- Curl requests: 10 seconds
- Nmap scans: Various RTT timeouts
#!/bin/bash
# Script to scan for SSL/TLS certificates with flexible input options:
# - Single IP address
# - Network range in CIDR notation
# - List of servers from servers.txt file
# Outputs a combined CSV file with all results
# Supports excluded ports via excluded_ports.txt
# Help function
show_help() {
echo "SSL Certificate Scanner"
echo ""
echo "Usage:"
echo " $0 # Scan servers from servers.txt file"
echo " $0 <ip_address> # Scan a single IP address"
echo " $0 <network_range> # Scan a network range (CIDR notation)"
echo " $0 -f <file> # Scan servers from specified file"
echo ""
echo "Examples:"
echo " $0 192.168.1.1"
echo " $0 10.0.0.0/24"
echo " $0 -f my_servers.txt"
echo ""
exit 0
}
# Check for help option
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_help
fi
# Locate script directory and required files
SCRIPT_DIR="$(dirname "$0")"
EXCLUDED_PORTS_FILE="${SCRIPT_DIR}/excluded_ports.txt"
# Set up excluded ports list
EXCLUDED_PORTS="3389 2381" # Default excluded ports
# Load excluded ports from file if it exists
if [ -f "$EXCLUDED_PORTS_FILE" ]; then
EXCLUDED_PORTS=$(cat "$EXCLUDED_PORTS_FILE" | tr '\n' ' ')
echo "Loaded excluded ports from ${EXCLUDED_PORTS_FILE}: ${EXCLUDED_PORTS}"
else
echo "Using default excluded ports: ${EXCLUDED_PORTS}"
echo "To customize excluded ports, create a file named excluded_ports.txt"
echo "in the same directory as this script with one port per line."
fi
# Determine scan mode and prepare server list
SCAN_MODE=""
SERVER_LIST=""
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Process arguments
if [ $# -eq 0 ]; then
# No arguments, use servers.txt
SCAN_MODE="file"
SERVERS_FILE="${SCRIPT_DIR}/servers.txt"
if [ ! -f "$SERVERS_FILE" ]; then
echo "Error: servers.txt not found in ${SCRIPT_DIR}"
echo "Please create a servers.txt file or specify an IP/network range"
show_help
fi
SERVER_LIST=$(cat "$SERVERS_FILE" | grep -v "^#" | sed '/^$/d')
CSV_FILE="certificate_scan_file_${TIMESTAMP}.csv"
echo "Using servers from servers.txt"
elif [ "$1" = "-f" ] && [ -n "$2" ]; then
# Custom file specified
SCAN_MODE="file"
SERVERS_FILE="$2"
if [ ! -f "$SERVERS_FILE" ]; then
echo "Error: File '$SERVERS_FILE' not found"
exit 1
fi
SERVER_LIST=$(cat "$SERVERS_FILE" | grep -v "^#" | sed '/^$/d')
CSV_FILE="certificate_scan_file_${TIMESTAMP}.csv"
echo "Using servers from $SERVERS_FILE"
elif [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$ ]]; then
# Network range in CIDR notation
SCAN_MODE="network"
NETWORK_RANGE="$1"
CSV_FILE="certificate_scan_network_${TIMESTAMP}.csv"
echo "Scanning network range: $NETWORK_RANGE"
echo "Discovering live hosts..."
SERVER_LIST=$(nmap -sn "$NETWORK_RANGE" | grep "Nmap scan report" | awk '{print $5}')
if [ -z "$SERVER_LIST" ]; then
echo "No live hosts found in $NETWORK_RANGE"
exit 0
fi
echo "Found $(echo "$SERVER_LIST" | wc -l) live hosts"
elif [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# Single IP address
SCAN_MODE="single"
SERVER_LIST="$1"
CSV_FILE="certificate_scan_ip_${TIMESTAMP}.csv"
echo "Scanning single IP: $1"
else
echo "Error: Invalid argument '$1'"
show_help
fi
# Create CSV file with header
echo "Hostname,Port,Protocol,Server_Signature,Certificate_CN,Certificate_SAN,Expiry_Date"
> "$CSV_FILE"
echo "Starting certificate scan"
echo "========================================================"
# Process each server in the list
echo "$SERVER_LIST" | while IFS= read -r SERVER || [ -n "$SERVER" ]; do
# Skip empty lines
if [[ -z "$SERVER" ]]; then
continue
fi
echo ""
echo "Scanning server: $SERVER"
echo "----------------------------------------"
# Run nmap to discover open ports
echo "Finding open ports on $SERVER..."
OPEN_PORTS=$(nmap -p- --min-rate=1000 -T4 --max-rtt-timeout=10s --initial-rtt-timeout=10s
--min-rtt-timeout=10s "$SERVER" | grep ^[0-9] | grep "open" | cut -d '/' -f 1)
# Check if any ports were found
if [ -z "$OPEN_PORTS" ]; then
echo "No open ports found on $SERVER"
continue
fi
# Scan each open port for certificates
for PORT in $OPEN_PORTS; do
# Check if port is in the excluded list
if echo "$EXCLUDED_PORTS" | grep -w -q "$PORT"; then
echo "Skipping excluded port $PORT"
continue
fi
# Determine protocol based on port number
case $PORT in
80|8080|8000)
PROTOCOL="http"
;;
443|8443)
PROTOCOL="https"
;;
*)
# Try to get service info
SERVICE=$(nmap -p $PORT -sV "$SERVER" | grep "^$PORT" | awk '{print $4}')
if [[ "$SERVICE" == *"ssl"* || "$SERVICE" == *"tls"*
|| "$SERVICE" == *"https"* ]]; then
PROTOCOL="https"
elif [[ "$SERVICE" == *"http"* ]]; then
PROTOCOL="http"
else
PROTOCOL="unknown"
fi
;;
esac
echo "Checking port $PORT ($PROTOCOL)..."
# Get server signature for HTTP/HTTPS
if [[ "$PROTOCOL" == "http" || "$PROTOCOL" == "https" ]]; then
SERVER_SIG=$(timeout 10 curl -s -I -m 10 ${PROTOCOL}://${SERVER}:${PORT}
2>/dev/null | grep -i "Server:" | sed 's/Server: //i' | tr -d '\r')
if [ -z "$SERVER_SIG" ]; then
SERVER_SIGNATURE="Unknown"
else
SERVER_SIGNATURE="$SERVER_SIG"
fi
else
SERVER_SIGNATURE="N/A"
fi
# Check for SSL certificate
CERT_OUTPUT=$(timeout 10 openssl s_client -connect ${SERVER}:${PORT}
-servername ${SERVER} </dev/null 2>/dev/null)
# Process certificate if found
if echo "$CERT_OUTPUT" | grep -q "BEGIN CERTIFICATE"; then
# Extract certificate info
CERT_INFO=$(echo "$CERT_OUTPUT" | openssl x509 -noout -subject
-ext subjectAltName -enddate 2>/dev/null)
# Extract CN
CN=$(echo "$CERT_INFO" | grep "subject" | sed -n 's/.*CN = \([^,]*\).*/\1/p')
if [ -z "$CN" ]; then
CN="Not found"
fi
# Extract SAN
SAN=$(echo "$CERT_INFO" | grep -A1 "X509v3 Subject Alternative Name"
| tail -n1 | sed 's/DNS://g; s/, /|/g' | tr -d ' ')
if [ -z "$SAN" ]; then
SAN="Not found"
fi
# Extract expiry date
EXPIRY=$(echo "$CERT_INFO" | grep "notAfter=" | sed 's/notAfter=//g')
if [ -z "$EXPIRY" ]; then
EXPIRY="Not found"
fi
# Adjust protocol if needed
if [ "$PROTOCOL" = "http" ] || [ "$PROTOCOL" = "unknown" ]; then
PROTOCOL="https"
# Try to get updated server signature
SERVER_SIG=$(timeout 10 curl -s -I -m 10 https://${SERVER}:${PORT}
2>/dev/null | grep -i "Server:" | sed 's/Server: //i' | tr -d '\r')
if [ ! -z "$SERVER_SIG" ]; then
SERVER_SIGNATURE="$SERVER_SIG"
fi
fi
# Write directly to the combined CSV file
echo "$SERVER,$PORT,$PROTOCOL,$SERVER_SIGNATURE,$CN,$SAN,$EXPIRY"
>> "$CSV_FILE"
echo " - Found certificate on port $PORT (CN: $CN)"
else
echo " - No certificate found on port $PORT"
fi
done
# Count certificates found for this server by filtering the combined CSV
CERT_COUNT=$(grep "^$SERVER," "$CSV_FILE" | wc -l)
if [ $CERT_COUNT -eq 0 ]; then
echo "No SSL certificates found on $SERVER"
else
echo "Found $CERT_COUNT certificates on $SERVER"
fi
echo ""
done
# Summary
TOTAL_CERTS=$(wc -l < "$CSV_FILE")
TOTAL_CERTS=$((TOTAL_CERTS - 1)) # Subtract header line
echo "========================================================"
echo "Scan completed"
echo "Total certificates found: $TOTAL_CERTS"
echo "Results saved to $CSV_FILE"
echo "========================================================
This is that script running from the "external file" option:
When this scan completed you end up with a CSV file, this CSV will then drive the graphical website generation which is coming up next.
Part 2 : Creating the web dashboard
Leading on from Part 1 you will find managing SSL/TLS certificates across multiple servers can be a challenging task, especially when you need to keep track of expiration dates and ensure everything remains valid. I've developed a PowerShell script that transforms certificate scan data into a beautiful, interactive HTML dashboard that makes certificate monitoring effortless.
The Problem
In today's environment, tracking certificates across numerous servers is crucial. Missing an expiration date can lead to service outages and security vulnerabilities.
While various tools exist for certificate scanning, presenting this data in a meaningful, actionable format is often the missing piece.
The Solution
I created a PowerShell script that:
- Automatically finds the latest certificate scan CSV file in the current directory
- Processes the data, handling even malformed CSV files gracefully
- Generates a clean, professional HTML dashboard
- Provides interactive filtering and search capabilities
Smart Data Processing
The script handles real-world CSV files that might have:
- Records split across multiple lines
- Inconsistent formatting
- Special characters in certificate SANs
It intelligently parses these files and reconstructs the data properly before processing.
Interactive Health Cards
At the top of the dashboard, you'll find clickable health cards showing:
- Total Servers: Complete count of all monitored servers
- Valid: Certificates that are current and valid
- Nearly Expiring: Certificates expiring within the next 30 days
- Expired: Certificates that have already expired
Clicking any card instantly filters the table to show only those results.
Real-Time Search
Below the health cards, there's a search box that enables instant filtering of servers. You can search by hostname, port, protocol, or any other field - the results update as you type.
Intelligent Status Detection
The script automatically categorizes certificates:
- Expired certificates are flagged in red
- Certificates expiring within 30 days are marked in yellow
- Valid certificates appear in green
Clean Data Presentation
I've ensured the dashboard only displays relevant information. If certain data isn't available across all records (like server signatures), the script filters out incomplete entries to maintain a clean presentation.
How to Use
- Place the script in a directory containing your certificate scan CSV files
- Run the script - it automatically finds the latest scan file
- Open the generated HTML file in any browser
- Use the health cards to filter by status
- Search for specific servers using the search box
Visual Results
The outputted report will then look like this: