Created
July 19, 2025 13:07
-
-
Save dzogrim/c3a841e72fac41e8259483fea8aa92fd to your computer and use it in GitHub Desktop.
checks the expiration dates of TLS certificates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# SPDX-License-Identifier: MIT | |
# SPDX-FileCopyrightText: 2015-2025 dzogrim | |
# SPDX-FileContributor: Sébastien L. <[email protected]> | |
# ----------------------------------------------------------------------------- | |
# File: certs_validity.sh | |
# Description: | |
# This script checks the expiration dates of TLS certificates | |
# for common services (IMAP, SMTP, HTTPS) on a list of domains. | |
# It performs DNS resolution and TLS negotiation using OpenSSL. | |
# | |
# ProtonMail-hosted domains are automatically detected via MX lookup | |
# and their IMAP/SMTP entries are skipped, since these services | |
# are handled internally and not exposed via standard subdomains. | |
# | |
# Domains in the SERVERS list can include options like: | |
# - with_srv=false # disables testing the "srv." subdomain | |
# | |
# Output is color-coded: | |
# - RED → certificate unavailable, invalid, or expired | |
# - BLACK on YELLOW → certificate valid but expires within $DAYS days | |
# - default (white) → certificate valid for more than $DAYS days | |
# - YELLOW → service skipped due to ProtonMail MX | |
# | |
# Requirements: | |
# - GNU bash | |
# - openssl | |
# - host (from bind-utils) | |
# - GNU date (e.g. via coreutils on macOS) | |
set -euo pipefail | |
# Domain list: domain|key=value[,key=value...] | |
SERVERS=( | |
"google.com|with_srv=false" | |
"microsoft.tld|with_srv=false" | |
"dzogrim.pw|with_srv=true" | |
) | |
# Services and ports to test | |
declare -A SERVICES=( | |
["imap"]="993" | |
["smtp"]="465" | |
["www"]="443" | |
["srv"]="443" | |
) | |
DAYS=30 | |
# ANSI colors | |
RED='\033[0;31m' | |
YELLOW='\033[0;33m' | |
BLACK_ON_YELLOW='\033[30;43m' | |
NC='\033[0m' | |
# Ensure required commands are available | |
require_bin() { | |
local bin="$1" | |
if ! command -v "$bin" &>/dev/null; then | |
echo "❌ Missing required binary: $bin" | |
exit 1 | |
fi | |
} | |
# Check MX for ProtonMail hosting | |
is_proton_dns() { | |
local domain="$1" | |
dig +short MX "$domain" | awk '{print $2}' | grep -qE '\.protonmail\.ch\.$' | |
} | |
# Extract key=value options from SERVER entry | |
get_domain_option() { | |
local entry="$1" key="$2" | |
local meta="${entry#*|}" | |
[[ "$meta" == "$entry" ]] && echo "" && return 0 # no metadata | |
IFS=',' read -ra kvs <<< "$meta" | |
for kv in "${kvs[@]}"; do | |
[[ "$kv" == "$key="* ]] && echo "${kv#*=}" && return 0 | |
done | |
echo "" | |
} | |
# Check dependencies | |
require_bin host | |
require_bin openssl | |
require_bin date | |
if ! date --version >/dev/null 2>&1; then | |
echo "❌ GNU date required (e.g. install coreutils on macOS)" | |
exit 1 | |
fi | |
# Main loop | |
for entry in "${SERVERS[@]}"; do | |
domain="${entry%%|*}" | |
with_srv=$(get_domain_option "$entry" "with_srv") | |
[[ -z "$with_srv" ]] && with_srv="true" | |
is_proton="no" | |
if is_proton_dns "$domain"; then | |
is_proton="yes" | |
fi | |
printf '\nDomain: %s\n' "$domain" | |
for service in "${!SERVICES[@]}"; do | |
fqdn="${service}.${domain}" | |
port="${SERVICES[$service]}" | |
# Skip srv if not supported | |
if [[ "$service" == "srv" && "$with_srv" != "true" ]]; then | |
continue | |
fi | |
# Skip imap/smtp for Proton domains | |
if [[ "$is_proton" == "yes" && ( "$service" == "imap" || "$service" == "smtp" ) ]]; then | |
printf ' %-28s: %bProtonMail MX → ignored%b\n' "$fqdn" "$YELLOW" "$NC" | |
continue | |
fi | |
# DNS check | |
if ! host "$fqdn" &>/dev/null; then | |
printf ' %-28s: %bUnavailable (NXDOMAIN)%b\n' "$fqdn" "$RED" "$NC" | |
continue | |
fi | |
# Cert expiration | |
# Cert expiration | |
set +e | |
output=$(timeout 5 bash -c "echo | openssl s_client -servername \"$fqdn\" \ | |
-connect \"$fqdn:$port\" 2>/dev/null") | |
status=$? | |
set -e | |
if [[ $status -eq 124 ]]; then | |
printf ' %-28s: %bTimeout after 5s%b\n' "$fqdn" "$RED" "$NC" | |
continue | |
fi | |
expiry=$(echo "$output" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) | |
if [[ -z "$expiry" ]]; then | |
printf ' %-28s: %bUnavailable (No cert)%b\n' "$fqdn" "$RED" "$NC" | |
continue | |
fi | |
expiry_ts=$(date -d "$expiry" +%s 2>/dev/null) | |
now_ts=$(date +%s) | |
if [[ -z "$expiry_ts" ]]; then | |
printf ' %-28s: %bInvalid date%b\n' "$fqdn" "$RED" "$NC" | |
continue | |
fi | |
delta_days=$(( (expiry_ts - now_ts) / 86400 )) | |
if (( delta_days < 0 )); then | |
color="$RED" | |
elif (( delta_days < ${DAYS} )); then | |
color="$BLACK_ON_YELLOW" | |
else | |
color="$NC" | |
fi | |
printf ' %-28s: %b%s%b\n' "$fqdn" "$color" "$expiry" "$NC" | |
done | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment