You Don’t Need Budget to Start
The #1 reason certificate outages happen isn’t lack of budget for fancy tools. It’s lack of any monitoring at all. Most organizations go from “no monitoring” to “expensive platform” without exploring the middle ground: free tools that prevent 80% of incidents.
This post outlines a complete $0 certificate outage prevention strategy using only open-source tools, built-in OS utilities, and scripts you can deploy today. No licenses. No procurement. No budget approval needed.
Then we’ll be honest about when this approach stops being enough.
The $0 Strategy: Four Layers
Layer 1: Discovery → Know what certificates you have
Layer 2: Monitoring → Get alerts before they expire
Layer 3: Documentation → Know who owns what
Layer 4: Process → Know what to do when alerts fire
Total cost: $0 (labor only)
Implementation time: 1-2 days
Effectiveness: Prevents ~80% of certificate outages
Layer 1: Free Discovery
PowerShell — Internal Network Scan
# discover-certs.ps1 - Scan internal network for TLS certificates
# No tools needed — uses built-in .NET
$ranges = @(
"10.0.1.1-10.0.1.254",
"10.0.2.1-10.0.2.254",
"192.168.1.1-192.168.1.254"
)
$ports = @(443, 8443, 636, 3389, 5986, 993, 995)
$results = @()
function Test-TLSCertificate {
param([string]$Hostname, [int]$Port)
try {
$tcpClient = New-Object System.Net.Sockets.TcpClient
$tcpClient.Connect($Hostname, $Port)
$sslStream = New-Object System.Net.Security.SslStream(
$tcpClient.GetStream(), $false,
{ param($s,$c,$ch,$e) return $true } # Accept all certs for scanning
)
$sslStream.AuthenticateAsClient($Hostname)
$cert = $sslStream.RemoteCertificate
if ($cert) {
$x509 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert)
return [PSCustomObject]@{
Host = $Hostname
Port = $Port
Subject = $x509.Subject
Issuer = $x509.Issuer
NotAfter = $x509.NotAfter
DaysLeft = ($x509.NotAfter - (Get-Date)).Days
Thumbprint = $x509.Thumbprint
KeySize = $x509.PublicKey.Key.KeySize
}
}
} catch {
# Host doesn't have TLS on this port — skip
} finally {
if ($sslStream) { $sslStream.Dispose() }
if ($tcpClient) { $tcpClient.Dispose() }
}
return $null
}
# Scan all targets
foreach ($ip in (Get-NetworkRange $ranges)) {
foreach ($port in $ports) {
$result = Test-TLSCertificate -Hostname $ip -Port $port
if ($result) {
$results += $result
}
}
}
# Export results
$results | Export-Csv -Path "certificate-inventory.csv" -NoTypeInformation
$results | Where-Object { $_.DaysLeft -lt 30 } |
Format-Table Host, Port, Subject, DaysLeft -AutoSize
Bash — External Certificate Scan
#!/bin/bash
# discover-external-certs.sh - Scan external endpoints
# Requirements: openssl, curl (both pre-installed on most Linux)
DOMAINS=(
"www.company.com"
"api.company.com"
"portal.company.com"
"mail.company.com"
"vpn.company.com"
)
echo "Host,Port,Subject,Issuer,Expiry,DaysLeft" > cert-inventory.csv
for domain in "${DOMAINS[@]}"; do
for port in 443 8443; do
cert_info=$(echo | timeout 5 openssl s_client \
-connect "$domain:$port" -servername "$domain" 2>/dev/null)
if [ $? -eq 0 ] && echo "$cert_info" | grep -q "BEGIN CERTIFICATE"; then
subject=$(echo "$cert_info" | openssl x509 -noout -subject 2>/dev/null | sed 's/subject=//')
issuer=$(echo "$cert_info" | openssl x509 -noout -issuer 2>/dev/null | sed 's/issuer=//')
expiry=$(echo "$cert_info" | openssl x509 -noout -enddate 2>/dev/null | sed 's/notAfter=//')
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
echo "$domain,$port,$subject,$issuer,$expiry,$days_left" >> cert-inventory.csv
fi
done
done
echo "Discovery complete. Results in cert-inventory.csv"
cat cert-inventory.csv | column -t -s,
AD CS — Free Export
# Export all issued certificates from AD CS (built-in)
certutil -view -restrict "Disposition=20,NotAfter>$(Get-Date -Format 'MM/dd/yyyy')" `
-out "RequestID,CommonName,NotAfter,CertificateTemplate,RequesterName" `
> adcs-active-certs.csv
Scripts vs. CLM Platform: Capability Fit
Score by operational factor (higher = better fit for the approach)
<200 certs
Scripts can work
200-500
Gray zone
500+
CLM required
Layer 2: Free Monitoring
Option A: PowerShell Scheduled Task (Windows)
# cert-monitor.ps1 - Run daily via Task Scheduler
# Checks all known certificates and emails alerts
$AlertThresholdDays = 30
$EmailTo = "it-team@company.com"
$EmailFrom = "cert-monitor@company.com"
$SmtpServer = "smtp.company.com"
# Load inventory (from discovery)
$inventory = Import-Csv "C:\Scripts\cert-inventory.csv"
$alerts = @()
foreach ($cert in $inventory) {
$result = Test-TLSCertificate -Hostname $cert.Host -Port $cert.Port
if ($result -and $result.DaysLeft -lt $AlertThresholdDays) {
$alerts += $result
}
}
if ($alerts.Count -gt 0) {
$body = "<h2>⚠️ Certificate Expiry Alerts</h2>"
$body += "<p>The following certificates expire within $AlertThresholdDays days:</p>"
$body += $alerts | ConvertTo-Html -Fragment
$body += "<p>Action required: Renew these certificates before expiry.</p>"
Send-MailMessage -From $EmailFrom -To $EmailTo `
-Subject "Certificate Alert: $($alerts.Count) certificates expiring soon" `
-Body $body -BodyAsHtml -SmtpServer $SmtpServer
}
# Log results
$alerts | Export-Csv -Append "C:\Scripts\cert-alert-history.csv" -NoTypeInformation
Schedule it:
# Create scheduled task to run daily at 7 AM
$trigger = New-ScheduledTaskTrigger -Daily -At 7AM
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-File C:\Scripts\cert-monitor.ps1"
Register-ScheduledTask -TaskName "CertificateMonitor" `
-Trigger $trigger -Action $action -User "SYSTEM"
Option B: Cron + Bash (Linux)
#!/bin/bash
# /opt/scripts/cert-monitor.sh - Run daily via cron
THRESHOLD=30
ALERT_EMAIL="it-team@company.com"
INVENTORY="/opt/scripts/endpoints.txt"
ALERT_FILE="/tmp/cert-alerts.txt"
> "$ALERT_FILE"
while IFS=, read -r host port description; do
expiry=$(echo | timeout 5 openssl s_client -connect "$host:$port" \
-servername "$host" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$expiry" ]; then
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ $days_left -lt 0 ]; then
echo "EXPIRED ($days_left days): $host:$port - $description" >> "$ALERT_FILE"
elif [ $days_left -lt $THRESHOLD ]; then
echo "EXPIRING ($days_left days): $host:$port - $description" >> "$ALERT_FILE"
fi
else
echo "UNREACHABLE: $host:$port - $description" >> "$ALERT_FILE"
fi
done < "$INVENTORY"
# Send alert if any findings
if [ -s "$ALERT_FILE" ]; then
mail -s "Certificate Expiry Alert - $(date +%Y-%m-%d)" \
"$ALERT_EMAIL" < "$ALERT_FILE"
fi
Cron schedule:
# /etc/cron.d/cert-monitor
# Run every day at 6:00 AM
0 6 * * * root /opt/scripts/cert-monitor.sh
Option C: Prometheus + Blackbox Exporter (Free, Scalable)
# /etc/prometheus/blackbox.yml
modules:
tls_connect:
prober: tcp
timeout: 5s
tcp:
tls: true
# /etc/prometheus/prometheus.yml (add this scrape job)
scrape_configs:
- job_name: 'certificate-expiry'
metrics_path: /probe
params:
module: [tls_connect]
static_configs:
- targets:
- api.company.com:443
- www.company.com:443
- portal.company.com:443
- mail.company.com:443
- vpn.company.com:443
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
Alert rules (free with Prometheus):
# /etc/prometheus/rules/cert-alerts.yml
groups:
- name: certificate_expiry
rules:
- alert: CertExpired
expr: probe_ssl_earliest_cert_expiry - time() < 0
for: 5m
labels:
severity: critical
annotations:
summary: "EXPIRED: {{ $labels.instance }}"
- alert: CertExpiring7d
expr: probe_ssl_earliest_cert_expiry - time() < 604800
for: 1h
labels:
severity: warning
annotations:
summary: "Expiring in {{ $value | humanizeDuration }}: {{ $labels.instance }}"
- alert: CertExpiring30d
expr: probe_ssl_earliest_cert_expiry - time() < 2592000
for: 4h
labels:
severity: info
annotations:
summary: "Expiring in {{ $value | humanizeDuration }}: {{ $labels.instance }}"
Layer 3: Free Documentation
Simple Inventory Template
# Certificate Inventory
Last updated: [date]
Updated by: [name]
## Production Certificates
| # | Host | Port | CN/SAN | CA | Expiry | Owner | Auto-Renew |
|---|------|------|--------|-----|--------|-------|------------|
| 1 | www.company.com | 443 | www.company.com | Let's Encrypt | 2026-09-15 | DevOps | Yes (certbot) |
| 2 | api.company.com | 443 | api.company.com | DigiCert | 2027-01-20 | Platform | No |
| 3 | mail.company.com | 443 | mail.company.com | DigiCert | 2026-11-03 | IT Ops | No |
| 4 | vpn.company.com | 443 | vpn.company.com | Sectigo | 2026-08-22 | Network | No |
## Internal Certificates (High Priority)
| # | Service | Host | Template/CA | Expiry | Owner | Auto-Enroll |
|---|---------|------|-------------|--------|-------|-------------|
| 1 | NPS/RADIUS | nps01 | RAS-IAS / AD CS | 2027-03-15 | Network | Yes |
| 2 | Exchange | mail-int | WebServer / AD CS | 2026-12-01 | Messaging | No |
| 3 | SQL Server | sql01 | WebServer / AD CS | 2026-10-15 | DBA | No |
| 4 | LDAPS | dc01-dc03 | DomainController / AD CS | 2027-06-01 | AD Team | Yes |
Ownership Assignment
Rule: Every certificate MUST have an owner.
Owner = Person who gets paged when it expires.
If nobody is willing to own it → ask whether it should exist at all.
Layer 4: Free Process
Renewal Runbook
# Certificate Renewal Process
## When You Get an Alert (30 days before expiry):
### Public Certificates (DigiCert, Sectigo):
1. Log into CA vendor portal
2. Locate certificate by domain name
3. Click "Renew" or generate new CSR
4. Complete domain validation
5. Download new certificate + chain
6. Install on target server
7. Verify: curl -v https://[domain] 2>&1 | grep "expire date"
8. Update inventory spreadsheet
### Let's Encrypt (certbot):
1. SSH to server
2. Run: sudo certbot renew --dry-run
3. If dry-run succeeds: sudo certbot renew
4. Verify: sudo certbot certificates
5. If auto-renew is broken, fix cron/systemd timer
### Internal (AD CS):
1. On the server: certutil -pulse (force auto-enrollment)
2. If auto-enrollment fails: certreq -enroll -machine "[TemplateName]"
3. Verify: Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*[name]*"}
4. Restart dependent services if needed
### Escalation:
- If renewal fails after 2 attempts → escalate to [senior admin]
- If <7 days remaining → declare incident, emergency change
- If expired → immediate response, all-hands
When $0 Stops Being Enough
The free approach works well — until it doesn’t. Here’s when you’ve outgrown it:
| Signal | Why It Means You Need More |
|---|---|
| >100 certificates | Scripts don’t scale; too many to track manually |
| Multiple people managing certs | Coordination breaks down without central system |
| Compliance audits asking for reports | Spreadsheets aren’t audit evidence |
| Scripts breaking regularly | Maintenance burden exceeds tool cost |
| Discovery finds unknown certs every week | You need continuous, not periodic, discovery |
| Alert fatigue from false positives | Need smart alerting with context |
| Renewals taking >30 min each | Need automation, not just alerts |
| Multi-cloud environment | Scripts can’t cover AWS + Azure + on-prem + K8s |
The Progression
Stage 1: Nothing → Any outage is a surprise
Stage 2: Scripts ($0) → You know what's expiring, react in time
Stage 3: CLM Platform → Automated discovery, alerting, renewal
Stage 4: Full Automation → Zero-touch certificate management
Most orgs are at Stage 1.
This post gets you to Stage 2.
Stage 2 prevents ~80% of outages.
Stage 3-4 prevents the remaining 20% + scales.
Complete $0 Implementation Checklist
Week 1:
☐ Run discovery scan (choose PowerShell or Bash script above)
☐ Export AD CS inventory (if applicable)
☐ Create initial inventory document
☐ Assign owners to every certificate found
☐ Identify certificates expiring in <60 days (handle immediately)
Week 2:
☐ Deploy monitoring script (daily email alerts)
☐ Set up scheduled task/cron job
☐ Test alert delivery (verify emails arrive)
☐ Write renewal runbook (per cert type)
☐ Share inventory + runbook with team
Ongoing:
☐ Run discovery monthly (catch new certs)
☐ Review alerts daily
☐ Update inventory when changes occur
☐ Quarterly: full re-scan and reconcile
The Honest Truth
A $0 strategy prevents most outages. It requires discipline — someone must maintain the scripts, check the alerts, and update the inventory. The moment that discipline slips (vacation, team change, other priorities), you’re back to hoping nothing expires.
That’s the trade-off: $0 in tools, but ongoing human reliability as the single point of failure. A paid platform replaces human reliability with system reliability. Both work. One is just more fragile than the other.
Start with $0. Prove the value. Then use the prevented outages as evidence to justify a platform investment when you’re ready.
About QCecuring
QCecuring is here when you’ve outgrown scripts. Our platform takes everything described in this post — discovery, monitoring, alerting, inventory — and makes it automatic, continuous, and scalable. Start free. Upgrade when your certificate count demands it.
Tags: free certificate monitoring, PowerShell scripts, certificate expiry alerts, open source, Prometheus, cron jobs, certutil, certificate management, outage prevention, zero-budget security