The Problem: You Don’t Know What You Don’t Know
Most organizations can name their primary domains. They know about www.company.com and mail.company.com. But when asked how many total subdomains they have with active certificates, the answer is usually a guess — and that guess is almost always low by 40-60%.
This walkthrough demonstrates a real subdomain enumeration and certificate scanning process. We’ll go from zero visibility to a complete expiry report in under an hour.
Tools You’ll Need
Before starting, ensure you have these tools installed:
# Install subfinder (subdomain enumeration)
go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest
# Install httpx (HTTP probing with TLS info)
go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest
# Install sslyze (deep TLS analysis)
pip install sslyze
# OpenSSL (usually pre-installed)
openssl version
| Tool | Purpose | Speed | Depth |
|---|---|---|---|
| subfinder | Passive subdomain discovery | Fast | Broad |
| crt.sh | CT log queries | Medium | Historical |
| httpx | Active probing + TLS grab | Fast | Current |
| sslyze | Deep TLS/cert analysis | Slow | Detailed |
| OpenSSL | Manual cert inspection | Manual | Full |
Phase 1: Subdomain Enumeration
Passive Discovery with subfinder
# Run subfinder with all passive sources
subfinder -d example.com -all -silent -o subdomains.txt
# Sample output:
# www.example.com
# mail.example.com
# vpn.example.com
# api.example.com
# staging.example.com
# portal.example.com
# cdn.example.com
# ...
The Certificate Visibility Gap
Typical enterprise: what's tracked vs. what actually exists
Tracking Coverage
Certificates Found by Source
⚠️ In the average mid-market enterprise, less than 15% of certificates have both ownership documentation and expiry monitoring.
CT Log Enrichment
# Query crt.sh for additional subdomains not found by subfinder
curl -s "https://crt.sh/?q=%.example.com&output=json" | \
jq -r '.[].name_value' | \
sed 's/\*\.//g' | \
sort -u > ct-subdomains.txt
# Merge both sources
cat subdomains.txt ct-subdomains.txt | sort -u > all-subdomains.txt
echo "Total unique subdomains: $(wc -l < all-subdomains.txt)"
Real Results Example
For a typical mid-size enterprise, here’s what enumeration reveals:
Discovery Source Breakdown:
━━━━━━━━━━━━━━━━━━━━━━━━━
subfinder passive: 142 subdomains
crt.sh CT logs: 287 subdomains
Combined unique: 341 subdomains
With active DNS: 198 subdomains
Serving HTTPS: 156 subdomains
The gap between “subdomains IT knows about” (typically 30-50) and “subdomains that actually exist” (198 in this case) is your certificate visibility gap.
Phase 2: Active Certificate Scanning
Quick Scan with httpx
# Probe all subdomains and extract TLS certificate information
cat all-subdomains.txt | \
httpx -silent -tls-grab -json -o tls-results.json
# Extract expiry dates
cat tls-results.json | \
jq -r '{
host: .host,
issuer: .tls.issuer_organization,
not_after: .tls.not_after,
subject_cn: .tls.subject_cn,
fingerprint: .tls.fingerprint_hash.sha256
}' > cert-inventory.json
Deep Analysis with sslyze
For endpoints requiring detailed analysis:
# Run sslyze against specific targets
sslyze --regular api.example.com portal.example.com vpn.example.com
# JSON output for automation
sslyze --json_out=sslyze-results.json \
--certinfo \
--targets_in=high-priority-hosts.txt
OpenSSL for Individual Inspection
# Detailed certificate inspection
echo | openssl s_client -connect api.example.com:443 \
-servername api.example.com 2>/dev/null | \
openssl x509 -noout \
-subject -issuer -dates -serial -fingerprint -ext subjectAltName
# Output:
# subject=CN = api.example.com
# issuer=C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
# notBefore=Jan 15 00:00:00 2026 GMT
# notAfter=Feb 14 23:59:59 2027 GMT
# serial=0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D
# SHA1 Fingerprint=AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD
Phase 3: Expiry Analysis
The Complete Scanning Script
#!/bin/bash
# cert-expiry-scanner.sh - Find expiring certificates across subdomains
# Usage: ./cert-expiry-scanner.sh example.com [days-threshold]
DOMAIN="${1:?Usage: $0 domain.com [days]}"
THRESHOLD="${2:-30}"
DATE=$(date +%Y%m%d)
OUTPUT_DIR="cert-scan-$DOMAIN-$DATE"
mkdir -p "$OUTPUT_DIR"
echo "=== Certificate Expiry Scanner ==="
echo "Domain: $DOMAIN"
echo "Threshold: $THRESHOLD days"
echo "Output: $OUTPUT_DIR/"
echo ""
# Step 1: Enumerate subdomains
echo "[1/4] Enumerating subdomains..."
subfinder -d "$DOMAIN" -all -silent > "$OUTPUT_DIR/subdomains-raw.txt"
curl -s "https://crt.sh/?q=%.$DOMAIN&output=json" | \
jq -r '.[].name_value' 2>/dev/null | \
sed 's/\*\.//g' >> "$OUTPUT_DIR/subdomains-raw.txt"
sort -u "$OUTPUT_DIR/subdomains-raw.txt" > "$OUTPUT_DIR/subdomains.txt"
TOTAL=$(wc -l < "$OUTPUT_DIR/subdomains.txt")
echo " Found $TOTAL unique subdomains"
# Step 2: Check which are alive on 443
echo "[2/4] Probing HTTPS endpoints..."
cat "$OUTPUT_DIR/subdomains.txt" | \
httpx -silent -ports 443 -tls-grab -json \
> "$OUTPUT_DIR/tls-scan.json" 2>/dev/null
ALIVE=$(wc -l < "$OUTPUT_DIR/tls-scan.json")
echo " $ALIVE endpoints responding on HTTPS"
# Step 3: Parse and analyze expiry
echo "[3/4] Analyzing certificate expiry..."
NOW=$(date +%s)
echo "STATUS,DAYS_LEFT,HOST,ISSUER,EXPIRY_DATE,SUBJECT" \
> "$OUTPUT_DIR/expiry-report.csv"
cat "$OUTPUT_DIR/tls-scan.json" | while read line; do
host=$(echo "$line" | jq -r '.host')
not_after=$(echo "$line" | jq -r '.tls.not_after // empty')
issuer=$(echo "$line" | jq -r '.tls.issuer_organization // "Unknown"')
subject=$(echo "$line" | jq -r '.tls.subject_cn // "N/A"')
if [ -n "$not_after" ]; then
expiry_epoch=$(date -d "$not_after" +%s 2>/dev/null)
if [ -n "$expiry_epoch" ]; then
days_left=$(( (expiry_epoch - NOW) / 86400 ))
if [ $days_left -lt 0 ]; then
status="EXPIRED"
elif [ $days_left -lt $THRESHOLD ]; then
status="EXPIRING"
else
status="OK"
fi
echo "$status,$days_left,$host,$issuer,$not_after,$subject" \
>> "$OUTPUT_DIR/expiry-report.csv"
fi
fi
done
# Step 4: Generate summary
echo "[4/4] Generating summary..."
EXPIRED=$(grep -c "^EXPIRED" "$OUTPUT_DIR/expiry-report.csv" 2>/dev/null || echo 0)
EXPIRING=$(grep -c "^EXPIRING" "$OUTPUT_DIR/expiry-report.csv" 2>/dev/null || echo 0)
OK=$(grep -c "^OK" "$OUTPUT_DIR/expiry-report.csv" 2>/dev/null || echo 0)
echo ""
echo "=== RESULTS ==="
echo "Total subdomains found: $TOTAL"
echo "HTTPS endpoints alive: $ALIVE"
echo "Certificates expired: $EXPIRED"
echo "Certificates expiring: $EXPIRING (within $THRESHOLD days)"
echo "Certificates OK: $OK"
echo ""
echo "Report saved: $OUTPUT_DIR/expiry-report.csv"
# Show critical findings
if [ $EXPIRED -gt 0 ] || [ $EXPIRING -gt 0 ]; then
echo ""
echo "=== CRITICAL FINDINGS ==="
grep -E "^(EXPIRED|EXPIRING)" "$OUTPUT_DIR/expiry-report.csv" | \
sort -t',' -k2 -n | \
column -t -s','
fi
Sample Output
=== Certificate Expiry Scanner ===
Domain: example.com
Threshold: 30 days
Output: cert-scan-example.com-20260717/
[1/4] Enumerating subdomains...
Found 341 unique subdomains
[2/4] Probing HTTPS endpoints...
156 endpoints responding on HTTPS
[3/4] Analyzing certificate expiry...
[4/4] Generating summary...
=== RESULTS ===
Total subdomains found: 341
HTTPS endpoints alive: 156
Certificates expired: 7
Certificates expiring: 12 (within 30 days)
Certificates OK: 137
=== CRITICAL FINDINGS ===
EXPIRED -45 old-portal.example.com Let's Encrypt 2026-06-01 old-portal.example.com
EXPIRED -23 staging-v1.example.com DigiCert 2026-06-24 *.example.com
EXPIRED -12 dev-api.example.com Let's Encrypt 2026-07-05 dev-api.example.com
EXPIRING 3 vpn.example.com Sectigo 2026-07-20 vpn.example.com
EXPIRING 7 mail.example.com DigiCert 2026-07-24 mail.example.com
EXPIRING 14 api-v2.example.com Let's Encrypt 2026-07-31 api-v2.example.com
EXPIRING 21 portal.example.com DigiCert 2026-08-07 portal.example.com
Interpreting the Findings
Pattern Recognition
When analyzing scan results, look for these patterns:
1. Cluster Expiry Multiple certificates expiring within the same week often indicate a batch purchase that was never set up for staggered renewal.
2. Let’s Encrypt Gaps Let’s Encrypt certificates expiring suggest automation failure. These should auto-renew at 60 days — if they’re expiring, certbot or the ACME client is broken.
3. Wildcard Drift
When you see the same wildcard cert (*.example.com) reported across many hosts but with different expiry dates, you have key distribution sprawl.
4. Orphaned Services Expired certificates on hosts that still resolve but return errors indicate decommissioned services that were never fully cleaned up.
Risk Classification
┌─────────────────────────────────────────────────┐
│ RISK CLASSIFICATION MATRIX │
├────────────┬────────────────────────────────────┤
│ Category │ Criteria │
├────────────┼────────────────────────────────────┤
│ Critical │ Expired + customer-facing │
│ │ Expiring <7 days + production │
├────────────┼────────────────────────────────────┤
│ High │ Expired + internal-reachable │
│ │ Expiring <14 days + production │
├────────────┼────────────────────────────────────┤
│ Medium │ Expiring <30 days │
│ │ Expired + non-production │
├────────────┼────────────────────────────────────┤
│ Low │ Expiring 30-60 days │
│ │ Non-critical environments │
└────────────┴────────────────────────────────────┘
Automation: Making This Repeatable
Cron-Based Weekly Scanning
# /etc/cron.d/cert-scanner
# Run every Monday at 6 AM
0 6 * * 1 /opt/scripts/cert-expiry-scanner.sh company.com 30 2>&1 | \
mail -s "Weekly Certificate Expiry Report" security-team@company.com
Integration with Monitoring
# prometheus-cert-exporter config
modules:
https:
prober: tcp
tls_config:
insecure_skip_verify: false
tcp:
tls: true
# Alert rule
groups:
- name: certificate_expiry
rules:
- alert: CertificateExpiringSoon
expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 14
for: 1h
labels:
severity: warning
annotations:
summary: "Certificate expiring within 14 days"
description: "{{ $labels.instance }} cert expires in {{ $value | humanizeDuration }}"
What You’ll Typically Find
Based on running this process across dozens of organizations:
| Finding | Frequency | Typical Count |
|---|---|---|
| Unknown subdomains | 95% of orgs | 40-200 extra domains |
| Expired certificates | 80% of orgs | 5-25 expired certs |
| Certificates expiring <30 days | 90% of orgs | 8-15 certs |
| Multiple CA providers | 85% of orgs | 3-7 different CAs |
| Shadow IT certificates | 70% of orgs | 10-50 unknown certs |
| Weak key algorithms | 40% of orgs | 5-20 weak certs |
Next Steps After Discovery
- Triage — Sort findings by risk level, address critical items within 24 hours
- Assign ownership — Every certificate needs an owner, not just an issuer
- Establish baselines — Run the scan weekly to track improvement
- Automate monitoring — Move from periodic scanning to continuous monitoring
- Consider CLM — If you have >100 certificates, manual processes won’t scale
About QCecuring
QCecuring helps enterprises move from reactive certificate scanning to proactive lifecycle management. Our platform provides continuous discovery, automated alerting, and complete inventory management across all certificate types and environments.
Tags: subdomain enumeration, certificate scanning, expiring certificates, sslyze, subfinder, crt.sh, CT logs, certificate discovery, TLS analysis, security automation