Emergency server help: get in touch

AutoSSL DCV Failures Report

Lists AutoSSL DCV failures on a cPanel server grouped by cause (DNS elsewhere, CAA, rate limit, blocked /.well-known/) and every installed certificate that expires within 14 days.

Version
1.0.0
Last updated
October 6, 2026
Language
Bash
Tested on
AlmaLinux 9.8 with cPanel & WHM 11.138 (lab test, 5 Oct 2026); Parsing verified on sample cPanel data; written for cPanel on AlmaLinux 8/9 and CloudLinux 8/9
License
MIT
Pricing
Free

AutoSSL runs every night and on most servers nobody reads what it says. The log for a busy server is thousands of lines, and the few that matter, the AutoSSL DCV failures, are buried between “Analyzing” lines. Then a certificate quietly expires because the customer moved DNS to another host six months ago, or a security plugin started redirecting /.well-known/ to the login page, and the first report is a browser warning on a checkout page.

This script reads the latest AutoSSL run, pulls out only the problem lines, and sorts the failing domains by cause. It then checks the certificates actually installed in Apache, because a domain can pass DCV today and still be running on a certificate that expires on Friday.

What the AutoSSL DCV failures report checks

  • The newest run under /var/cpanel/logs/autossl/ (or the last N runs with --runs), from the plain-text txt log or the json log when that is all there is.
  • Every WARN or ERROR line that names a domain, classified by cause (see below).
  • The owning account for each domain, from /etc/userdomains (with www., mail. and the other service subdomains mapped back to the parent).
  • Every certificate in /var/cpanel/ssl/apache_tls/*/combined that expires within --days days or has already expired, with the issuer, so a self-signed or third-party certificate stands out.

The causes, in the order the script checks them:

  • CAA — a CAA record does not authorize the provider’s certificate authority.
  • RATE-LIMIT — the provider refused because of its rate limits.
  • DNS — the domain resolves to an IP that is not on this server, or does not resolve at all.
  • HTTP-BLOCKED — the validation file under /.well-known/ came back as a redirect, 403 or 404: usually an .htaccess rule, a forced HTTPS or www redirect, or a WAF.
  • OTHER — anything else, with the log line for context.

Usage

curl -fsSL https://srvscripts.com/get/autossl-failure-report/ -o autossl-failure-report.sh
bash autossl-failure-report.sh                  # latest run, certificates expiring in < 14 days
bash autossl-failure-report.sh --runs=3         # merge the last three runs
bash autossl-failure-report.sh --days=30        # longer expiry window

Sample output

== AutoSSL DCV failures ==
Log: 2026-09-30T02:09:51Z

CAUSE         DOMAIN                             ACCOUNT        LOG LINE
CAA           shopsite.io                        shopsite       ERROR: The certificate authority rejected the request for "shopsite.io": urn:ietf:params:acme:error:caa :: CAA
DNS           www.acme.org                       acmeco         ERROR: Local HTTP DCV error (www.acme.org): The domain "www.acme.org" resolved to an IP address "198.51.100.7"
HTTP-BLOCKED  acme-shop.net                      acmeco         WARN: Local HTTP DCV error (acme-shop.net): The system queried for a temporary file at "http://acme-shop.net/.

FAIL 3 domain(s) failing DCV

== Apache certificates expiring within 14 days ==
DAYS    VHOST                                  ISSUER                     EXPIRES
EXPIRED blogger.example                        Let's Encrypt              Sep 27 19:25:00 2026 GMT
4       acmeco.com                             Sectigo Limited            Oct  5 19:24:33 2026 GMT

Three different fixes. shopsite.io needs its CAA record to include the AutoSSL provider’s CA. www.acme.org points to another server, so either the DNS gets corrected or the alias is removed from the account. acme-shop.net answers the validation request with a redirect, which is almost always a rewrite rule in .htaccess that needs an exception for /.well-known/. The expired blogger.example certificate belongs to a suspended account, which is expected; acmeco.com renews in four days only if its DCV passes tonight.

Tested on a real server

We ran this script on our lab server on 5 October 2026: AlmaLinux 9.8 with cPanel & WHM 11.138, MariaDB 10.11 and three WordPress test accounts. The screenshot is the real terminal output; only IP addresses are masked.

Terminal output of bash autossl-failure-report.sh --runs=3 --days=21 on AlmaLinux 9.8 with cPanel and WHM 11.138
bash autossl-failure-report.sh --runs=3 --days=21 — exit code 0, 0.2 s. AlmaLinux 9.8, cPanel & WHM 11.138, 5 Oct 2026. IP addresses masked.

Options

  • --runs=N — parse the last N AutoSSL runs (default 1, the latest). For each domain the most specific cause wins.
  • --days=N — certificate expiry window in days (default 14).
  • --no-color — plain output even on a terminal.
  • -h, --help — usage.

Running it from cron

Exit code 1 means at least one DCV failure or one certificate inside the expiry window. Schedule it an hour or two after AutoSSL’s nightly run:

15 6 * * * root bash /root/autossl-failure-report.sh --no-color > /root/autossl-report.txt || mail -s "AutoSSL problems on $(hostname)" admin@example.com < /root/autossl-report.txt

Notes

Read-only: the script reads log files and certificates and never triggers an AutoSSL run. The cause is inferred from the wording of cPanel’s log messages, so a new message format may land in OTHER until the patterns catch up; the log line is always printed so nothing is hidden. Mail subdomains that do not resolve are a common, harmless source of DNS entries.

For certificates on other services and remote hosts, the SSL expiry check connects to each endpoint instead of reading files. The official Let’s Encrypt rate limits and CAA pages explain the two provider-side causes.

AutoSSL DCV failures at a glance

AutoSSL DCV Failures Report summary card: AutoSSL runs every night and on most servers nobody reads what it says.
In short: AutoSSL runs every night and on most servers nobody reads what it says.
AutoSSL DCV Failures Report sections: What the AutoSSL DCV failures report checks, Usage, Sample output and Options
Covers: What the AutoSSL DCV failures report checks, Usage, Sample output and Options.
AutoSSL DCV Failures Report questions answered: Why is a domain listed as DNS when it loads fine in my browser? Should I re-run AutoSSL after a rate-limit failure?
Answers: Why is a domain listed as DNS when it loads fine in my browser? Should I re-run AutoSSL after a rate-limit failure?

Official documentation: Let’s Encrypt documentation, cPanel & WHM documentation, Linux man pages.

Related guides: cPanel SSL/TLS Interface (v136+): Easy Install and Renew · WP Toolkit CVE-2026-87900: what is affected and how to patch to 6.11.3 · KernelCare Setup on cPanel and DirectAdmin: Reliable Patching.

The script

autossl-failure-report.shDownload
#!/usr/bin/env bash
# AutoSSL DCV Failures Report (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/autossl-failure-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
# autossl-failure-report.sh — which domains AutoSSL cannot secure, why, and which certificates expire soon
# https://srvscripts.com/scripts/autossl-failure-report/   License: MIT
#
# Read-only. Parses the latest AutoSSL run log(s) in /var/cpanel/logs/autossl/,
# groups failing domains by cause (DNS not pointing here, CAA, rate limit, HTTP
# validation blocked by .htaccess/redirects, other) and lists installed Apache
# certificates that expire within N days.
#   bash autossl-failure-report.sh               # latest run, certs expiring < 14 days
#   bash autossl-failure-report.sh --runs=3 --days=21
# Exit codes: 0 = OK, 1 = failures or expiring certificates, 2 = usage/dependency error.
#
# Testing only: SRVS_ROOT=/some/dir prefixes every cPanel path the script reads
# so the log and certificate parsing can be exercised against a fixture tree.
set -uo pipefail
export LC_ALL=C

R=${SRVS_ROOT:-}
RUNS=1; DAYS=14; COLOR=1

usage() {
  cat <<'EOF'
Usage: autossl-failure-report.sh [options]

Lists domains that failed AutoSSL domain control validation (DCV), grouped by
cause, and installed certificates that expire soon. Read-only. Run as root.

Options:
  --runs=N      parse the last N AutoSSL runs (default 1 = latest only)
  --days=N      warn about certificates expiring within N days (default 14)
  --no-color    plain output even on a terminal
  -h, --help    this help

Exit codes: 0 = OK, 1 = failures or expiring certificates, 2 = usage/dependency error.
EOF
}

for arg in "$@"; do
  case $arg in
    --runs=*)   RUNS=${arg#*=} ;;
    --days=*)   DAYS=${arg#*=} ;;
    --no-color) COLOR=0 ;;
    -h|--help)  usage; exit 0 ;;
    *) echo "Unknown option: $arg (see --help)" >&2; exit 2 ;;
  esac
done
[[ $RUNS =~ ^[0-9]+$ && $DAYS =~ ^[0-9]+$ ]] && (( RUNS > 0 )) || { echo "--runs and --days take whole numbers (--runs >= 1)" >&2; exit 2; }

[[ -d $R/usr/local/cpanel ]] || { echo "cPanel not found ($R/usr/local/cpanel is missing). This script is for cPanel/WHM servers." >&2; exit 2; }
if [[ -z $R && $EUID -ne 0 ]]; then echo "Run as root." >&2; exit 2; fi

if (( COLOR )) && [[ -t 1 ]]; then
  C_OK=$'\e[32m'; C_WARN=$'\e[33m'; C_FAIL=$'\e[31m'; C_OFF=$'\e[0m'
else
  C_OK=; C_WARN=; C_FAIL=; C_OFF=
fi
hr() { printf '\n== %s ==\n' "$*"; }
st() { # st STATUS message
  local c=
  case $1 in OK) c=$C_OK ;; WARN) c=$C_WARN ;; FAIL) c=$C_FAIL ;; esac
  printf '%s%-4s%s %s\n' "$c" "$1" "$C_OFF" "$2"
}

problems=0
LOGDIR=$R/var/cpanel/logs/autossl

# log_text DIR — print the run's messages as plain text. Prefers the "txt" log;
# falls back to the "json" log (one JSON object per line, text in "contents").
log_text() {
  if [[ -r $1/txt ]]; then
    cat "$1/txt"
  elif [[ -r $1/json ]]; then
    awk '
      function field(name,   i, s, out, c) {
        i = index($0, "\"" name "\":\""); if (!i) return ""
        s = substr($0, i + length(name) + 4); out = ""
        while (length(s)) {
          c = substr(s, 1, 1)
          if (c == "\\") { out = out substr(s, 1, 2); s = substr(s, 3); continue }
          if (c == "\"") break
          out = out c; s = substr(s, 2)
        }
        return out
      }
      { t = field("type"); m = field("contents"); if (m != "") print (t != "" ? toupper(t) ": " : "") m }' "$1/json"
  fi
}

# ---------------------------------------------------------------- AutoSSL runs
hr "AutoSSL DCV failures"
runs=()   # one directory per run; newest by modification time last
if [[ -d $LOGDIR ]]; then
  while IFS= read -r d; do runs+=("$d"); done < <(find "$LOGDIR" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -n | tail -n "$RUNS" | cut -d' ' -f2-)
fi
if (( ${#runs[@]} == 0 )); then
  echo "skipped: no AutoSSL logs in $LOGDIR (AutoSSL disabled or never run?)"
else
  for d in "${runs[@]}"; do echo "Log: ${d##*/}"; done
  echo
  # Normalise cPanel's curly quotes (raw UTF-8 or \u escapes) to plain quotes,
  # keep problem lines, pull out the domain and classify the cause.
  report=$(for d in "${runs[@]}"; do log_text "$d"; done |
    sed -e 's/\xe2\x80\x9c/"/g; s/\xe2\x80\x9d/"/g; s/\xe2\x80\x99/'"'"'/g' \
        -e 's/\\u201[cd]/"/g; s/\\u2019/'"'"'/g; s/\\"/"/g' |
    awk -v udfile="$R/etc/userdomains" '
      BEGIN {
        while ((getline l < udfile) > 0) { split(l, a, /: */); owner[a[1]] = a[2] }
        pri["CAA"] = 5; pri["RATE-LIMIT"] = 4; pri["DNS"] = 3; pri["HTTP-BLOCKED"] = 2; pri["OTHER"] = 1
      }
      function grab(re, trim) { if (match(line, re)) return substr(line, RSTART + trim, RLENGTH - 2 * trim); return "" }
      {
        line = tolower($0)
        # Problem lines only: informational lines also mention DCV and CAA.
        if (line !~ /error|warn|fail|ratelimit|rate limit|too many|does not resolve|prevents issuance/) next
        if (line ~ /(^|[ :])(ok|info|success)[ :]/ && line !~ /error|warn|fail/) next
        dom = grab("\\([a-z0-9*][a-z0-9.-]*\\.[a-z][a-z0-9-]+\\)", 1)
        if (dom == "") dom = grab("\"[a-z0-9*][a-z0-9.-]*\\.[a-z][a-z0-9-]+\"", 1)
        if (dom == "") next
        if (line ~ /caa/) c = "CAA"
        else if (line ~ /ratelimit|rate limit|too many (certificates|requests|failed|new orders)/) c = "RATE-LIMIT"
        else if (line ~ /does not exist on this server|does not resolve|resolved to|nxdomain|servfail|no ipv4|no a record/) c = "DNS"
        else if (line ~ /htaccess|redirect|forbidden|40[34]|30[1278]|well-known|pki-validation|acme-challenge/) c = "HTTP-BLOCKED"
        else if (line ~ /dns/) c = "DNS"
        else c = "OTHER"
        if (dom in cat && pri[cat[dom]] > pri[c]) next
        cat[dom] = c
        msg = $0; sub(/^[ \t]*/, "", msg)
        why[dom] = substr(msg, 1, 110)
      }
      END {
        for (d in cat) {
          b = d; sub(/^(www|mail|webmail|cpanel|webdisk|cpcalendars|cpcontacts|autodiscover|autoconfig|whm)\./, "", b)
          printf "%s\t%s\t%s\t%s\n", cat[d], d, (d in owner) ? owner[d] : ((b in owner) ? owner[b] : "-"), why[d]
        }
      }' | sort)
  if [[ -z $report ]]; then
    st OK "no DCV failures in the parsed run(s)"
  else
    printf '%-13s %-34s %-14s %s\n' CAUSE DOMAIN ACCOUNT "LOG LINE"
    awk -F'\t' '{ printf "%-13s %-34s %-14s %s\n", $1, $2, $3, $4 }' <<<"$report"
    echo
    n=$(wc -l <<<"$report"); problems=$((problems + n))
    st FAIL "$n domain(s) failing DCV"
    awk -F'\t' '{ c[$1]++ } END { for (k in c) printf "  %-13s %d\n", k, c[k] }' <<<"$report" | sort
    echo
    echo "What each cause usually means:"
    echo "  DNS           domain resolves somewhere else (moved, Cloudflare proxy, stale alias); fix DNS or remove it"
    echo "  HTTP-BLOCKED  .htaccess rewrite, forced redirect or WAF answers /.well-known/ instead of the file"
    echo "  CAA           a CAA record does not allow the AutoSSL provider's CA to issue"
    echo "  RATE-LIMIT    provider rate limit hit; wait, do not keep re-running AutoSSL"
  fi
fi

# ---------------------------------------------------------------- expiring certificates
hr "Apache certificates expiring within $DAYS days"
if ! command -v openssl >/dev/null 2>&1; then
  echo "skipped: openssl not installed"
else
  now=$(date +%s); checked=0; rows=""
  for f in "$R"/var/cpanel/ssl/apache_tls/*/combined; do
    [[ -r $f ]] || continue
    checked=$((checked + 1))
    vhost=${f%/combined}; vhost=${vhost##*/}
    # "combined" holds key + certificate + CA chain; take the first certificate only.
    cert=$(awk '/-----BEGIN CERTIFICATE-----/ {p = 1} p {print} /-----END CERTIFICATE-----/ {exit}' "$f")
    [[ -n $cert ]] || { rows+="$(printf '%s\t%s\t%s\t%s' "?" "$vhost" "?" "no certificate found in combined file")"$'\n'; continue; }
    info=$(openssl x509 -noout -enddate -issuer -subject <<<"$cert" 2>/dev/null) || continue
    end=$(sed -n 's/^notAfter=//p' <<<"$info")
    endts=$(date -d "$end" +%s 2>/dev/null) || continue
    left=$(( (endts - now) / 86400 )); (( endts < now )) && left=$(( -((now - endts) / 86400) - 1 ))
    (( left < DAYS )) || continue
    iss=$(sed -nE 's/^issuer=(.*[ ,\/])?O *= *([^,\/]+).*/\2/p' <<<"$info"); iss=${iss:-unknown}
    [[ $(sed -n 's/^issuer=//p' <<<"$info") == "$(sed -n 's/^subject=//p' <<<"$info")" ]] && iss="self-signed"
    rows+="$(printf '%s\t%s\t%s\t%s' "$left" "$vhost" "$iss" "$end")"$'\n'
  done
  if [[ -z $rows ]]; then
    st OK "$checked certificate(s) checked, none expire within $DAYS days"
  else
    printf '%-7s %-38s %-26s %s\n' DAYS VHOST ISSUER EXPIRES
    printf '%s' "$rows" | sort -t$'\t' -k1,1n | awk -F'\t' '{ printf "%-7s %-38s %-26s %s\n", ($1 == "?" ? "?" : ($1 < 0 ? "EXPIRED" : $1)), $2, substr($3, 1, 26), $4 }'
    n=$(printf '%s' "$rows" | grep -c .)
    problems=$((problems + n))
    st WARN "$n of $checked certificate(s) expire within $DAYS days or are unreadable"
  fi
fi

echo
if (( problems > 0 )); then st FAIL "$problems problem(s) found"; exit 1; fi
st OK "AutoSSL looks healthy"
exit 0
Version 1.0.0 · SHA-256 3142b94ee02f661677a792fc0a6f1518d1b30fb52bac9957b55014442ccf0ca1
Download and verify on Linux or macOS
curl -fsSL -o autossl-failure-report.sh https://scr.srvscripts.com/autossl-failure-report/autossl-failure-report.sh && curl -fsSL https://scr.srvscripts.com/autossl-failure-report/autossl-failure-report.sh.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/autossl-failure-report/autossl-failure-report.sh' -OutFile 'autossl-failure-report.sh'; if ((Get-FileHash 'autossl-failure-report.sh' -Algorithm SHA256).Hash -eq '3142B94EE02F661677A792FC0A6F1518D1B30FB52BAC9957B55014442CCF0CA1') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

Why is a domain listed as DNS when it loads fine in my browser?

It loads from somewhere else. AutoSSL validates from this server’s point of view, so a domain behind a proxy or pointing at another host fails DCV even though the site works.

Should I re-run AutoSSL after a rate-limit failure?

No. Wait for the limit window to pass; repeated runs keep hitting the same limit. Fix DNS and HTTP causes first so the next run does not waste attempts.

Does it read the WHM AutoSSL problem list?

No, it reads the run logs directly, which hold the same messages and work on every cPanel version that has AutoSSL.

Changelog

  • 1.0.0 — Initial release

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.