• 0

Exclude self-signed certificates in SSL_Certificate


Go to solution Solved by Stuart Weenig,

Question

6 answers to this question

Recommended Posts

  • 0
  • Administrators
  • Solution

"This has got to be the ugliest hack I've ever done."

See, this is the problem when I get curious. Use this as the active discovery script. Probably best to test it out in a clone to make sure what you want gets discovered properly. Once discovery is working right, you can set a discovery filter for auto.selfSigned NotEqual true.

/*******************************************************************************
 *  © 2007-2020 - LogicMonitor, Inc. All rights reserved.
 ******************************************************************************/

// Begin by getting the host name.
def host = hostProps.get("system.hostname")

// Create a map of all common SSL ports with their names.
def ports = ["HTTP - SSL"           : 443,
             "SMTP - SSL"           : 465,
             "NNTP - SSL"           : 563,
             "LDAP - SSL"           : 636,
             "IEEE-MMS-SSL"         : 695,
             "FTP - SSL"            : 989,
             "FTP Control- SSL"     : 990,
             "IMAP - SSL"           : 993,
             "IRC - SSL"            : 994,
             "POP3 - SSL"           : 995,
             "Oracle DB - SSL"      : 1521,
             "Docker Rest API - SSL": 1521,
             "cPanel - SSL"         : 2083,
             "WebHost Manager - SSL": 2087,
             "XMPP - SSL"           : 5223,
             "OSAUT - SSL"          : 6619,
             "IRC Secure - SSL"     : 6679,
             "IRC Secure - SSL"     : 6697,
             "AMQP - SSL"           : 5671,
             "Apache Tomcat - SSL"  : 8443,
             "Memcached - SSL"      : 11214]

// Loop through each port...
def instances = []

ports.each { name, port ->
    // There is a high chance at least some of these ports won't connect!
    try {
        // Create a socket.
        def socket = new Socket()

        // Attempt a connection to our host and port with a one second timeout.
        socket.connect(new InetSocketAddress(host, port), 1000)

        // The socket could connect! The port is open! Print the instance.
        //println "$host:$port##$name"
        instances.add([host,port,name])

        // Close the socket.
        socket.close()
    }
    catch (e) {
        // Port isn't open
    }
}

/*******************************************************************************
 *  © 2007-2020 - LogicMonitor, Inc. All rights reserved.
 ******************************************************************************/
// We have to be careful with imports, as javax.net.ssl and apache conflict.
import org.apache.http.conn.ssl.SSLSocketFactory

import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import java.security.Security
import java.security.cert.CertificateException
import java.security.cert.X509Certificate

// Gather certificates from a host and port, and put them into global storage.
def loadCerts(host, port, log, sslSocketFac, certificates, reported, timeout) {
  // Create an ssl socket
  def sslSock = null

  // Writing to sockets has a chance of failure, so lets try/catch.
  try {
    // Do we have an existing socket factory?
    if (sslSocketFac == null) {
      // No, lets create one
      // We use a custom trust manager which allows all certificates.
      def trustManager = new LMEmptyX509TrustManager(log, certificates)

      // In the case of TLSv1.2, we want to use our custom trust manager.
      def context = SSLContext.getInstance("TLSv1.2")
      context.init(null, [trustManager] as TrustManager[], null)

      // Store this socket factory for later use.
      sslSocketFac = context.getSocketFactory()
    }

    // Start with a default socket.
    def sock = new Socket()

    // Connect it to our host and port.
    sock.connect(new InetSocketAddress(host, port.toInteger()), timeout)

    // Create an ssl socket for our host and port, using our socket.
    sslSock = sslSocketFac.createSocket(sock, host, port.toInteger(), true)

    // Specify some common ciphers.
    def ciphers = new HashSet<String>()
    ciphers.addAll(Arrays.asList(sslSock.getEnabledCipherSuites()))
    ciphers.add("SSL_RSA_WITH_RC4_128_SHA")
    ciphers.add("SSL_RSA_WITH_RC4_128_MD5")


    sslSock.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()]))

    // Make a request to send. The text doesnt seem to matter as we don't care about the response.
    def request = "GET / HTTP/1.1\r\nHost:${host}:${port}\r\nConnection:Close\r\nUser-Agent:SSLClient/1.0\r\n\r\n"

    // Create a writer we will use to send our request.
    def writer = new OutputStreamWriter(sslSock.getOutputStream(), "utf-8")

    log.push("Try to send request to server.")

    // Write the request to the output stream.
    writer.write(request)

    log.push("Request sent...")

    // Make sure the output stream has consumed our request.
    writer.flush()

    log.push("Request flushed...")
  }
  catch (Exception e) {
    // Something went wrong! Will be investigated later in the script.
    log.push("Socket Error: ${e}")

    if (e.message.contains("unrecognized_name") && !reported) {
      states["UnrecognisedName"] = 1
      reported = true
    }

    // If the ssl sock isn't closed already, we need to do it!
    if (sslSock != null) {
      sslSock.close()
    }

    return false
  }

  // If the ssl sock isn't closed already, we need to do it!
  if (sslSock != null) {
    sslSock.close()
  }

  return true
}

// This custom trust manager allows all certificates, but extacts them as it does so.
class LMEmptyX509TrustManager implements TrustManager, X509TrustManager {
  public def managerLog
  public def certificates

  private LMEmptyX509TrustManager(log, certs) {
    managerLog = log
    certificates = certs
  }

  // We need to overwrite all typical elements of a trust manager to not fail validation on expire.
  @Override
  X509Certificate[] getAcceptedIssuers() {
    managerLog.push("TrustManager: getAcceptedIssuers called.")

    return null
  }

  // We need to overwrite all typical elements of a trust manager to not fail validation on expire.
  @Override
  void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
    managerLog.push("TrustManager: checkServerTrusted got ${(certs == null ? 0 : certs.length)} certs. Auth type: ${authType}")

    // Instead of checking if the cert is trusted...save it to our global storage!
    if (certs) {
      certificates.clear()
      certificates.addAll(certs)
    }
  }

  // We need to overwrite all typical elements of a trust manager to not fail validation on expire.
  @Override
  void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
    managerLog.push("TrustManager: checkClientTrusted got ${(certs == null ? 0 : certs.length)} certs. Auth type: ${authType}")
  }
}

instances.each{
  def states = [
    "UnrecognisedName"    :  0,
    "Valid"          :  0,
    "HostNameMismatch"    : -1,
    "TimestampCheckFailed"  : -1,
    "SelfSignedCertificate"  : -1,
    "SelfIssuedCertificate"  : -1,
    "InvalidCertificationPath": -1,
    "ConnectionReset"      : -1,
    "UnknownHostIPFallback"  : -1,
    "OtherException"      : -1
  ]

  def isDebug = false

  def ips = hostProps.get("system.ips") ? hostProps.get("system.ips").split(",") : []
  host = it[0]
  port = it[1]
  name = it[2]
  def timeout = 10000
  def log = []
  def certificates = []
  def reported = false // Create a bool to track if we've alerted (don't want to double alert!).
  def sslSocketFac // We store the socket factory so it can be reused.

  // we specify these properies to prevent some auth types.
  System.setProperty("jdk.tls.disabledAlgorithms", "SSLv3, DH keySize < 768")
  Security.setProperty("jdk.tls.disabledAlgorithms", "SSLv3, DH keySize < 768")

  // Attempt collection of the certificates via our functions!
  if (!loadCerts(host, port, log, sslSocketFac, certificates, reported, timeout)) {
    ips.any { ip -> // Something has gone wrong. Try the IP instead of the hostname.
      if (loadCerts(ip, port, log, sslSocketFac, certificates, reported, timeout)) {
        return true // If we get a successful run, don't process the other IP
      }
    }
  }

  def selfSigned = false
  def selfIssued = false

  if (certificates) { // Did we get some certificates?
    // Yes! We can calculate expiry time!
    // Get the current time and add 10 years as a base expiry time.
    def time = Calendar.getInstance()
    time.add(1, 10)
    def expire = time.getTime()

    // Get epoch time to use as a base issue time.
    time.setTimeInMillis(0)
    issue = time.getTime()

    certificates.eachWithIndex { cert, index -> // Loop through all certificates...
      if (cert.getIssuerDN() == cert.getSubjectDN()) { // Is the issuer the same as the subject?
        // Yes! This cert is self issued or self signed.
        selfIssued = cert.isSelfIssued(cert)
        selfSigned = cert.isSelfSigned(cert, null)
      }

      if (cert.getNotBefore().after(issue)) { // Is the certificate time after the issue time?
        issue = cert.getNotBefore() // Update the issue time to the certificate's time.
      }

      if (cert.getNotAfter().before(expire)) { // Is the certificate time before the expiry time?
        expire = cert.getNotAfter() // Update the expiry time to the certificate's time.
      }
    }
  }

  // We can start a new connection with validation enabled to get validation info.
  try {
    // Create a new ssl socket factory with a default context and strict verification (similar to IE6+ internally).
    def sslSocketFactory = new SSLSocketFactory(SSLContext.getDefault(), SSLSocketFactory.STRICT_HOSTNAME_VERIFIER)

    // Create a new socket with our host and port.
    def socket = new Socket(host, port.toInteger())

    // Create an ssl socket for our host and port, using our socket.
    def sslsocket = sslSocketFactory.createSocket(socket, host, port.toInteger(), true)

    // Specify some common ciphers.
    def ciphers = new HashSet<String>()
    ciphers.addAll(Arrays.asList(sslsocket.getEnabledCipherSuites()))
    ciphers.add("SSL_RSA_WITH_RC4_128_SHA")
    ciphers.add("SSL_RSA_WITH_RC4_128_MD5")

    // Enable the ciphers on our ssl socket.
    sslsocket.setEnabledCipherSuites(ciphers.toArray(new String[ciphers.size()]))

    // Begin a handshake. At this point we will find out if we have an exception.
    sslsocket.startHandshake()

    states["Valid"] = 1

    states.each{ k,v->
      if(v == -1) {
        states[k] = 0
      }
    }
  }
  catch (Exception e) {
    // The certificate is invalid!
    log.push("Message: $e.message")

    // Does the exception contain the string "doesn't match"?
    if (e.message.contains("doesn't match")) {
      // Yes! This is probably a mismatch.
      states["HostNameMismatch"] = 1
      reported = true
    }

    // Does the exception contain the string "timestamp check failed"?
    if (e.message.contains("timestamp check failed")) {
      // Yes! This is a time related issue such as an expiry.
      states["TimestampCheckFailed"] = 1
      reported = true
    }

    // Does the exception contain the string "valid certification path"?
    if (e.message.contains("valid certification path")) {
      // Yes! This is an error further down the path (such as an invalid root or self-sign).

      if (selfSigned) {
        states["SelfSignedCertificate"] = 1
      } else {
        if (selfIssued) {
          states["SelfIssuedCertificate"] = 1
        } else {
          states["InvalidCertificationPath"] = 1
        }
      }

      reported = true
    }

    if (e.message.contains("Connection reset")) {
      states["ConnectionReset"] = 1
      reported = true
    }

    if (e.toString().contains("UnknownHostException")) {
      states["UnknownHostIPFallback"] = 1
      reported = true
    }

    // Have we reported a specific error?
    if (!reported) {
      // No! The Exception is unknown, but still important!
      states["OtherException"] = 1

      println "Debug Information - The following issue was detected but doesn't have a known solution. Reporting as 'Other': ${e.message}"
    }
  }

  println("${host}:${port}##${name}######selfSigned=${states["SelfSignedCertificate"]==1 ? true : false}")

  if (isDebug && log) {
    println "--- Debug Info ---\n${log.join('\n')}"
  }

}

return 0

 

Link to post
Share on other sites
  • 0
  • Administrators

There are several ways of doing this, the easiest being an Active Discovery filter. However, in order to setup an Active Discovery filter, you need to have a DataSource that is already discovering the certificates you want to monitor. Are you looking for a DataSource or do you already have one that works that needs to exclude the self-signed certs? If so, what's the name of the DS or its locator code (found at the top of the DataSource settings page)?

Link to post
Share on other sites
  • 0
1 minute ago, Stuart Weenig said:

There are several ways of doing this, the easiest being an Active Discovery filter. However, in order to setup an Active Discovery filter, you need to have a DataSource that is already discovering the certificates you want to monitor. Are you looking for a DataSource or do you already have one that works that needs to exclude the self-signed certs? If so, what's the name of the DS or its locator code (found at the top of the DataSource settings page)?

The datasource I am using is SSL_Certificates from the LM repository. If I could just exclude selfsigned on that one that would be great!

Link to post
Share on other sites
  • 0
  • Administrators

Ok, that makes it a bit more difficult because of how discovery runs in that DS. Discovery simply checks if certain ports are open. It isn't until collection that the actual cert is downloaded and inspected. Discovery itself doesn't pull any actual properties of the certificate itself (like whether or not it's self signed). It's a pity really and I think the discovery on this DS is lacking actual discovery.

In order to use an Active Discovery filter, the discovery script would have to discover whether or not each cert is self-signed and store that as a property. So what you'll have to do is add logic from the collection script into the discovery script so that the self-signed status gets stored as a property on the instance and can then be used to filter out those certs from discovery.

The logic in the collection script is a bit complex (some developer really flexed his OOP skills). Take a look at it and see what you can do. It's beyond my skills to extract the required logic with the limited time I have (would probably take me a few days to iron it out). If you really want to automate this (a worthy goal IMO), I suggest reaching out to your CSM to talk about professional services or see if someone on the community may have already cracked this nut.

 

All that said, if this is a small thing, then it would probably work to just create instance groups and sort them out manually. If it's more than a few server's certs, then automation is the only way to go. There may be a simpler way that's not occurring to me at the moment.

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Answer this question...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.