
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.
Before you run this
What it does: The core script is read-only. It enumerates the certificates in the Local Machine "Personal" store (Cert:\LocalMachine\My), compares each certificate's expiry date against a threshold you set, and prints (and optionally emails) the ones due to expire. It does not modify, renew, move, or delete any certificate.
Privileges: Reading the Local Machine store reliably — including certificates whose private keys are protected — needs an elevated PowerShell session (Run as administrator). If you later register the scheduled task to run as SYSTEM, that registration step also requires elevation. The reporting script alone, run interactively, will show you certificate metadata even unprivileged, but run it elevated so you see the full store.
Test first: Read the script before you run it. Run the reporting part interactively on a test or non-production server and confirm the list it prints matches what you see in certlm.msc. Only once you trust its output should you schedule it. Nothing here is destructive, but the scheduled task is a change to the system — I show how to remove it cleanly at the end.
Assumptions: Windows Server 2019 or 2022, Windows PowerShell 5.1 (the version that ships in the box), and the built-in ScheduledTasks module. The commands work the same on Windows 10/11. I'm checking Cert:\LocalMachine\My, which holds the machine's own service certificates (IIS bindings, RDP, LDAPS, and so on).
Step 1 — Read the certificate store
PowerShell exposes the certificate stores as a drive through the certificate provider, so you browse them like a filesystem. Each item you get back is an X.509 certificate object with a NotAfter property (the expiry date) and a NotBefore property.
Start interactively so you can see what you're working with:
# List every certificate in the machine's Personal store
Get-ChildItem -Path Cert:\LocalMachine\My |
Select-Object Subject, NotAfter, Thumbprint |
Sort-Object NotAfter |
Format-Table -AutoSize
NotAfter is a real DateTime, so filtering on it is straightforward. Confirm the subjects and dates match what certlm.msc shows before going further.
Step 2 — The expiry-check script
Save this as C:\Scripts\Check-CertExpiry.ps1. Substitute your own path if you keep scripts elsewhere.
# Check-CertExpiry.ps1
# Reports certificates in the Local Machine Personal store that expire soon.
# Read-only: it does not change any certificate.
param(
# Warn about certificates expiring within this many days
[int]$WarningDays = 30,
# Store to inspect. LocalMachine\My = the machine's own certificates.
[string]$StorePath = 'Cert:\LocalMachine\My',
# Optional: write the report to this CSV path. Leave empty to skip.
[string]$CsvPath = ''
)
$threshold = (Get-Date).AddDays($WarningDays)
# Pull matching certificates and compute days remaining for readability
$expiring = Get-ChildItem -Path $StorePath |
Where-Object { $_.NotAfter -le $threshold } |
Select-Object Subject,
Thumbprint,
NotAfter,
@{ Name = 'DaysLeft'
Expression = { [int](($_.NotAfter) - (Get-Date)).TotalDays } } |
Sort-Object NotAfter
if (-not $expiring) {
Write-Output "No certificates in $StorePath expire within $WarningDays days."
return
}
$expiring | Format-Table -AutoSize
# Optional CSV export for a paper trail
if ($CsvPath) {
$expiring | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
Write-Output "Report written to $CsvPath"
}
Run it interactively to check the logic:
# Temporarily lower the threshold so you can see it catch something real
.\Check-CertExpiry.ps1 -WarningDays 3650
A large -WarningDays value should return most of your certificates — a good way to confirm the filter and the DaysLeft calculation work before you rely on the default 30-day window.
Step 3 — Get the results to a human
Printing to the console is fine when you run it by hand, but a scheduled run needs to reach you. Two standard options:
Email. In PowerShell 5.1 the built-in cmdlet is Send-MailMessage. Note that Microsoft has marked Send-MailMessage as obsolete — it still works and is fine for an internal notification, but it is no longer recommended for new secure-transport work, so check the current guidance on Microsoft Learn before wiring it to anything sensitive. A minimal internal notification looks like this — replace every placeholder:
if ($expiring) {
$body = $expiring | Format-Table -AutoSize | Out-String
Send-MailMessage `
-SmtpServer 'smtp.example.com' `
-From 'certcheck@example.com' `
-To 'admins@example.com' `
-Subject "Certificates expiring soon on $env:COMPUTERNAME" `
-Body $body
}
Add this block to the end of the script if you want email. Test it against your own relay first — an SMTP server that silently drops mail is worse than no alert.
File / log. The -CsvPath parameter already lets you drop a report somewhere a monitoring tool can pick it up. Writing to the Windows Event Log is also possible, but it requires you to register an event source first with New-EventLog; check the docs for the exact syntax if you go that way.
Step 4 — Schedule it to run daily
Use the built-in ScheduledTasks cmdlets. This registers a task that runs the script every morning as SYSTEM. Run these in an elevated session:
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\Check-CertExpiry.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
# Run as SYSTEM with full rights so it can read the machine store
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' `
-LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Certificate Expiry Check' `
-Action $action -Trigger $trigger -Principal $principal `
-Description 'Daily check for soon-to-expire machine certificates'
-ExecutionPolicy Bypass applies only to this one invocation; it does not change the machine-wide policy. If you would rather set the policy properly, see Set-ExecutionPolicy on Microsoft Learn.
Verify it worked
Confirm the task is registered and check its schedule:
Get-ScheduledTask -TaskName 'Certificate Expiry Check'
Run it once on demand rather than waiting for 7 a.m., then look at the result of the last run:
Start-ScheduledTask -TaskName 'Certificate Expiry Check'
Get-ScheduledTaskInfo -TaskName 'Certificate Expiry Check'
Get-ScheduledTaskInfo reports LastRunTime and LastTaskResult; a result of 0 means it completed without error. If you enabled the CSV export, confirm the file appeared and lists the certificates you expect. If you enabled email, confirm the message arrived — and remember a task that runs successfully but sends nothing simply means nothing is expiring within the window, which is the good outcome.
You can also open Task Scheduler (taskschd.msc) and find the task under the Task Scheduler Library to review history in the GUI.
Undo
The script itself changes nothing, so there is nothing to reverse there. To remove the scheduled task:
Unregister-ScheduledTask -TaskName 'Certificate Expiry Check' -Confirm:$false
Then delete C:\Scripts\Check-CertExpiry.ps1 if you no longer want it. If you registered an event log source, that is the only other artifact to clean up.
That's the whole thing: a read-only report you trust, wrapped in a daily task, so a lapsing certificate becomes a calendar item weeks out instead of an incident.
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.
