Script to send Email alerts on Expiring certificates for Important Certificate Templates
Published Feb 13 2020 12:00 AM 42K Views
Microsoft

 

Hi all! Zoheb Shaikh here again, and this time I will be sharing an interesting script to alert on Expiring certificates.

 

A special thank you goes out to Eddy Ng Seng Eu  for help in development of this Script. Eddy Ng is a PowerShell champion based out of Malaysia whom I always reach out to when I need help.

 

As a part of Mission Critical team, we always go above and beyond to help our SMC customers. SMC is part of Microsoft’s family of Premier Support offerings which delivers personalized support coverage through designated support professionals who understand a customer’s unique solution configuration and deployment environment, facilitating faster response time and more effective problem resolution. To know more about SMC, reach out to your Microsoft Technical Account Manager.

 

Coming back to the purpose of this post I want to share something interesting that I came across recently where one of our SMC customers had an important internal certificate Expired and no one had a clue until the users started shouting that application is no longer working. 

                                                                 

So the application stopped working because of certificate expiration from an internal issued Certificate Authority, had there been a mechanism to alert on Certificate expiration this could have been avoided, my customer was looking for a quick fix around this which would have below capabilities :-

  1. We discussed on enabling Certificate expiry notification for certificates expiring in the next 30 Days.
  2. These notifications need to be sent over email to the certificate Owner and a common DL
  3. Owners of the certificate to be Emailed if Email Address attribute is published
  4. Expiry notifications needs to be sent only for specific/Important templates because there are millions of certificates issued and 100s expiring every day

 

We had above things to be considered in preparing something as a quick fix to the problem they experienced and there is a plan to make this solution better with time (I will share this in time to come).

 

There were a couple of scripts we saw on gallery.technet which helped us get closer to the below script.

https://gallery.technet.microsoft.com/scriptcenter/Certificate-expiry-Alert-2f63c2d5

https://gallery.technet.microsoft.com/scriptcenter/Monitor-certificate-9d7a2141

 

With the assistance of Eddy Ng, the script has been modified to produce an output like below in the email.

 

image001.jpg

 

Below is filter applied in the Script to choose only the important Certificate Templates you want to be alerted and If needed you could also modify the duration for Certificate expiry from 30 days to a duration of your choice.

 

#variables

#filter template list

$filterlist ="Copy of User","EFS"

#setup duration

$duration = 30

 

 

Please find the script below in text and as attachment also at the end of the blog.

Pre-requisite:

  • Public Key Infrastructure PowerShell module
  • Email server connectivity
  • Tested with Windows Server 2016


Installation:

  • Connect on your PKI CA server (issuing CA) using RDP or Local Logon
  • Download and install the PKI PowerShell module
  • Save this in a PKI scripts folder

 

Create a script file with the following source code:

 

<#Sample scripts provided are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.#>

 

#functions

 

Function Send-CertificateList

{

    #initialize email pre-reqs

    $FromAddress = 'emailaddress@domainname.com'

    $ToAddress  = 'emailaddress@domainname.com'

    $MessageSubject = "Certificate expiration reminder from $env:COMPUTERNAME.$env:USERDNSDOMAIN"

    $SendingServer = 'smtp.office365.com'

    $SmtpServerPort = "Port Number"

 

    #Test if SMTP server is responding

    if(Test-Connection -Cn $SendingServer -BufferSize 16 -Count 1 -ea 0 -quiet){

            #Send email

            Send-MailMessage -From $FromAddress -To $ToAddress -Subject $MessageSubject -Body $mailbody -BodyAsHtml -SmtpServer $SendingServer -Port $SmtpServerPort

    }else{

            #Error Handling

            write-host -object 'No connection to SMTP server. Failed to send email!'

    }

}

 

Function Send-Certificatemail

{

    #initialize email pre-reqs

    $FromAddress = 'emailaddress@domainname.com'

    $CCAddress  = 'emailaddress@domainname.com'

    $MessageSubject = "Certificate expiration reminder from $env:COMPUTERNAME.$env:USERDNSDOMAIN"

    $SendingServer = 'smtp.office365.com'

    $SmtpServerPort = "Port Number"

 

    #Test if SMTP server is responding

    if(Test-Connection -Cn $SendingServer -BufferSize 16 -Count 1 -ea 0 -quiet){

            #Send email

            Send-MailMessage -From $FromAddress -To $ToAddress -Cc $CCAddress -Subject $MessageSubject -Body $Emailbody -BodyAsHtml -SmtpServer $SendingServer -Port $SmtpServerPort

    }else{

            #Error Handling

            write-host -object 'No connection to SMTP server. Failed to send email!'

    }

}

 

# --------------------------------------------------

 

#HTML Style

$style = @'

<style>body{font-family:`"Calibri`",`"sans-serif`"; font-size: 14px;}

@font-face

       {font-family:`"Cambria Math`";

       panose-1:2 4 5 3 5 4 6 3 2 4;}

@font-face

       {font-family:Calibri;

       panose-1:2 15 5 2 2 2 4 3 2 4;}

@font-face

       {font-family:Tahoma;

       panose-1:2 11 6 4 3 5 4 4 2 4;}

       table{border: 1px solid black; border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;}

       th{border: 1px solid black; background: #dddddd; padding: 5px; }

       td{border: 1px solid black; padding: 5px; }

       .crtsn{font-weight: bold; color: blue; }

       .crtexp{font-weight: bold; color: red; }

       .crtcn{font-weight: bold; color: orange; }

       </style>

'@

 

# --------------------------------------------------

#variables

#filter template list

$filterlist ="Copy of User","EFS"

#setup duration

$duration = 30

#setup strDate with yyyyMMdd-HHmmss

$strDate = get-date -format yyyyMMdd-HHmmss

#create unique export file name

$exportFileName = "certificates_" + $strDate + ".csv"

#date of Now

$now = (Get-Date)

#date of Then

$Then = (Get-Date).AddDays($duration)

#empty mailbody

$mailbody = ""

#empty array initialization

$table = @()

 

# --------------------------------------------------

#variables

 

#export certificates to CSV

certutil.exe -view csv > $exportFileName

 

#Import certificate info where Serial Number is not "empty" with various properties

$importall = Import-Csv $exportFileName | Where-Object {$_.'Serial Number' -notcontains 'EMPTY'} | Select-Object -Property 'Request ID','Serial Number','Requester Name','Certificate Expiration Date','Certificate Template','Request Common Name','Request Disposition' -ErrorAction SilentlyContinue

 

#Run through each ObjectID  to get the Certificate Template Name

foreach ($OID in (get-catemplate).Oid)

{

    #populate the field "Certificate Template"

    $importall | where-object "certificate template" -match $OID | foreach-object {

        #ensure whitespaces removed

        $_.'Certificate Template' = ($_.'Certificate Template').replace($OID+" ","")

    }

 

}

#filter only required certificates based on $filterlist

$importall = $importall | where-object "certificate template" -in $filterlist

 

#build email body

$mailbody += '<html><head><meta http-equiv=Content-Type content="text/html; charset=utf-8">' + $style + '</head><body>'

$mailbody += "The certificate expiry details:<br />"

 

#collect cultureinfo for short date and time pattern

$cultureinfo = Get-Culture

$formatdata = "$($cultureinfo.DateTimeFormat.ShortDatePattern) $($cultureinfo.DateTimeFormat.ShortTimePattern)"

 

#mail body template

$mailbody += '<p>'

$mailbody += 'Hello Reader, '+"<br />"

$mailbody += 'Please find below the list of certificaes Expiring in next ' + $duration + ' days' + "</span><br />"

$mailbody += '</p>'

 

#cycle through array and search for matching cetificates

for($i=0;$i -lt $importall.Count;$i++)

{

     #for each object, get the "certificate expirate date" and convert to [datetime]

     $Certexpirydate = [datetime](Get-date $importall[$i].'Certificate Expiration Date' -Format $formatdata)

     #perform comparison

     If(($Certexpirydate -gt $now) -and ($Certexpirydate -le $then))

 

        {

            #write to console

            write-host -object 'Certificate ID:' $importall[$i].'Request ID' 'with Serial Number:' $importall[$i].'Serial Number' 'will expire in ' -NoNewline; write-host -object ([datetime]($importall[$i].'Certificate Expiration Date') - (get-date)) ' Days!'-ForegroundColor Red

            write-host -object 'This certificate has DN: ' -NoNewline; write-host -object $importall[$i].'Request Distinguished Name' -ForegroundColor DarkYellow

            write-host -object 'Please don`t forget to renew this certificate before expiration date: ' -NoNewline; write-host -object $importall[$i].'Certificate Expiration Date' -ForegroundColor Red "`n"

 

            #save info in table array

            $table += $importall[$i] | Sort-Object 'Certificate Expiration Date' | Select-Object -Property 'Request ID','Serial Number','Requester Name','Certificate Template','Certificate Expiration Date','Request Common Name','Issued Email Address'

        }

}

 

#mailbody html formatting

$mailbody += '<p><table>'

$mailbody += '<th>Request ID</th><th>Serial Number</th><th>Requester Name</th><th>Requested CN</th><th>Certificate Template</th><th>Expiration date</th>'

 

#run through each row

foreach($row in $table)

    {

        #create necessary row information

        $mailbody += "<tr><td>" + $row.'Request ID' + "</td><td>" + $row.'Serial Number' + "</td><td>" + $row.'Requester Name' + "</td><td>" + $row.'Request Common Name' + "</td><td>" + $row.'Certificate Template' + "</td><td>" + $row.'Certificate Expiration Date' + "</td></tr>"

    }

#closing html tags

$mailbody += '</table></p>'

$mailbody += '</body>'

$mailbody += '</html>'

 

#if there are matching certificates found send email

if($table.Count -gt '0')

    {

        #send email

        Send-CertificateList

    }

 

#run through each row

foreach($row in $table)

{

    #if email address exist

    if($($row.'Issued Email Address') -like "*@*")

    {

        #populate to address and email body

        $ToAddress  = $row.'Issued Email Address'

        $emailbody =  "Hello Reader,<br>

                            <br>

                            The certificate requested by you is about to expire :

 

                            <br>

                            Details <br>

                            -------------------- <br>

                            $row

                            <Br>

                            <br>

                            Thanks"

        #send mail

        Send-Certificatemail

    }

}

 

 

Hope this helps,

Zoheb & Eddy

 

31 Comments
Copper Contributor

Thank you for this detailed material and all the examples

Microsoft

Excellent work Zoheb! 

 

Copper Contributor

Thanks @Zoheb Shaikh.

I had to add ",'Request Distinguished Name','Issued Email Address'" to the list of objects selected from the CSV in order to enable per certificate notification.

 

#Import certificate info where Serial Number is not "empty" with various properties

$importall = Import-Csv $exportFileName | Where-Object {$_.'Serial Number' -notcontains 'EMPTY'} | Select-Object -Property 'Request ID','Serial Number','Requester Name','Certificate Expiration Date','Certificate Template','Request Common Name','Request Disposition','Request Distinguished Name','Issued Email Address' -ErrorAction SilentlyContinue
Microsoft

@duhouxt  great addition, thanks for sharing

Copper Contributor

Thanks for your sharing, after corrected the $filterlist then it's working now.

Microsoft

@Puthearith  yes it is the $filterList..Glad this worked for you

Iron Contributor

@Puthearith @Zoheb Shaikh 

Please be specific with exactly what you have to change to make filterlist work.

You mentioned correcting it, but it’s too vague what you meant.

 

Copper Contributor

Hi, the $filterList is means you have to input the right Certificate template types such Web Application or WebServer replace in the script, otherwise it cannot find it.

$filterlist ="DomainController","Domain Controller Authentication","Web Application","WebServer"

Iron Contributor

@Puthearith So, you can’t use template names that you have copied and renamed with a custom name such as “ 123 Corp Web Server With Exportable Private Key?”

You can only use the default template names?

When I tried changing the examples in the script to our actual template names, it failed with errors.

Copper Contributor

@Zoheb Shaikh and @Puthearith  : Will this script not work for templates which we have customized. For example if we have duplicated the template "Web Server" and named it as "Hon_Server" then will this script be able to query and remind of expired certificates ?

Under $filterlist variable i have provided the value of my custom template name . But it does not work

Could you help here ?

 

Copper Contributor

Very Good Info Thanks@Zoheb, next I would be exploring this powerShell script. Do you have any suggestions or tips for executing this script on week or month basis. Thanks.

Microsoft

@rahul8071  verify the name ones, you must use "Template Name" and not "Template Display Name", there is a diffrence

ZohebShaikh_0-1646742617794.png

 

Copper Contributor

Hi Zoheb, the SmtpServerPort should work on both 587 and 25 as well right?

Microsoft

@Sagir_  Yes but this depends on the Provider, best to check with your Messaging admin

Microsoft

@Kalimanne J 

verify the name ones, you must use "Template Name" and not "Template Display Name", there is a diffrence

 

ZohebShaikh_0-1646745980956.png

 

Copper Contributor

Thanks Zoheb yes, I simply triggering outbound traffic over port 25 towards Office 365 cloud (though my firewall team opened both ports 587/25) with Powershell "Send-MailMessage -From NoReply 'Email address removed' and -SmtpServer would be my MX record and public ip added and authenticated in office 365.

Copper Contributor

Hi Zoheb, can you help and explain a bit more for Installation part here ,

1. I am locally logon on to my Windows CA server.
2. Download and install the PKI PowerShell module (since it is a CA server and PKI module already there I checked with "Get-Command -Module PSPKI" )
3. Save this in a PKI scripts folder (what do you mean by this because I simply edited initialize email pre-reqs & #Test if SMTP server is responding related stuff but getting error saying "no connection to SMTP server. Failed to send email!" ) but when I attempted plain and using simple PowerShell command

Send-MailMessage -From NoReplycustomoffice365 -To mydomain -Subject XXX -SmtpServer my MX record and it is working fine. Thanks in advance

Copper Contributor

-

Copper Contributor

@Zoheb Shaikh Hi Zoheb

 

Is there a way to filter by template name with certutil at the export step and get only a speficied templates ?

In my case the file created exceeds 1Gb and it is more deffuclt to import it and then filter on it.

 

Thank you

Copper Contributor

Where does the SMTP authentication happen ? Looks like we can use any sender email to send emails via SMTP as there is no part of code which does SMTP authentication ?

Looks it would be a Spam / Fake email sent from the machine itself without getting SMTP into picture ?

Copper Contributor

Hi @Zoheb Shaikh . 
Thank you for the script but when executing i am having this error: 

Could you please help! Cheers, Mark

It is failing on the line: 

$Certexpirydate = [datetime](Get-date $importall[$i].'Certificate Expiration Date' -Format $formatdata)

The error I am getting is as follows:
Cannot convert value "28/06/2023 15:04" to type "System.DateTime". Error: "String was not recognized as a valid DateTime."
At line:123 char:6
+ $Certexpirydate = [datetime](Get-date $importall[$i].'Certificat ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastParseTargetInvocationWithFormatProvider

Cannot convert value "28/06/2023 15:12" to type "System.DateTime". Error: "String was not recognized as a valid DateTime."
At line:123 char:6
+ $Certexpirydate = [datetime](Get-date $importall[$i].'Certificat ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastParseTargetInvocationWithFormatProvider

Cannot convert value "27/10/2023 16:02" to type "System.DateTime". Error: "String was not recognized as a valid DateTime."
At line:123 char:6
+ $Certexpirydate = [datetime](Get-date $importall[$i].'Certificat ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastParseTargetInvocationWithFormatProvider

Copper Contributor

I also have the same question and issue, where the CSV file is 1GB, and no email is sent.  

"Is there a way to filter by template name with certutil at the export step and get only a specified templates ?

In my case the file created exceeds 1Gb and it is more difficult to import it and then filter on it."

 

Microsoft

@Mark Grindstaff  for sure it is not feasible to run it for all Templates, you want to filter this with important templates.

 

See below part of script you can modify which can give you results only for Specific templates.

#variables

#filter template list

$filterlist ="Copy of User","EFS"

#setup duration

$duration = 30

Copper Contributor

Is it possible that it's not working due to my CA is 2012R2?

 

#variables
#filter template list
$filterlist ="AutoIISWebCertRebind"
#setup duration
$duration = 30

 

MarkGrindstaff_0-1651070841792.png

 

Copper Contributor

I got it to work by using the Template display name

 

MarkGrindstaff_0-1651074469495.png

 

Copper Contributor

Hi  @Mark Grindstaff@Zoheb Shaikh  

Did you manage to find out why it is throwing me an error above?

It is failing on the line: 

$Certexpirydate = [datetime](Get-date $importall[$i].'Certificate Expiration Date' -Format $formatdata)

The error I am getting is as follows:
Cannot convert value "28/06/2023 15:04" to type "System.DateTime". Error: "String was not recognized as a valid DateTime."
At line:123 char:6
+ $Certexpirydate = [datetime](Get-date $importall[$i].'Certificat ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastParseTargetInvocationWithFormatProvider


Cheers, Mark.

Copper Contributor

Some notes:

  • To use STARTTLS apply this change:

Function Send-CertificateList
{
#initialize email pre-reqs
$FromAddress = 'emailaddress@domainname.com'
$ToAddress = 'emailaddress@domainname.com'
$MessageSubject = "Certificate expiration reminder from $env:COMPUTERNAME.$env:USERDNSDOMAIN"
$SendingServer = 'Mail server'
$SmtpServerPort = "Port number"

 

[System.Net.ServicePointManager]::SecurityProtocol = 'TLS12'

 

#Test if SMTP server is responding
if(Test-Connection -Cn $SendingServer -BufferSize 16 -Count 1 -ea 0 -quiet){
#Send email
Send-MailMessage -From $FromAddress -To $ToAddress -Subject $MessageSubject -Body $mailbody -BodyAsHtml -SmtpServer $SendingServer -Port $SmtpServerPort -UseSsl
}else{
#Error Handling
write-host -object 'No connection to SMTP server. Failed to send email!'
}
}

 

Function Send-Certificatemail
{
#initialize email pre-reqs
$FromAddress = 'emailaddress@domainname.com'
$MessageSubject = "Certificate expiration reminder from $env:COMPUTERNAME.$env:USERDNSDOMAIN"
$SendingServer = 'Mail server'
$SmtpServerPort = "Port number"

 

[System.Net.ServicePointManager]::SecurityProtocol = 'TLS12'

 

#Test if SMTP server is responding
if(Test-Connection -Cn $SendingServer -BufferSize 16 -Count 1 -ea 0 -quiet){
#Send email
Send-MailMessage -From $FromAddress -To $ToAddress -Cc $CCAddress -Subject $MessageSubject -Body $Emailbody -BodyAsHtml -SmtpServer $SendingServer -Port $SmtpServerPort -UseSsl
}else{
#Error Handling
write-host -object 'No connection to SMTP server. Failed to send email!'
}
}

  • To check all the certificates and templates, comment out all lines which include the filterlist variable.

That's all!

Copper Contributor

@Mark Grindstaff Hi Mark did you finally export the file with the filters ? could you tell me how you did it ?

thanks 

Copper Contributor

i try the script but get this it anyone know why?

long_lanh_0-1657731832303.png

 

Copper Contributor

Thank you @Zoheb Shaikh and @duhouxt!

I got a little stuck on the 'Issued email address' part since the domain I was in had not populated the email field in the user accounts when the initial user certs were created, therefore, the field was blank in the issued certs. Updating the email field for every user object requiring a certificate and issuing new certificates resolved the problem.

Copper Contributor

Not sure if this is still maintained.  I was wondering if there was a way to export the data that it is emailing to a CSV file as well.  We have an automated process that will open tickets for us on ones that are expiring.

 

thanks

 

app

Version history
Last update:
‎Feb 13 2020 05:20 AM
Updated by: