Shore Up
A night watchman walking past a row of buildings, holding a lantern up to a dated wax seal on each door and jotting the ones about to expire into a ledger.
WindowsSecurity

Automate Reporting of Expiring IIS Certificates Across a Farm

Ketan Aagja8 min read
No ratings yet

Before you run this

This guide builds a read-only report. The script reaches each IIS server in your farm over PowerShell remoting, reads the HTTPS (SSL) bindings, looks up the bound certificate in the local machine store, and emails you a list of anything expiring within a threshold you set. It does not install, bind, delete, or renew any certificate, and it makes no change to IIS or the certificate store. The only thing that persists is a scheduled task, if you choose to create one — and I show you how to remove that at the end.

It does need elevation. Enumerating IIS:\SslBindings and reading Cert:\LocalMachine\My requires administrative rights, and reaching the farm members over WinRM requires an account that is a local administrator on each target server. Run PowerShell as Administrator, and run the whole thing under a domain account with that access.

Even though this is read-only, read the script before you run it and run it against a single test server first — point $servers at one non-production box, confirm the output looks sane, and only then widen the list. Nothing here is destructive, so there is no rollback to worry about beyond deleting the scheduled task.

What I'm assuming

  • Windows Server 2019 or 2022, IIS role installed, so the WebAdministration module is present.
  • Windows PowerShell 5.1 (the in-box version). This also works in PowerShell 7, but 5.1 is what ships and what Task Scheduler will call most cleanly.
  • WinRM is enabled on the farm members (winrm quickconfig, or via GPO) and the WinRM firewall rule is open to your management host.
  • You run the script from a management host in the same domain, under an account that is local admin on every farm member.
  • Certificates are bound in the normal place, Cert:\LocalMachine\My.

If your farm uses a centralized certificate store (CCS) or a shared UNC store, the per-server store lookup below won't apply and you'd read the share directly instead — that's a different procedure.

The collection logic

For each server we enumerate SSL bindings, and for each binding that has a thumbprint we pull the matching certificate and read its NotAfter date. I use the binding's PSChildName (e.g. 0.0.0.0!443 or 0.0.0.0!443!site.example.com) as the identifier because that property is stable; I deliberately don't hand-assemble the IP/port/host string from individual properties, whose names vary by SNI configuration.

#Requires -Version 5.1
#Requires -RunAsAdministrator

# ---- edit these ----
$servers    = 'WEB01.example.com','WEB02.example.com'   # your farm members
$warnDays   = 30                                          # flag certs expiring within N days
$smtpServer = 'smtp.example.com'
$mailFrom   = 'cert-report@example.com'
$mailTo     = 'admins@example.com'
# --------------------

# This block runs ON each remote server
$collect = {
    Import-Module WebAdministration -ErrorAction Stop
    foreach ($b in Get-ChildItem IIS:\SslBindings) {
        if (-not $b.Thumbprint) { continue }                 # binding with no cert
        # $b.Store is normally 'My'; build the store path from it
        $cert = Get-Item -Path "Cert:\LocalMachine\$($b.Store)\$($b.Thumbprint)" `
                         -ErrorAction SilentlyContinue
        if (-not $cert) { continue }                         # thumbprint not found locally
        [pscustomobject]@{
            Server     = $env:COMPUTERNAME
            Binding    = $b.PSChildName                       # e.g. 0.0.0.0!443
            Subject    = $cert.Subject
            NotAfter   = $cert.NotAfter
            Thumbprint = $cert.Thumbprint
        }
    }
}

# Fan out across the farm
$all = Invoke-Command -ComputerName $servers -ScriptBlock $collect

Invoke-Command runs the block in parallel across the named servers and returns the custom objects to your session. If a server is unreachable you'll get a clear per-server error rather than a silent gap.

Filtering and emailing the report

$expiring = $all |
    Where-Object { $_.NotAfter -le (Get-Date).AddDays($warnDays) } |
    Sort-Object NotAfter |
    Select-Object Server, Binding, Subject, NotAfter,
        @{ n = 'DaysLeft'; e = { [int](($_.NotAfter - (Get-Date)).TotalDays) } },
        Thumbprint

if ($expiring) {
    $html = $expiring |
        ConvertTo-Html -Title 'Expiring IIS certificates' |
        Out-String

    Send-MailMessage -SmtpServer $smtpServer -From $mailFrom -To $mailTo `
        -Subject "IIS certificates expiring within $warnDays days" `
        -Body $html -BodyAsHtml
}
else {
    Write-Host "No IIS certificates expiring within $warnDays days."
}

One honesty note: Send-MailMessage is officially obsolete — Microsoft flags it in its own docs and no longer develops it — but it still works and remains the least-friction option for an internal report. If you'd rather not use it, the common replacement is a maintained mail library such as MailKit (via the Send-MailKitMessage community module); I'm not walking through it here. If your SMTP relay requires authentication or a specific port, add -Credential and -Port — check the Send-MailMessage reference on Microsoft Learn for the exact parameters before you rely on it.

Save the whole thing as, for example, C:\Scripts\Report-ExpiringCerts.ps1.

Run it once, by hand, to verify

Before scheduling anything, run the script interactively against one server and inspect the objects — no email needed:

# Temporarily set $servers = 'WEB01.example.com' and run just the collection,
# then look at everything it found (not only the expiring ones):
$all | Sort-Object NotAfter | Format-Table Server, Binding, Subject, NotAfter -AutoSize

If that table matches what you see in IIS Manager (Sites → binding → Edit → certificate) on that box, the lookup is working. Then set $warnDays high (say 3650) for a dry run so the filter catches everything, confirm the email arrives and renders, and finally set $warnDays back to your real threshold.

Schedule it

Once the manual run is clean, register a daily task. These cmdlets are from the in-box ScheduledTasks module:

$action  = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Report-ExpiringCerts.ps1"'

$trigger = New-ScheduledTaskTrigger -Daily -At 7am

Register-ScheduledTask -TaskName 'IIS Cert Expiry Report' `
    -Action $action -Trigger $trigger `
    -User 'EXAMPLE\svc-certreport' -RunLevel Highest

Replace EXAMPLE\svc-certreport with a service account that is local admin on the farm members. Register-ScheduledTask will prompt for that account's password (or use a group-managed service account with the appropriate -Principal/-LogonType — see the Register-ScheduledTask reference on Microsoft Learn for the exact parameter set). Replace all example.com, server names, and the script path with your own.

Verify the scheduled task

Confirm it's registered and run it on demand:

Get-ScheduledTask -TaskName 'IIS Cert Expiry Report'
Start-ScheduledTask  -TaskName 'IIS Cert Expiry Report'
Get-ScheduledTaskInfo -TaskName 'IIS Cert Expiry Report'   # LastRunTime / LastTaskResult

A LastTaskResult of 0 means the task ran without error. Check your inbox — or, if nothing was expiring, check that the task completed and simply had nothing to send.

Undo

There's nothing to reverse in IIS or the certificate store — the script never touched them. The only artifact is the scheduled task, and the script file itself:

Unregister-ScheduledTask -TaskName 'IIS Cert Expiry Report' -Confirm:$false
Remove-Item 'C:\Scripts\Report-ExpiringCerts.ps1'

That returns the environment to exactly where it started.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Automate Certificate Expiry Checks on Windows with PowerShell

An expired TLS certificate is the kind of outage that is entirely preventable and still catches everyone. This guide builds a small PowerShell script that reads the certificates in a server's own store, flags any that expire soon, and then schedules it to run daily so you hear about it weeks in advance instead of from a monitoring alert at 2 a.m.

9 min read

Email a Daily Windows Event Log Error Summary with PowerShell

This guide builds a small PowerShell script that reads the last 24 hours of Critical and Error events from the System and Application logs, writes them to an HTML file, and emails that file to you. It's a read-only report: it does not clear, modify, or delete any event log. The only things it creates on the system are the HTML report files in a folder you choose and — in the last section — a scheduled task.

10 min read

Monitor Free Disk Space and Email an Alert with PowerShell

This script reads the free space on the fixed local disks of the machine it runs on and emails you when any of them drops below a percentage you set. It is a lightweight stand-in for a monitoring platform — good for a handful of servers, not a replacement for one across a fleet.

8 min read

Generate a Disk-Space Report Across Servers with PowerShell Remoting

This guide builds a read-only disk-space report. It uses PowerShell remoting ( Invoke-Command over WinRM) to query each server's local fixed disks and returns size, free space, and percent free as one combined table you can export to CSV or HTML. It does not write to, resize, or clean up any disk — it only reads WMI/CIM data.

10 min read