Guide
Check SSL expiry for a CSV of domains
Batch-check SSL expiry for multiple domains from a CSV using openssl and parallel workers—dedupe hosts and catch bad SNI early.
Updated Apr 10, 2026 803 words
A disciplined workflow to check ssl expiry multiple domains starts with a clean CSV, explicit port and SNI columns, and parallel OpenSSL probes—not a paste of marketing URLs with mixed schemes.
Portfolio managers, agencies, and platform teams inherit domain lists from spreadsheets, acquisitions, and CMS exports. Running checks one hostname at a time wastes incident hours; running unchecked bulk scripts creates false positives from http:// prefixes, trailing paths, and duplicate www rows.
This guide structures the CSV, runs a Bash batch job, and explains how to sort results for renewal planning—without replacing per-host verification on critical revenue domains.
Agencies inherit client CSV exports with mixed schemes, uppercase hostnames, and dormant microsites. Normalizing once per quarter prevents noisy ERROR rows that desensitize on-call to real check ssl expiry multiple domains warnings.
Finance may fund renewals in batches aligned to fiscal quarters. Sorted CSV output becomes the slide deck for renewal budget meetings—earliest notAfter clusters justify single change windows instead of scattered fire drills.
What to check
Each row needs a connect host (DNS name or IP), port (default 443), and SNI name (usually the public hostname). Certificate expiry comes from the leaf presented after SNI. HTTP redirects are out of scope for raw TLS probes: if marketing URLs redirect, store the final TLS name or probe both.
Separate staging rows from production in a environment column so alerts do not page for labs. Wildcard coverage does not appear in CSV cells—if you list app1.example.com and app2.example.com, you still probe each name unless you rely on a wildcard and accept SAN limits.
After bulk TLS dates, spot-check outliers with certificate transparency logs search to find unlisted hostnames.
Step-by-step
Prepare domains.csv
hostname,port,sni,notes
www.client-a.com,443,www.client-a.com,production
api.client-a.com,443,api.client-a.com,production
legacy.client-b.com,443,legacy.client-b.com,staging
No https://, no paths, one host per row.
Bash worker with timeout
check_one() {
IFS=',' read -r host port sni notes <<<"$1"
port=${port:-443}
sni=${sni:-$host}
end=$(timeout 12 bash -c "echo | openssl s_client -connect '${host}:${port}' -servername '${sni}' 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null")
if [ -z "$end" ]; then
echo "${host},${port},${sni},ERROR,${notes}"
else
echo "${host},${port},${sni},${end#notAfter=},${notes}"
fi
}
export -f check_one
tail -n +2 domains.csv | while IFS= read -r line; do
check_one "$line"
done | tee ssl-expiry-results.csv
Parallelize safely (GNU parallel)
tail -n +2 domains.csv | parallel -j8 --colsep ',' check_one {1},{2},{3},{4} \
| sort -t, -k4 > ssl-expiry-results-sorted.csv
Keep -j modest to avoid tripping rate limits on small shared hosting providers.
Flag expiring within 30 days
awk -F, '$4 != "ERROR" {
cmd="date -d \"" $4 "\" +%s"; cmd | getline t; close(cmd)
now=systime(); if (t - now < 2592000) print $0
}' ssl-expiry-results.csv
On macOS, replace date -d with gdate from coreutils or parse in Python—see check SSL expiry with Python.
Optional: add registrar column via RDAP (slow)
For high-value rows only, call RDAP per registrable domain and join in a spreadsheet—not inside tight parallel TLS loops.
How to read the output
Result rows show hostname, port, sni, notAfter or ERROR, notes. ERROR means timeout, refusal, TLS handshake failure, or empty cert—investigate DNS, firewall, and whether the host is HTTP-only.
Sorted by expiry date, clusters on the same day often share a certbot timer or a single load balancer PEM. Isolated early dates may be legacy VMs or customer-managed sites.
Present ERROR rows in a separate tab during renewal meetings so decision makers do not conflate unreachable sandboxes with revenue production hosts. Quantify ERROR rate as a percentage of total rows; sudden spikes often mean corporate VPN DNS broke the runner rather than mass expiry events.
When two hosts share identical notAfter but different sni, they might still hit the same wildcard listener—confirm before opening duplicate renewal tasks. When dates differ by exactly ninety days on many rows, suspect a single ACME integration renewed a subset while legacy manual certs linger on older VMs.
Notes column values like staging should drive filtering in executive PDFs. Nothing erodes trust in your check ssl expiry multiple domains program faster than paging a VP about a laboratory hostname intentionally using week-long certificates.
Compare sni to the certificate SAN from a verbose probe when dates look wrong:
openssl s_client -connect HOST:443 -servername SNI </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName
Default certificate on shared hosting shows unrelated SANs while your row’s name was never covered.
Common mistakes
Feeding full URLs into openssl -connect (https://host/path) breaks parsing. Normalize in the CSV step.
Using apex SNI against hosts that only answer on www (or the reverse) yields ERROR or wrong default certs. Maintain both rows if both are customer-facing.
Running unbounded parallel checks against one small provider gets your IP throttled; backoff and retry ERROR rows.
Reporting CSV ERROR as “expired”—distinguish unreachable from certificate has expired via explicit openssl verify:
echo | openssl s_client -connect host:443 -servername sni 2>&1 | grep -i expire
Assuming one script replaces per-platform renewal ownership; the CSV tells you who to ping, not how to redeploy nginx. For reusable logic, lift this loop into Bash script to check SSL certificate expiry.
Skipping deduplication before the run wastes time probing www and apex twice when both redirect to the same certificate behind one CDN edge—dedupe by resolving final SNI where redirects are stable.
Leaving UTF-8 BOM characters in CSV exports from Excel creates invisible hostname failures. Save as UTF-8 without BOM or normalize with sed before batching.
When to use automation
Quarterly CSV exports from finance or ITSM should feed a scheduled job that writes results to object storage and opens tickets for <30 day notAfter. Version-control the input CSV in git so diffs show newly acquired domains.
Add live monitoring on top 20 revenue hosts daily; use CSV batch for the long tail weekly. Merge TLS output with RDAP registrar expiry columns so procurement sees one table.
Services like ssldays offer free multi-domain checks without maintaining worker fleets—still keep openssl runbooks for air-gapped audits. After any bulk run, spot-verify ERROR rows manually; automation should not auto-renew on bad data.
Add a reviewed_by column humans fill after spot checks on the ten earliest expiries. Accountability matters more than perfect automation when clients sue over downtime.
Store historical result CSVs in git-lfs or object storage with date stamps. Trend analysis shows whether ERROR rates climb because your portfolio grew or because a hosting provider degraded.
When sharing CSV results externally, strip internal notes columns that reference unreleased product codenames. Clients need hostnames and dates, not your JIRA ticket shorthand.
Throttle retries: schedule a second pass twenty-four hours later for ERROR hosts only. Many transient failures disappear; persistent ERROR rows deserve tickets with DNS and traceroute evidence attached.
Stakeholders reading your first bulk export often ask why ERROR and EXPIRED appear on the same row in contradictory ways. Train them that ERROR means the monitor could not complete a handshake, while EXPIRED requires a successful read of a past notAfter. Clarify that WARN is a policy threshold, not a browser error string. When legal requests evidence, attach the CSV plus two openssl transcripts for the earliest production hostname—not the entire thousand-row file.
Seasonal businesses should run an extra CSV pass before peak traffic holidays because agencies sometimes deploy campaign hostnames with ninety-day certificates without telling central IT. Black Friday microsites are a recurring source of surprise rows unless marketing submits hostname lists two weeks ahead.
Ownership disputes between product and platform teams often start when the CSV shows a hostname nobody recognizes. Use the notes column religiously and require business unit tags before rows enter the canonical list. Without ownership, renewal budget conversations stall while expiry dates do not move.
Capacity planning for runners matters when portfolios exceed a few thousand hosts. Estimate average handshake duration multiplied by concurrency and add headroom for TCP retransmits on lossy Wi-Fi based laptops running ad hoc checks. Move long-running batches to data center runners with stable egress rather than engineer laptops that sleep mid-loop.
Finally, celebrate visible wins: when sorted CSV shows zero WARN rows across top revenue domains, share that snapshot in engineering newsletters. Positive reinforcement keeps teams submitting updated host lists instead of hiding shadow projects that would otherwise become the next outage headline.
Define a data quality owner who approves additions to the canonical CSV each month. Ad hoc rows from chat messages reintroduce duplicates and typos that undermine trust in the entire batch program.
Require peer review on CSV pull requests the same way application code requires review. Diff visibility catches accidental deletion of production rows that cron would otherwise stop monitoring silently until expiry surprises arrive.
Procurement lawyers reviewing vendor SSL addendums appreciate historical CSV archives demonstrating due diligence before contractual penalties. Timestamped exports become exhibits without hiring experts to reconstruct monitoring from memory.
Customer support macros can reference internal doc IDs pointing to latest CSV summary pages, reducing escalations asking if subsidiary brand domains were included in last night batch.
Innovation labs running rapid experiments should use a separate CSV file merged monthly rather than polluting production canonical lists with hosts deleted after forty-eight hour hackathons.
Telecom and remote work spikes change which hostnames users hit globally; rerun batches after major routing changes even when calendar expiry dates unchanged, because ERROR rates reveal reachability not only certificates.
Sponsor a quarterly office hours session where engineers bring proposed CSV additions; central review catches duplicates and teaches naming conventions before rows hit production cron.
Frequently asked questions
Should the CSV include IP addresses?
Use IPs only when DNS is unstable or you debug specific load balancers. For expiry tracking, prefer stable hostnames so renewals follow DNS cuts.
What about non-443 ports?
Add a port column for 8443, 9443, or custom APIs. Mail uses STARTTLS on other ports—use the check mail server TLS expiry guide instead of forcing 443.
Can I run this on Windows?
Use check SSL expiry with PowerShell or WSL with the Bash script. Native OpenSSL on Windows works but path quoting needs care.
Does CSV checking find all certificates for a brand?
No. It only tests listed rows. CT search and crawler discovery fill gaps for forgotten subdomains.
See what the server presents right now
Free live certificate check — no signup.