
A customer asked me the other day how to export a list of SSL/TLS certificate expiry dates from our load balancer appliance.
Here's what I told him...
The problem: need to monitor load balancer SSL/TLS certificates
For background on why load balancers need SSL/TLS certificates and the consequences of expired or broken certificates, check out this blog: Effortlessly monitor load balancer SSL certificates with the ADC Portal.
But for now, let's just crack on with the customer ask and our recommended solution.
So what was the request? It went something like this:
"Is there a simple way to get a list of SSL/TLS certificates with their expiration dates from the load balancer? I see that we could call "[LBIPAddress]:9443" over and over again, starting at 0 and continuing along the same lines until it runs out of certificates. But is there anything better? We keep expiration dates in our database, but are concerned that some of them may have gotten out of sync."
I put my thinking head on, knew I had done something like this for a previous customer, and allowed my fingers to do their thing!
The solution: Use this simple script to automate reporting (v8.5.9 and above)
Scripting is one of the fun tasks we get to do here at Loadbalancer.org, and here's how I used it to answer this particular request:
error_reporting(E_ALL);
// Script to walk ssl certs XML data and return domain and
// expiry date in the array key of the XML certdata.
require_once("ssl.php");
date_default_timezone_set('Europe/London');
require_once("/etc/loadbalancer.org/api-credentials");
if (!isset($username, $password, $apikey)) {
exit;
} else if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
header('WWW-Authenticate: Basic realm="CertsAPI"');
header('HTTP/1.0 401 Unauthorized');
exit;
} else if ($username == $_SERVER['PHP_AUTH_USER'] && $password == $_SERVER['PHP_AUTH_PW']) {
if ($_SERVER["HTTP_X_LB_APIKEY"]) {
if (trim(base64_decode($_SERVER["HTTP_X_LB_APIKEY"])) == $apikey) {
$cert_data = read_certs_xml();
if ($cert_data['cert']['_c']['file']) {
$results['certdata'][] = populate_cert_data($cert_data['cert']['_c']['file']['_v']);
} else {
foreach ($cert_data['cert'] as $key => $cert_info) {
$results['certdata'][] = populate_cert_data($cert_info['_c']['file']['_v']);
}
}
$results['certcount'] = count($results['certdata']);
header('Content-Type: application/json');
header("Cache-Control: no-cache, must-revalidate");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
echo json_encode($results);
}
}
}
function populate_cert_data($data) {
$results['file'] = "/etc/loadbalancer.org/certs/" . $data . "/" . $data . ".pem";
if (!file_exists($results['file'])) {
$results['error'] = "missing";
}
$cert_data = openssl_x509_parse(file_get_contents($results['file']));
$results['fqdn'] = $data;
$results['validFrom_time_t'] = date('Y-m-d', $cert_data['validFrom_time_t']);
$results['validTo_time_t'] = date('Y-m-d', $cert_data['validTo_time_t']);
return ($results);
}
The solution: Use this simple script to automate reporting (v8.5.8 and below)
For later versions, you need to write the interface and link it into the very simple API wrapper so it is secure, didn't take any args, and simply returns the data in JSON format:
// Script to walk ssl certs XML data and return domain and
// expiry date in the array key of the XML certdata.
require_once("ssl.inc");
date_default_timezone_set('Europe/London');
require_once("/etc/loadbalancer.org/api-credentials");
if (!isset($username, $password, $apikey)) {
exit;
} else if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
header('WWW-Authenticate: Basic realm="CertsAPI"');
header('HTTP/1.0 401 Unauthorized');
exit;
} else if ($username == $_SERVER['PHP_AUTH_USER'] && $password == $_SERVER['PHP_AUTH_PW']) {
if ($_SERVER["HTTP_X_LB_APIKEY"]) {
if (trim(base64_decode($_SERVER["HTTP_X_LB_APIKEY"])) == $apikey) {
$cert_data = read_certs_xml();
if ($cert_data['cert']['_c']['file']) {
$results['certdata'][] = populate_cert_data($cert_data['cert']['_c']['file']['_v']);
} else {
foreach ($cert_data['cert'] as $key => $cert_info) {
$results['certdata'][] = populate_cert_data($cert_info['_c']['file']['_v']);
}
}
$results['certcount'] = count($results['certdata']);
header('Content-Type: application/json');
header("Cache-Control: no-cache, must-revalidate");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
echo json_encode($results);
}
}
}
function populate_cert_data($data) {
$results['file'] = "/etc/loadbalancer.org/certs/" . $data . "/" . $data . ".pem";
if (!file_exists($results['file'])) {
$results['error'] = "missing";
}
$cert_data = openssl_x509_parse(file_get_contents($results['file']));
$results['fqdn'] = $data;
$results['validFrom_time_t'] = date('Y-m-d', $cert_data['validFrom_time_t']);
$results['validTo_time_t'] = date('Y-m-d', $cert_data['validTo_time_t']);
return ($results);
}
The script walks the XML for all certificates and then checks if the file is present. If it isn't present, it will return the JSON below:
{
"certdata":{
"file":"\/etc\/loadbalancer.org\/cerrs\/4.example.com\/4.example.com.pem",
"error":"missing",
"name":"4.example.com",
"state":"uploaded",
"validFrom_time_t":"1970-01-01",
"validTo_time_t":"1970-01-01"
},
"certcount":6
}
The script walks the XML for all certificates and then checks if the file is present. If it isn't present, it will return the JSON below:
So now we have:
1) The script to read the certificates present, and
2) We also have the return data format
Then we wonder, how do we call this?
Well, put simply, we run a simple HTTP GET Request, as seen below with a cURL example:
# loadbalancer.org api/v2 cURL JSON List Certs expiry
# Created by Andruw Smalley
# really simple curl command build from the input, no real validation.
while true; do
case "$1" in
-l | --loadbalancer ) loadbalancer="$2"; shift 2 ;;
-u | --username ) username="$2"; shift 2 ;;
-p | --password ) password="$2"; shift 2 ;;
-a | --apikey ) apikey=$(echo $2 | base64); shift 2 ;;
* ) break ;;
esac
done
if [[ $loadbalancer != "" ]] && [[ $username != "" ]] && [[ $password != "" ]] && [[ $apikey != "" ]]; then
curl -u ${username}:${password} -X GET \
--header "X-LB-APIKEY: ${apikey}" \
--header "Content-Type: application/json" \
https://${loadbalancer}:9443/api/v2/certs_with_expiry_date_report.php -k
else
echo "Usage: ./apicall.sh --loadbalancer 10.20.90.58 --username loadbalancer --password loadbalancer --apikey 50MlSDtFNWvLorPxnKqyY9ZfuHsgEw87"
fi
And there you have it, a computer-friendly output.
If you want something more 'human-friendly, use the following code which will provide you with an organised output for a quick consultation and also the option to export it into a CSV file:
#!/bin/bash
# apicall_table.sh – call load balancer API, display certificate expiry in three sorted, colored tables
# and optionally export to CSV
# === Parse command-line arguments ===
while [[ "$#" -gt 0 ]]; do
case $1 in
-l|--loadbalancer) loadbalancer="$2"; shift 2 ;;
-u|--username) username="$2"; shift 2 ;;
-p|--password) password="$2"; shift 2 ;;
-a|--apikey) apikey=$(echo -n "$2" | base64); shift 2 ;;
-o|--output) csv_file="$2"; shift 2 ;;
*) echo "Unknown parameter passed: $1"; exit 1 ;;
esac
done
# === Check required values ===
if [[ -z "$loadbalancer" || -z "$username" || -z "$password" || -z "$apikey" ]]; then
echo "Usage: $0 --loadbalancer <IP> --username <user> --password <pass> --apikey <key> [--output <csvfile>]"
exit 1
fi
# === Colors ===
RED="\033[0;31m"
YELLOW="\033[0;33m"
GREEN="\033[0;32m"
RESET="\033[0m"
# === Dates ===
today=$(date +%s)
threshold=$(date -d "+30 days" +%s)
# === Temporary arrays ===
expired=()
expiring_soon=()
valid=()
# === Call API and categorize certificates ===
while IFS=$'\t' read -r fqdn from to; do
expiry=$(date -d "$to" +%s)
days_left=$(( (expiry - today) / 86400 ))
line="$fqdn|$from|$to|$expiry|$days_left"
if (( expiry < today )); then
expired+=("$line")
elif (( expiry <= threshold )); then
expiring_soon+=("$line")
else
valid+=("$line")
fi
done < <(curl -s -u "${username}:${password}" -X GET \
--header "X-LB-APIKEY: ${apikey}" \
--header "Content-Type: application/json" \
"https://${loadbalancer}:9443/api/v2/certs_with_expiry_date_report.php" -k | \
jq -r '.certdata[] | [.fqdn, .validFrom_time_t, .validTo_time_t] | @tsv')
# === Function to print table with color and sorting ===
print_table() {
local title=$1
local color=$2
local lines=("${!3}")
local category=$4
if [ "${#lines[@]}" -eq 0 ]; then
echo -e "${color}${title}: None${RESET}"
echo
return
fi
echo -e "${color}${title}${RESET}"
printf "%-15s | %-12s | %-12s | %-10s\n" "FQDN" "Valid From" "Valid To" "Days Left"
printf "%-15s-+-%-12s-+-%-12s-+-%-10s\n" "---------------" "------------" "------------" "----------"
# Sort lines by expiry (4th field, numeric)
IFS=$'\n' sorted=($(printf "%s\n" "${lines[@]}" | sort -t'|' -k4n))
for line in "${sorted[@]}"; do
IFS='|' read -r fqdn from to expiry days_left <<< "$line"
printf "${color}%-15s | %-12s | %-12s | %-10s${RESET}\n" "$fqdn" "$from" "$to" "$days_left"
# Export to CSV if requested
if [ -n "$csv_file" ]; then
echo "\"$category\",\"$fqdn\",\"$from\",\"$to\",\"$days_left\"" >> "$csv_file"
fi
done
echo
}
# === If CSV export requested, add header ===
if [ -n "$csv_file" ]; then
echo "Category,FQDN,Valid From,Valid To,Days Left" > "$csv_file"
fi
# === Print the three tables ===
print_table "Expired Certificates" "$RED" expired[@] "Expired"
print_table "Expiring Soon (<=30 days)" "$YELLOW" expiring_soon[@] "Expiring Soon"
print_table "Valid Certificates" "$GREEN" valid[@] "Valid"
Why walk when you can run?
And there you have it. Customer ticket closed in record time. Hopefully this solution is helpful to others. It enabled another form of automation — this time for a report that did not exist.
If you have any questions please don't hesitate to ask our technical team. Attaching the /var/log/lbadmin.log to your ticket should be enough to fix any problems.
Alternatively, you can make your life EVEN EASIER by using our centralized management platform, the ADC Portal, to effortlessly monitor SSL certificate expiry dates from a single window of control:

And the best bit is that with the ADC Portal you can see not just the certificate expiry details of our load balancers, but also those of your F5, Citrix, and Kemp appliances too!
Want to easily monitor SSL certificates?
Register for the ADC Portal and never let an expired certificate slip through your fingers again!