When the SIP TLS certificate on an Asterisk or FreePBX server expires, phones and trunks that use TLS stop registering and WebRTC phones lose their WSS connection, usually first thing in the morning and with nothing obvious in the PBX logs. This free script checks every SIP TLS certificate the server uses, before that happens.
It also catches a fault a normal expiry check misses: the certificate was renewed on disk, but Asterisk is still serving the old one because nothing reloaded it. A website checker looking at port 443 cannot see that, and the old certificate expires on schedule anyway. The script compares the file with the certificate Asterisk actually presents on its port.
Table of Contents
What the SIP TLS certificate check looks at
- Discovery. It asks Asterisk for its PJSIP transports (
pjsip show transports) and picks everytlstransport with its certificate, key and CA file. It reads the Asterisk HTTP TLS settings (http.confand the FreePBXhttp_additional.confandhttp_custom.conf) for the WSS server WebRTC phones use, normally port 8089. - The certificate file. Days left, whether it matches the host name (from
external_signaling_address,--nameor the server’s FQDN), whether the chain is trusted (system CAs, the transport’sca_list_fileand--ca), whether it is self-signed, whether every certificate in the chain is valid now (not expired and not valid yet) and allowed for TLS server use, and whether the private key belongs to it, also when certificate and key are in one PEM file. An encrypted key is reported instead of making the script wait for a password. - The served certificate. It opens a TLS connection to the port, compares that certificate with the file and checks the chain the server sends. It also checks that the TLS handshake completed: a valid certificate on a server that then refuses the connection (for example because it requires a client certificate) is reported, not counted as OK. A different fingerprint means Asterisk serves a different certificate than the file: usually a renewal that was never loaded, but a proxy in front of Asterisk, SNI or a transport that points at another file can also cause it.
- Protocol. It warns when the server negotiates TLS 1.0 or 1.1.
- Remote endpoints.
--check sip.provider.example:5061adds your SIP provider’s TLS port or a provisioning server, checked against the system CAs.
Why a renewed certificate can still break phones
Asterisk loads a certificate when the transport starts or reloads. If certbot or another tool replaces the file and nothing reloads Asterisk, the PBX keeps presenting the old certificate until it expires. On our test server the Asterisk log says exactly which settings a reload applies to an existing transport:
NOTICE res_pjsip/config_transport.c: Transport 'srvs-tls-test' is not fully reloadable, not reloading:
protocol, bind, TLS (everything but certificate and private key if filename is unchanged), TCP, ToS, or CoS options.
So on Asterisk 22 a reload is enough when the file name stays the same. In our test both fwconsole reload and asterisk -rx 'module reload res_pjsip.so' picked up the renewed SIP TLS certificate, and module reload http picked up a replaced WSS certificate. If the new certificate has a different file name, or on older Asterisk versions, the transport needs allow_reload=yes or an Asterisk restart (see the res_pjsip transport options).
Renewals are about to get much more frequent: public TLS certificates are limited to 200 days from March 2026 and will drop to 47 days by 2029 under the CA/Browser Forum ballot SC-081. Every renewal is another chance for the reload step to be missed; see our guide to the 47-day certificate lifetime.
Before you run it
- The script is read-only. It reads config files and certificates, asks Asterisk for its transports and opens TLS connections. It changes nothing, so there is nothing to back up or revert.
- Private keys are only used to compare public keys with the certificate; no key material is printed or saved.
- Run it as root, or as the
asteriskuser, because FreePBX keys are readable by that user only. As another user the key check reports that the key is not readable. - It connects to
127.0.0.1for transports bound to all addresses, so it does not test your firewall. Test from outside with--check pbx.example.com:5061on another machine.
Install and first run
curl -fsSLO https://scr.srvscripts.com/sip-tls-certificate-check/sip-tls-cert-check.sh
sha256sum sip-tls-cert-check.sh # compare with the SHA-256 on this page
bash sip-tls-cert-check.sh # everything Asterisk uses
bash sip-tls-cert-check.sh --name pbx.example.com --warn-days 30
bash sip-tls-cert-check.sh --check sip.provider.example:5061 --json
Exit codes follow the Nagios convention: 0 OK, 1 WARNING (expiring within --warn-days, untrusted or self-signed, wrong host name, served certificate differs from the file, served chain incomplete, certificate not allowed for TLS server use, handshake not completed, encrypted key), 2 CRITICAL (expired or within --crit-days, not valid yet, an expired CA certificate in the chain, key does not match or missing from a combined PEM, no TLS handshake), 3 UNKNOWN (bad option, no openssl, nothing to check).
Tested on a real server
| Test | Result |
|---|---|
| FreePBX 17 as installed (WSS on 127.0.0.1:8089) | WARNING, exit 1: the default certificate is self-signed |
| PJSIP TLS transport, private-CA certificate with 9 days left | WARNING, exit 1: expires in 9 days |
| Renewed certificate copied over the file, no reload | WARNING, exit 1: file 89 days, served 9 days, “renewed on disk but not loaded” |
After fwconsole reload | The transport is OK; only the self-signed WSS warning remains |
--name sip.wrong-name.example | WARNING: does not match host name, for both certificates |
| Key file replaced by a different key (not reloaded) | CRITICAL, exit 2: the key does not belong to the certificate |
| Same, after a reload | CRITICAL, exit 2: no TLS handshake (connection reset); Asterisk logs “key values mismatch” |
| Expired certificate loaded | CRITICAL, exit 2: expired 3 days ago |
--check srvscripts.com:443 --json | Remote certificate read (45 days left), valid JSON |
--quiet from cron | Prints only when there is a problem; exit code unchanged |
We ran these tests on our FreePBX 17 lab server (Debian 12, Asterisk 22.11, 4 vCPU) on 16 and 17 October 2026 with OpenSSL 3.0. For the test we added a temporary PJSIP TLS transport on port 5062 with certificates from a private test CA, then removed it and restarted Asterisk; the FreePBX WSS certificate was put back exactly as it was. The same cases also run as automated tests against local OpenSSL servers on every change.
Version 1.0.1 (17 October 2026) adds checks an independent review found missing in 1.0.0: a certificate that is not valid yet, an expired intermediate CA certificate (1.0.0 reported OK), a certificate not allowed for TLS server use, and a server that does not send its intermediate certificate. The served chain is now always verified, and the “different certificate” message names the other possible causes. 16 automated fixtures cover these cases with certificates from a test CA; 1.0.0 fails 5 of them. On the rebuilt lab, 1.0.1 gives the same result as 1.0.0 for FreePBX as installed (self-signed WARNING). The screenshots are the real terminal output of 1.0.0; only IP addresses are masked, and 1.0.1 words the “different certificate” message more carefully.
A second review of 1.0.1 found two more gaps, fixed in 1.0.2: when the certificate and the private key were in one PEM file (FreePBX tlscertfile without tlsprivatekey), the key was not compared at all, and a server that sends a valid certificate but then ends the connection, for example because it requires a client certificate, was reported as OK. 1.0.2 compares the key in a combined file too, reports an encrypted key instead of waiting for its password, keeps the connection open for a second so an alert after the handshake is seen, and reports certificate_valid and handshake_complete separately in the JSON. 7 more fixtures (23 in all) cover this; 1.0.1 fails 6 of them, and with the encrypted key it waited until it was killed. On the lab, 1.0.2 gives the same result as before for FreePBX as installed.

fwconsole reload. Real output from our FreePBX 17 lab, 16 Oct 2026 (server time).


Options
| Option | Meaning |
|---|---|
--name HOST | Host name the certificates must match (default: the transport’s external_signaling_address, then the server’s FQDN) |
--check HOST:PORT | Also check a remote TLS endpoint; repeat for several |
--ca FILE | Extra CA certificates to trust, for a private CA |
--warn-days N | Warn when a certificate expires within N days (default 21) |
--crit-days N | Critical within N days (default 7) |
--conf DIR | Asterisk config folder (default /etc/asterisk) |
--json | JSON output for monitoring systems |
-q, --quiet | Print nothing when everything is OK |
Run it daily from cron
install -m 755 sip-tls-cert-check.sh /usr/local/bin/
cat > /etc/cron.d/sip-tls-cert-check <<'EOF'
15 7 * * * root /usr/local/bin/sip-tls-cert-check.sh --quiet --warn-days 21 | mail -E -s "PBX certificate" ops@example.com
EOF
With --quiet the script prints only when something needs attention, so mail -E sends nothing on good days. In Nagios, Icinga or Zabbix, use the exit code directly or parse the --json output (file_days_left, served_days_left, served_matches_file, certificate_valid, handshake_complete). Run it right after every certificate renewal too, as a check that the reload happened.
Notes and limits
- The script does not check DTLS certificates set on WebRTC endpoints (
dtls_cert_file). Browsers check those by fingerprint rather than by CA, so expiry matters less there. - A WSS transport in PJSIP has no certificate of its own: it uses the Asterisk HTTP TLS server, which the script checks.
- Phones that verify the server need the full chain. If the check reports an untrusted chain for a public certificate, install the full chain file, not the leaf certificate alone.
- A self-signed certificate works only for clients configured to trust it. Browsers reject it for WebRTC; see our Asterisk WebRTC setup guide and the Asterisk WebRTC documentation.
Related: certificate and key matcher, SSL certificate checker, certificate decoder, automate SSL certificate renewal, PJSIP health check, Asterisk security audit.
The script
#!/usr/bin/env bash
# SIP TLS and WSS Certificate Check for Asterisk and FreePBX: Expiry, Hostname, Chain, Reload (v1.0.2) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/sip-tls-certificate-check/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
#
# Read-only check of the certificates an Asterisk/FreePBX server uses for encrypted signalling: PJSIP TLS and WSS
# transports (SIP over TLS, WebRTC) and the Asterisk HTTP TLS server that WebRTC phones connect to. For each one it
# checks the certificate FILE (days left, host name, chain, private key match) and the certificate Asterisk actually
# SERVES on its port. A served certificate that differs from the file means the certificate was renewed on disk but
# Asterisk was never reloaded, the classic cause of "phones stopped registering when the old certificate expired".
# Remote endpoints (your SIP provider's TLS port, a provisioning server) can be added with --check.
#
# bash sip-tls-cert-check.sh # discover and check everything on this PBX
# bash sip-tls-cert-check.sh --name pbx.example.com --warn-days 30
# bash sip-tls-cert-check.sh --check sip.provider.example:5061 --json
# 0 7 * * * root /usr/local/bin/sip-tls-cert-check.sh --quiet || mail -s "PBX certificate" ops@example.com < /dev/null
#
# Nothing is changed: it reads config files and certificates and opens TLS connections. Private keys are only used
# to compare public keys; nothing secret is printed.
# Exit codes (Nagios style): 0 OK, 1 WARNING, 2 CRITICAL, 3 UNKNOWN (bad option, openssl missing, nothing to check).
# Version 1.0.2 (2026-10-17): second review: the private key is also compared when it sits in the same PEM file as
# the certificate (1.0.1 skipped that); an encrypted or missing key is reported; a server certificate that verifies
# is no longer taken as a working connection: an unfinished handshake (for example a server that requires a client
# certificate) is a WARNING, and the JSON has certificate_valid and handshake_complete.
# Version 1.0.1 (2026-10-17): fixes from an independent review: a certificate that is not valid yet, an expired or
# not-yet-valid CA in the chain, a certificate not allowed for TLS servers, and a server that does not send its
# intermediate certificate are now reported (1.0.0 said OK); the served chain is always verified, with the transport's
# ca_list_file and --ca; the "different certificate" message names other possible causes.
# Version 1.0.0 (2026-10-17): first release. Tested on FreePBX 17.0.33 / Asterisk 22.11 (Debian 12, OpenSSL 3.0) with a
# PJSIP TLS transport and the FreePBX WSS server: private-CA, self-signed default, 9 days left, expired, mismatched
# key, renewed on disk but not reloaded. Wrong host name and "nothing listening" are tested against openssl s_server.
set -uo pipefail
export LC_ALL=C
WARN_DAYS=21; CRIT_DAYS=7; NAME=""; CAFILE=""; JSON=0; QUIET=0; CHECKS=(); AST=${ASTERISK_BIN:-asterisk}; CONF=/etc/asterisk; TIMEOUT=8
usage() {
cat <<'EOF'
Usage: sip-tls-cert-check.sh [options]
--name HOST host name the certificates must match (default: the transport's external address,
then the machine's FQDN)
--check HOST:PORT also check a remote TLS endpoint (repeatable), e.g. your provider's SIP TLS port
--ca FILE extra CA certificates to trust (a private CA); system CAs are always trusted
--warn-days N warn when a certificate expires within N days (default 21)
--crit-days N critical within N days (default 7)
--conf DIR Asterisk config folder (default /etc/asterisk)
--json JSON output
-q, --quiet print nothing when everything is OK
-h, --help this help
EOF
}
die() { echo "UNKNOWN: $*" >&2; exit 3; }
HPRE='^[][A-Za-z0-9.:-]+:[0-9]{1,5}$'
while [ $# -gt 0 ]; do
case "$1" in
--name) NAME="${2:-}"; [[ "$NAME" =~ ^[A-Za-z0-9.*:-]+$ ]] || die "--name: invalid host name"; shift 2 ;;
--check) [[ "${2:-}" =~ $HPRE ]] || die "--check needs HOST:PORT"; CHECKS+=("$2"); shift 2 ;;
--ca) CAFILE="${2:-}"; [ -r "$CAFILE" ] || die "--ca: cannot read $CAFILE"; shift 2 ;;
--warn-days) [[ "${2:-}" =~ ^[0-9]+$ ]] || die "--warn-days needs a number"; WARN_DAYS=$2; shift 2 ;;
--crit-days) [[ "${2:-}" =~ ^[0-9]+$ ]] || die "--crit-days needs a number"; CRIT_DAYS=$2; shift 2 ;;
--conf) CONF="${2:-}"; [ -d "$CONF" ] || die "--conf: no such folder"; shift 2 ;;
--json) JSON=1; shift ;;
-q|--quiet) QUIET=1; shift ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; die "unknown option: $1" ;;
esac
done
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"
[ "$CRIT_DAYS" -le "$WARN_DAYS" ] || die "--crit-days must not be larger than --warn-days"
TMP=$(mktemp -d) || die "mktemp failed"; trap 'rm -rf "$TMP"' EXIT
STATUS=0; LINES=(); JS=(); NOW=$(date +%s)
bump() { case "$1" in CRIT) [ "$STATUS" -lt 2 ] && STATUS=2 ;; WARN) [ "$STATUS" -lt 1 ] && STATUS=1 ;; UNKNOWN) [ "$STATUS" -lt 2 ] && STATUS=3 ;; esac; }
say() { bump "$1"; LINES+=(" $1 $2"); }
jstr() { local s=${1//\\/\\\\}; s=${s//\"/\\\"}; printf '"%s"' "${s//$'\n'/ }"; }
# ---- discovery: list of "label|host:port|certfile|keyfile|cafile|name" ----
ITEMS=()
default_name() { [ -n "$NAME" ] && { echo "$NAME"; return; }; [ -n "${1:-}" ] && { echo "$1"; return; }; hostname -f 2>/dev/null || hostname; }
connect_host() { case "$1" in 0.0.0.0|::|'['::']'|'') echo 127.0.0.1 ;; *) echo "$1" ;; esac; }
if command -v "$AST" >/dev/null 2>&1 && "$AST" -rx "core show version" >/dev/null 2>&1; then
while read -r tid; do
det=$("$AST" -rx "pjsip show transport $tid" 2>/dev/null)
proto=$(awk '$1=="protocol"{print $3}' <<<"$det"); [[ "$proto" =~ ^(tls|wss)$ ]] || continue
bind=$(awk '$1=="bind"{print $3}' <<<"$det"); cert=$(awk '$1=="cert_file"{print $3}' <<<"$det"); key=$(awk '$1=="priv_key_file"{print $3}' <<<"$det")
ca=$(awk '$1=="ca_list_file"{print $3}' <<<"$det"); ext=$(awk '$1=="external_signaling_address"{print $3}' <<<"$det")
h=${bind%:*}; p=${bind##*:}; [ "$proto" = wss ] && p="" # WSS rides on the HTTP TLS server, checked below
if [ -z "$cert" ]; then [ "$proto" = tls ] && say WARN "PJSIP transport $tid (tls) has no cert_file"; continue; fi
ITEMS+=("PJSIP $proto transport $tid|$( [ -n "$p" ] && echo "$(connect_host "$h"):$p")|$cert|$key|$ca|$(default_name "$ext")")
done < <("$AST" -rx "pjsip show transports" 2>/dev/null | awk '$1=="Transport:" && $2 !~ /^</ {print $2}')
fi
# Asterisk HTTP TLS (WSS for WebRTC phones). The last value wins, as in Asterisk's own include order.
hcert=""; hkey=""; hbind=""; hen=""
for f in "$CONF/http.conf" "$CONF/http_additional.conf" "$CONF/http_custom.conf"; do
[ -r "$f" ] || continue
while IFS='=' read -r k v; do
k=${k//[[:space:]]/}; v=${v%%;*}; v=${v//[[:space:]]/}
case "$k" in tlsenable) hen=$v ;; tlsbindaddr) hbind=$v ;; tlscertfile) hcert=$v ;; tlsprivatekey) hkey=$v ;; esac
done < <(grep -E '^[[:space:]]*(tlsenable|tlsbindaddr|tlscertfile|tlsprivatekey)[[:space:]]*=' "$f")
done
if [[ "$hen" =~ ^(yes|true|1)$ ]] && [ -n "$hcert" ]; then
hb=${hbind:-0.0.0.0:8089}; hh=${hb%:*}; hp=${hb##*:}; [ "$hh" = "$hb" ] && hp=8089
ITEMS+=("Asterisk HTTP TLS (WSS/WebRTC)|$(connect_host "$hh"):$hp|$hcert|${hkey:-$hcert}||$(default_name "")")
fi
for c in "${CHECKS[@]}"; do rn=${c%:*}; rn=${rn#[}; rn=${rn%]}; ITEMS+=("Remote $c|$c||||${NAME:-$rn}"); done
[ "${#ITEMS[@]}" -gt 0 ] || die "nothing to check: no TLS transport or HTTP TLS found in Asterisk and no --check given"
days_left() { local e; e=$(openssl x509 -enddate -noout -in "$1" 2>/dev/null | cut -d= -f2); [ -n "$e" ] || return 1; echo $(( ($(date -d "$e" +%s) - NOW) / 86400 )); }
cn_of() { openssl x509 -noout -subject -nameopt multiline -in "$1" 2>/dev/null | awk -F'= ' '/commonName/{print $2; exit}'; }
fp_of() { openssl x509 -noout -fingerprint -sha256 -in "$1" 2>/dev/null | cut -d= -f2; }
host_ok() { if [[ "$2" =~ ^[0-9.]+$|: ]]; then openssl x509 -noout -checkip "$2" -in "$1" 2>/dev/null | grep -q 'does match'; else openssl x509 -noout -checkhost "$2" -in "$1" 2>/dev/null | grep -q 'does match'; fi; }
cabundle() { # transport ca_list_file + --ca -> path of a combined CA file, or nothing (system CAs only)
[ -z "$1" ] && [ -z "$CAFILE" ] && return 0
cat ${1:+"$1"} ${CAFILE:+"$CAFILE"} > "$TMP/ca.pem" 2>/dev/null; echo "$TMP/ca.pem"
}
verify_chain() { # cert cafile -> one "code depth message" line per problem; dates checked on every certificate in the chain
local ca=() out; [ -n "$2" ] && ca=(-CAfile "$2")
out=$(openssl verify "${ca[@]}" -purpose sslserver -untrusted "$1" "$1" 2>&1) && return 0
sed -nE 's/^error ([0-9]+) at ([0-9]+) depth lookup: (.*)$/\1 \2 \3/p' <<<"$out" | sort -u; return 1
}
not_before() { local b; b=$(openssl x509 -startdate -noout -in "$1" 2>/dev/null | cut -d= -f2); [ -n "$b" ] && [ "$(date -d "$b" +%s)" -gt "$NOW" ] && echo "$b"; }
left() { if [ "$1" -lt 0 ]; then echo "expired $(( -$1 )) day(s) ago"; else echo "$1 day(s) left"; fi; }
check_days() { # label days
if [ "$2" -lt 0 ]; then say CRIT "$1 EXPIRED $(( -$2 )) day(s) ago"
elif [ "$2" -le "$CRIT_DAYS" ]; then say CRIT "$1 expires in $2 day(s)"
elif [ "$2" -le "$WARN_DAYS" ]; then say WARN "$1 expires in $2 day(s)"; fi
}
for it in "${ITEMS[@]}"; do
IFS='|' read -r label hp cert key ca name <<<"$it"
hs=""; LINES+=("$label${hp:+ ($hp)}"); fdays=""; ffp=""; sdays=""; sfp=""; proto=""; selfsigned=0; vcode=""; vtext=""
if [ -n "$cert" ]; then
if [ ! -r "$cert" ] || ! openssl x509 -noout -in "$cert" 2>/dev/null; then say CRIT "certificate file $cert is missing or unreadable"
else
fdays=$(days_left "$cert"); ffp=$(fp_of "$cert")
LINES+=(" file $cert: CN=$(cn_of "$cert"), $(left "$fdays")")
check_days "file $cert" "$fdays"
nb=$(not_before "$cert") && say CRIT "file certificate is not valid yet (valid from $nb): clients reject it until then"
host_ok "$cert" "$name" || say WARN "file certificate does not match host name $name"
selfsigned=0
[ "$(openssl x509 -noout -subject -nameopt RFC2253 -in "$cert" | cut -d= -f2-)" = "$(openssl x509 -noout -issuer -nameopt RFC2253 -in "$cert" | cut -d= -f2-)" ] && selfsigned=1
cab=$(cabundle "$ca")
if ! why=$(verify_chain "$cert" "$cab"); then
while read -r code depth msg; do
case "$code" in
10) [ "$depth" -gt 0 ] && say CRIT "a CA certificate in the file's chain (depth $depth) has expired" ;; # the leaf itself is reported above
9) [ "$depth" -gt 0 ] && say CRIT "a CA certificate in the file's chain (depth $depth) is not valid yet" ;;
18|19) [ "$selfsigned" = 1 ] && say WARN "file certificate is self-signed: only clients told to trust this exact certificate accept it; browsers (WebRTC) and most SIP providers reject it" \
|| say WARN "file certificate chain ends in a CA that is not trusted here: $msg (add the CA with --ca)" ;;
26) say WARN "file certificate (depth $depth) is not allowed for TLS server use (extended key usage): $msg" ;;
"") ;;
*) say WARN "file certificate chain is not trusted: $msg (add the CA with --ca, or install the full chain)" ;;
esac
done <<<"${why:-0 0 unknown error}"
fi
if [ -n "$key" ]; then # also when the key sits in the same PEM file as the certificate
kfile="private key $key"; [ "$key" = "$cert" ] && kfile="the private key in $key"
if [ ! -r "$key" ]; then say WARN "private key $key is not readable by this user"
elif ! timeout 10 openssl pkey -pubout -in "$key" -passin pass: -out "$TMP/kpub.pem" </dev/null >/dev/null 2>&1; then
if grep -q 'ENCRYPTED' "$key" 2>/dev/null; then say WARN "$kfile is encrypted, so it could not be compared with the certificate"
elif [ "$key" = "$cert" ]; then say CRIT "no private key found in $key (certificate and key in one file): Asterisk cannot load it"
else say CRIT "$kfile could not be read as a private key"; fi
elif ! cmp -s <(openssl x509 -noout -pubkey -in "$cert" 2>/dev/null) "$TMP/kpub.pem"; then
say CRIT "$kfile does not belong to the certificate: after the next reload TLS handshakes fail (Asterisk logs 'key values mismatch')"
fi
fi
fi
fi
if [ -n "$hp" ]; then
if [[ "$name" =~ ^[0-9.]+$|: ]]; then vopt=(-verify_ip "$name"); else vopt=(-servername "$name" -verify_hostname "$name"); fi
cab=$(cabundle "$ca")
# keep the connection open for a second, so an alert the server sends after the handshake (TLS 1.3 client
# certificate check) is seen; with an immediately closed input openssl would exit 0 before it arrives
out=$(sleep 1 | timeout "$TIMEOUT" openssl s_client -connect "$hp" "${vopt[@]}" ${cab:+-CAfile "$cab"} -showcerts 2>&1); rc=$?
awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{exit}' <<<"$out" > "$TMP/served.pem"
if ! openssl x509 -noout -in "$TMP/served.pem" 2>/dev/null; then
say CRIT "no TLS handshake on $hp: nothing listening, port blocked, or the server cannot load its certificate ($(grep -m1 -iE 'connect|errno|refused|timeout|alert' <<<"$out" | cut -c1-80 || echo "openssl exit $rc"))"
else
sdays=$(days_left "$TMP/served.pem"); sfp=$(fp_of "$TMP/served.pem"); proto=$(grep -m1 -oE 'TLSv1(\.[0-3])?' <<<"$out")
LINES+=(" served on $hp: CN=$(cn_of "$TMP/served.pem"), $(left "$sdays"), ${proto:-TLS}")
[ "$ffp" = "$sfp" ] || { check_days "served certificate" "$sdays"; nb=$(not_before "$TMP/served.pem") && say CRIT "served certificate is not valid yet (valid from $nb)"; } # same certificate: already reported above
if [ -n "$ffp" ] && [ "$ffp" != "$sfp" ]; then
if [[ "$label" == PJSIP* ]]; then fix="Reload PJSIP: fwconsole reload, or asterisk -rx 'module reload res_pjsip.so' (older Asterisk versions may need allow_reload=yes on the transport or a restart)"
else fix="Reload the HTTP server: asterisk -rx 'module reload http'"; fi
say WARN "Asterisk serves a different certificate than $cert. Usually the file was renewed but not reloaded ($fix); a proxy in front, SNI or another configuration can also cause this."
fi
# a valid certificate is not a working connection: the handshake must also have completed
hs=true; alert=$(grep -m1 -oiE 'alert [a-z][a-z ]*|alert number [0-9]+' <<<"$out")
if [ "$rc" -ne 0 ] || grep -qiE 'certificate required|alert number 116' <<<"$out"; then
hs=false
if grep -qiE 'certificate required|alert number 116|Acceptable client certificate CA names|Client Certificate Types' <<<"$out"; then
say WARN "$hp asks for a client certificate, so the TLS connection was not completed: only the server certificate was checked"
else say WARN "TLS handshake on $hp did not complete (${alert:-openssl exit $rc}): only the server certificate was checked"; fi
fi
vr=$(grep -m1 -oE 'Verify return code: [0-9]+ \([^)]*\)' <<<"$out"); vcode=$(grep -oE '[0-9]+' <<<"$vr" | head -1); vtext=${vr#*(}; vtext=${vtext%)}
[ -z "$cert" ] && { host_ok "$TMP/served.pem" "$name" || say WARN "served certificate does not match host name $name"; }
# what a client sees: always judge the served chain, even when the leaf equals the file (the server may omit the intermediate)
case "${vcode:-x}" in
0) ;;
62) { [ -z "$cert" ] || [ "$ffp" = "$sfp" ]; } || say WARN "served certificate does not match host name $name" ;; # otherwise reported above
9) not_before "$TMP/served.pem" >/dev/null || say CRIT "served chain: a CA certificate is not valid yet" ;; # leaf: reported above
10) [ "${sdays:-1}" -lt 0 ] || say CRIT "served chain: a CA certificate has expired" ;;
18|19) { [ "$ffp" = "$sfp" ] && [ "${selfsigned:-0}" = 1 ]; } || say WARN "served certificate is self-signed or its CA is not trusted: $vtext" ;;
20|21) say WARN "served chain is incomplete or its CA is unknown ($vtext): the server probably does not send the intermediate certificate. Point the transport at the full chain file (certificate + intermediates), or add a private CA with --ca" ;;
x) [ "$hs" = false ] || say WARN "TLS handshake did not complete, so the served chain could not be verified (client certificate required, or the connection was closed)" ;;
*) say WARN "served certificate fails verification: $vtext" ;;
esac
[[ "$proto" =~ ^TLSv1(\.[01])?$ ]] && say WARN "server negotiated $proto: TLS 1.0 and 1.1 are obsolete; set method=tlsv1_2 (or newer)"
fi
fi
JS+=("{\"item\":$(jstr "$label"),\"endpoint\":$(jstr "$hp"),\"file\":$(jstr "$cert"),\"name\":$(jstr "$name"),\"file_days_left\":${fdays:-null},\"served_days_left\":${sdays:-null},\"served_matches_file\":$( [ -n "$ffp" ] && [ -n "$sfp" ] && { [ "$ffp" = "$sfp" ] && echo true || echo false; } || echo null),\"tls\":$(jstr "$proto"),\"served_verify\":$(jstr "${vtext:-}"),\"certificate_valid\":$(case "${vcode:-}" in 0) echo true;; "") echo null;; *) echo false;; esac),\"handshake_complete\":${hs:-null}}")
done
LVL=(OK WARNING CRITICAL UNKNOWN); host=$(hostname -f 2>/dev/null || hostname)
probs=(); for l in "${LINES[@]}"; do [[ "$l" =~ ^\ \ (WARN|CRIT|UNKNOWN)\ ]] && probs+=("${l# }"); done
summary="${LVL[$STATUS]}: ${#ITEMS[@]} TLS endpoint(s) checked, ${#probs[@]} problem(s)"
if [ "$JSON" = 1 ]; then
printf '{"host":%s,"status":%s,"warn_days":%s,"crit_days":%s,"items":[%s],"problems":[' "$(jstr "$host")" "$(jstr "${LVL[$STATUS]}")" "$WARN_DAYS" "$CRIT_DAYS" "$(IFS=,; echo "${JS[*]}")"
for i in "${!probs[@]}"; do [ "$i" -gt 0 ] && printf ','; jstr "${probs[$i]}"; done; printf '],"text":%s}\n' "$(jstr "$host $summary")"
elif [ "$QUIET" = 0 ] || [ "$STATUS" -gt 0 ]; then
echo "SIP TLS certificate check on $host"; printf '%s\n' "${LINES[@]}"; echo "$summary"
fi
exit "$STATUS"
200410d6d2b550bd8f742c8e08b0ee8779dd0507f6bc9aef2edbeeaa8d5ff858curl -fsSL -o sip-tls-cert-check.sh https://scr.srvscripts.com/sip-tls-certificate-check/sip-tls-cert-check.sh && curl -fsSL https://scr.srvscripts.com/sip-tls-certificate-check/sip-tls-cert-check.sh.sha256 | sha256sum -cInvoke-WebRequest -Uri 'https://scr.srvscripts.com/sip-tls-certificate-check/sip-tls-cert-check.sh' -OutFile 'sip-tls-cert-check.sh'; if ((Get-FileHash 'sip-tls-cert-check.sh' -Algorithm SHA256).Hash -eq '200410D6D2B550BD8F742C8E08B0EE8779DD0507F6BC9AEF2EDBEEAA8D5FF858') { '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
How do I check the SIP TLS certificate on Asterisk?
Run openssl s_client -connect 127.0.0.1:5061 -showcerts on the PBX to see the certificate Asterisk serves, and openssl x509 -enddate -noout -in on the file named by cert_file in the transport. This script does both for every TLS transport and the WSS server and compares them.
I renewed the certificate but phones still see the old one. Why?
Asterisk loads the certificate when the transport starts or reloads. Run fwconsole reload (FreePBX) or asterisk -rx "module reload res_pjsip.so". On Asterisk 22 that reloads the certificate if the file name is unchanged; otherwise set allow_reload=yes on the transport or restart Asterisk.
Which port does WebRTC use on FreePBX?
WebRTC phones connect over secure WebSocket (WSS) to the Asterisk HTTP TLS server, port 8089 by default. Its certificate is set by tlscertfile in the HTTP configuration, not in the PJSIP transport.
Can I use a self-signed certificate for SIP TLS?
Only with clients you configure to trust it, such as desk phones you provision with the certificate. Browsers and most SIP providers reject self-signed certificates, so use a certificate from a public CA for WebRTC and provider trunks.
Does the script change anything on the server?
No. It reads configuration and certificates, asks Asterisk for its transports and opens TLS connections. Private keys are only compared with the certificate and never printed.