There are news about some services forget to update TLS certificates from time to time.
It’s even more critical when you are hosting many sites with bunch of domains. Monitoring this is not difficult, it’s often caused by negligence.
In this post, we will build a very simple tool using Node.js. After then, you can deploy to AWS Lambda or wherever that can be triggered periodically.
The TLS Module#
We only need TLS core module and nothing else. The steps are pretty simple:
- Connect to the domain we want to check.
- Get Peer Certificate.
- Calculate remaining days.
- There is even no step 4!
1. Connect to the domain we want to check#
Now, let’s get TLS module and use it to connect to the target:
const tls = require('tls')
const socket = tls.connect({ host, port, servername })
//...
With the tls socket returned by tls.connect
, we can add listeners on it for further actions:
socket.once('secureConnect', () => { /*...*/ })
socket.once('close', () => { /*...*/ })
socket.once('error', error => { /*...*/ })
2. Get Peer Certificate#
After we successfully connected to the host (secureConnect
event), we can get peer certificate using tlsSocket.getPeerCertificate.
The Certificate object contains a lot of information, such as issuer, subject, fingerprint…etc. But what we really care here is the valid_from
and valid_to
fields. If you only want to check when will it expire, then the latter one is the only thing you need.
We can just disconnect (via socket.destroy([error])) the connection afterwards since there is no need to keep it.
Also, because we will likely reuse this simple tool to check lots of domains, it’s ideal to turn it to a promise-based function.
const tls = require('tls')
const TIMEOUT = 1500
const getCertExpiry = (host, port, servername) => {
return new Promise((resolve, reject) => {
const result = {}
const socket = tls.connect({ host, port, servername })
socket.setTimeout(TIMEOUT)
socket.once('secureConnect', () => {
const peerCert = socket.getPeerCertificate()
result.validFrom = peerCert.valid_from
result.validTo = peerCert.valid_to
socket.destroy()
})
socket.once('close', () => resolve(result))
socket.once('error', reject)
socket.once('timeout', () => {
socket.destroy(new Error(`Timeout after ${TIMEOUT} ms for ${servername}:${port}`))
})
})
}
Q: Why do we need to set timeout for socket?#
When I tried to connect valid domain but incorrect port (e.g. google.com:4133
), it’s will get stuck.
So, instead of letting it stuck, we can leverage socket.setTimeout(timeout[, callback])
to solve it.
Q: When do I need to specify servername?#
A: When you are dealing with SNI. Check the document here.
3. Calculate remaining days#
We now have the valid_to
date. What we need to implement is a little function to calculate the remaining days of this cert:
const getRemainingDays = date => {
const expiry = new Date(date).valueOf()
const now = new Date().valueOf()
return ((expiry - now) / 1000 / 60 / 60 / 24).toFixed(2)
}
That’s it.
Complete working example#
The following is a working example of everything said above.
Change the domains, add alarm mechanism, deploy to anywhere you like, and get informed!
const tls = require('tls')
const TIMEOUT = 1500
const getRemainingDays = date => {
const expiry = new Date(date).valueOf()
const now = new Date().valueOf()
return ((expiry - now) / 1000 / 60 / 60 / 24).toFixed(2)
}
const getCertExpiry = (host, port, servername) => {
return new Promise((resolve, reject) => {
const result = {}
const socket = tls.connect({ host, port, servername })
socket.setTimeout(TIMEOUT)
socket.once('secureConnect', () => {
const peerCert = socket.getPeerCertificate()
result.validFrom = peerCert.valid_from
result.validTo = peerCert.valid_to
socket.destroy()
})
socket.once('close', () => resolve(result))
socket.once('error', reject)
socket.once('timeout', () => {
socket.destroy(new Error(`Timeout after ${TIMEOUT} ms for ${servername}:${port}`))
})
})
}
const checkCertExpiration = async (host, port = 443, servername = host) => {
const { validTo } = await getCertExpiry(host, port, servername)
const remainingDays = getRemainingDays(validTo)
return { validTo, remainingDays }
}
const main = async () => {
const domains = ['google.com', 'facebook.com', 'wtcx.dev', '??????????.com']
const tasks = domains.map(domain => checkCertExpiration(domain))
const results = await Promise.allSettled(tasks)
for (let i = 0; i < domains.length; i++) {
const result = results[i]
if (result.status === 'fulfilled') {
const { validTo, remainingDays } = result.value
console.log(`${domains[i]}'s cert is valid until ${validTo}. Remaining Days: ${remainingDays}`)
} else {
console.error(`Error checking ${domains[i]}: ${result.reason}`)
}
}
}
main()
$ node app.js
google.com's cert is valid until Nov 3 08:53:40 2020 GMT. Remaining Days: 73.69
facebook.com's cert is valid until Oct 12 12:00:00 2020 GMT. Remaining Days: 51.82
wtcx.dev's cert is valid until Nov 14 14:39:25 2020 GMT. Remaining Days: 84.93
Error checking ??????????.com: Error: getaddrinfo ENOTFOUND ??????????.com