The customer asks, we deliver: “where are your reports...?''

The customer asks, we deliver: “where are your reports...?''

Performance Published on 5 mins Last updated

On Friday morning I arrived in the office and picked up an overnight customer request.

I wanted to turn it around asap to make the customer’s weekend a happy one. Job done. But in the process here's what I learned about how to export a list of certificate expiry dates on your load balancer!

Scripting shenanigans

Scripting is one of the fun tasks we get to do and, as I'm Mr Automation here at Loadbalancer.org, I knew it was a task to be completed and sent back to the customer before they got out of bed.

So what was the request? It was as typed below - don't worry, no sensitive information included (GDPR and all that!):

"I wonder if there is a simple way to get a list of certificates with their expiration dates from the load balancer. I see that we could call:  https://192.168.93.60:9443/lbadmin/ajax/get_ssl.php?t=cert&v=0 over and over again starting at 0 and going until it runs out of certs. Is there anything better? We keep expiration dates in our database, but we 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!

First I wrote the interface and linked it into the very simple API wrapper so it was secure, didn't take any args and simply returned the data in JSON format.

<?php
// 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);
}
Place this file in /var/www/html/api/v2/certs_with_expiry_date_report.php

The script walks the XML for all certificates and then checks if the file is present and, 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
}

So we have the script to read the certificates present, we also have the return data format and now we think: how do we call this? Well, put simply, it's 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 "./apicall.sh --loadbalancer 192.168.2.21 --username loadbalancer --password loadbalancer --apikey eP68pvSMM8dvn051LL4d35569d438ue0"

fi
cURL HTTP GET Certificate expiry dates

Now those who've read my previous blogs will recognise this script is simply the same as that in my APICALL script - only changing it from POST to GET request and removed the need to send any JSON. We only need the output here, but you don't use Linux as you don't have cURL present, so you need another way to call and retrieve the JSON output.

We can use any language that will perform a HTTP GET, so that leaves us with my normal (Powershell) or C#.

Holding my hands up, with no prior hands-on exposure of C# but, knowing the customer used C#, will use this as an example...

using System;
using System.Text;
using System.Diagnostics;
        
// c# code to retrieve cert list

// add https and port to url
String get_report_url = "https://192.168.100.100:123/api/v2/certs_with_expiry_date_report.php";
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

// ignore cert errors - needed unless you have a real cert with a real domain
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

String username = "user";
String password = "pwd";
string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(username + ":" + password));
String apikey = "apikey";
String b64apikey = Convert.ToBase64String(Encoding.UTF8.GetBytes(apikey));
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(get_report_url);
request.Method = "GET";
request.Headers.Add("Authorization", "Basic " + auth);

// original was missing the 'X-' prefix
request.Headers.Add("X-LB-APIKEY", b64apikey);
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

// save to file
using (StreamWriter writer = File.CreateText(@"D:\whatever\certs.json")) {
	writer.Write(responseString);
}

//Debug.WriteLine(responseString);

C# untested example to HTTP GET Request and print output on screen.

...and also because golang is, well, "EASY" to code and make a .exe file for windows, mac or linux!

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
    "flag"
)

func main() {
    var cmd_loadbalancer string
    var cmd_username string
    var cmd_password string
    var cmd_apikey string
    flag.StringVar(&cmd_loadbalancer, "-loadbalancer", "192.168.2.21", "The ip address to access your loadbalancer.org appliance")
    flag.StringVar(&cmd_username, "-username", "loadbalancer", "The username to access your loadbalancer.org appliance")
    flag.StringVar(&cmd_password, "-password", "loadbalancer", "The password to access your loadbalancer.org appliance")
    flag.StringVar(&cmd_apikey, "-apikey", "ghGZmu4HWBTwSktjisX6xf90zORMaYAK", "The apikey to access your loadbalancer.org appliance")

    client := http.Client{Timeout: 5 * time.Second}

    req, err := http.NewRequest(http.MethodGet, "https://"+cmd_loadbalancer+"/api/v2/certs_with_expiry_date_report.php")
    if err != nil {
        log.Fatal(err)
    }
    req.SetBasicAuth(cmd_username, cmd_password)
    req.Header.Set("X-LB-APIKEY", cmd_apikey)
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Printf("%s", body) 
    // print response body to screen, we could redirect to a file 
	// if that was wanted.
}
HTTP GET Request with headers and base64 encoded APIKEY

As you can see from above, golang is kinda 'wow'.

But is it really this simple to write the code? The answer is yes! To my surprise "anyone" with the aptitude and wish to learn can be posting data within an hour (it took me just 10 mins to find and implement this when I looked, and its cross-platform so no more scripts to do things! Go golang!).

Clever compiling?

So, you have a script, but now you ask about compiling? That's hard right? Well no, its "simply" simple! One would just type go build scriptname.go to build a binary or .exe on Windows and go run scriptname.go to test it!

It had me up and running with my first sensible script in a matter of hours.

Why walk when you can run?

So here we have a customer request made overnight, exceeding expectations in turnaround time, examples shared and I would like to hope everything works as the customer had asked!

It enabled another form of automation - this time for a report that did not exist.

I think that about sums it up. I should really say that not every request of this type can be turned around on the same day - but we do try!

If you have any questions please do not hesitate to ask, and if it all goes horribly wrong then email support@loadbalancer.org and let us know what happened – so we can do our very best to fix whatever the issue might be! Attaching /var/log/lbadmin.log should be enough to find any problems.

Happy Automation!