SSL Cert Auto Renew Module

I was debating whether to post this here or in module development but decided to put it here since it will probably be seen by more people here. The topic is around how to manage SSL certificates year in and year out in a automated way. In an ideal world, this functionality should be incorporated into Ignition because there is really no commercial system that doesn't use SSL these days. However, given the wide variance of systems that Ignition would have to interface with, its probably tough to provide something that works for everyone. Thankfully, the way Ignition itself picks up on the ssl.pfx file, automation is possible via module development which is what is discussed here.

If you've just setup your gateway and finally made it through the process of getting your SSL cert installed and finally see that fancy little lock icon in your browser, congratulations, what a great feeling! However, your work has just begun becuase 300 something days from now you'll need to do it again or else your Ignition provided web services will cease working.

Type of system being used
For this post, we are talking about a certificate authority (CA) that allows certificate management via HTTP calls. In our specific case the CA is Venafi. Interaction looks like this:

  • Create - makes a new certificate when there is not one
  • Renew - renews a certificate that already exists for the common name (CN)
  • Download - fetches the actual cert
  • Obsolete - deletes the cert from the remote CA

What I want to point out here is that in order for you to do what I am talking about, a similar system which can be interacted with automatically must exist.

Authentication
Certificates are precious security tokens and should not be obtainable unless you are the entity that should have access. As such, there are access controls in place so that not just anyone can make API calls and manipulate your certificates. Those are:

  • Application must be onboarded and recognized by the CA (in general via AD group)
  • MTLS

MTLS roughly means this:

  1. The calling application must be in possession of an authentication certificate that was issued by the CA
  2. The installed root CA cert must match at both ends. This is the cert held in java cacerts

So to manage certs automatically you actually need more certs :slight_smile:

The module leverages a central EAM gateway with webdev in order to hold and distribute the authentication cert. In summary, the auth cert is stored in one place and each gateway can reach out and get it when they need. The root ca cert is available inside the company over http so each gateway can get it when needed. Its something like a 10-20 year expiry so its not frequent. The MTLS auth cert does need to be renewed once per year.

Module Function
When the module is first installed on a gateway that has no certificate at all or has a certificate installed that is not managed in the automatic path, a create call will be made. After giving some time for the certificate to be created, the certificate is then downloaded and installed.

When there is an auto managed certificate installed and it will expire within a specified window (90 days in our case) a renew call is sent, and again after waiting for the cert to be created, the new cert is downloaded and installed.

Finally, when it is time to decommission the gateway, by taking manual action in the settings page in the gateway, a obsolete call is made and the cert is deleted on the CA side. At this point the installed cert will still work until its expiration date, but no further renew or create attempts will be made unless decommission is reversed, which is possible.

Some of the more tricky pieces of code
In general the module does 2 things:

  • HTTP calls
  • File manipulation

However, I have included some of the more difficult to figure out code here that may help if you decide to go down this path.

Making a HTTP Call with MTLS

    /** Generic HTTP POST function */
    private static JSONObject httpPost(String api, String params) {
        String url = settings.getCwsBaseUrl() + api;
        HttpClient client = HttpClient.newBuilder().sslContext(getSSLContext()).build();
        HttpRequest request = HttpRequest.newBuilder()
                .POST(HttpRequest.BodyPublishers.ofString(params))
                .uri(URI.create(url))
                .header("Content-Type", "application/json")
                .build();
        try {
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            return new JSONObject(response.body());

        } catch (Exception e) {
            logger.error(String.format("Error making http post: %s", api), e);
            return null;
        }
    }

    /**
     * Generates the SSL context required to establish mTLS with CWS.
     * Expects that CWS auth cert as a PKCS12 is located in the supplemental certs folder.
     * The password to access the PKCS12 should match that used in Ignition.
     * @return SSLContext based on the CWS auth certificate.
     */
    private static SSLContext getSSLContext() {
        try {
            char certPass[] = appId.toCharArray();
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            keyStore.load(new FileInputStream(authCertPath), certPass);
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
                    KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, certPass);
            SSLContext ctx = SSLContext.getInstance("TLSv1.3");
            ctx.init(keyManagerFactory.getKeyManagers(), null, null);
            return ctx;
        } catch (Exception e) {
            logger.error("Error Getting SSL Context", e);
            return null;
        }
    }

Load the ssl.pfx immediately
Ignition will do this for your automatically every 15 minutes, but for doing things automatically, it is necessary to do it right away.

     * Executes gwcmd -g.
     * As per the following reference, not available in the SDK:
     * https://forum.inductiveautomation.com/t/ssl-auto-renewal-module/71666/14
     * @return void
     */
    private static void loadSSLKeyStoreImmediately() {
        String drive = home.substring(0, 1);
        String command = "";
        ProcessBuilder builder;
        if( isWindows() ) {
            command = String.format("%s: && cd %s && gwcmd -g", drive, home);
            builder = new ProcessBuilder("cmd.exe", "/c", command);
        }
         else {
             //TODO need to test in Linux
             command = String.format("cd /mnt/%s && gwcmd.sh --reloadks", home);
             builder = new ProcessBuilder("/bin/sh", "-c", command);
        }
        try {
            builder.redirectErrorStream(true);
            Process p = builder.start();
            BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
            String line;
            while(true) {
                line = r.readLine();
                if (line == null) { break; }
                logger.info(line);
            }
        } catch (Exception e) {
            logger.error("Unable to execute gwcmd -g", e);
        }
    }

Fetch cert over port 8043
This method is used to verify the installation. If the installation does not work, the module will back out to the last known working cert if there is one.

    /**
     * Gets the serial number of the installed certificate over SSL port 8043.
     * Can be used to prove that the SSL cert is correctly installed.
     * Ref: https://forum.inductiveautomation.com/t/automated-ssl-certificate-renewal-and-installation/49039/9
     * Serial number is returned as radix 16 string
     * Ref: https://stackoverflow.com/questions/12582850/x509-serial-number-using-java
     */
    private static String getInstalledCertSerialNumber() {
        String certSerialNumber = null;
        try {
            String url = String.format("https://%s:8043", gatewayCN);
            URL destinationUrl = new URL(url);
            HttpsURLConnection conn = (HttpsURLConnection) destinationUrl.openConnection();
            conn.connect();
            Certificate[] certs = conn.getServerCertificates();

            for (Certificate cert : certs) {
                if (cert instanceof X509Certificate) {
                    String DN = ((X509Certificate) cert).getSubjectDN().toString();
                    if (DN.contains(gatewayCN)) {
                        certSerialNumber = ((X509Certificate) cert).getSerialNumber().toString(16).toUpperCase();
                        break;
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Unable to retrieve installed cert serial number", e);
        }
        return certSerialNumber;
    }

There is of course a lot more to it so if you ever do go down this path and need some help, feel free to reach out.

Cheers,

Nick

7 Likes