---
title: "Find Expired Windows Certificates Instantly via PowerShell"
description: "Use PowerShell to scan every certificate store across a Windows fleet, report expired and expiring certificates, and alert before authentication breaks."
canonical: "https://adamtheautomator.com/find-expired-certificates-powershell/"
---

# Find Expired Windows Certificates Instantly via PowerShell

> Use PowerShell to scan every certificate store across a Windows fleet, report expired and expiring certificates, and alert before authentication breaks.

Source: https://adamtheautomator.com/find-expired-certificates-powershell/

---

ATA Learning

Tap to hide

[

ATA Learning

](/)

*   [Home](/)
*   [Tutorials](/tutorials/)
*   [Instructors](/author/)
*   [Advertising](/advertising/)
*   [Recommended Resources](/resources/)
*   [About Adam](/about-adam/)

Search for:  

*   [](https://twitter.com/adbertram)
*   [](https://github.com/Adam-the-Automator)
*   [](https://www.linkedin.com/company/adam-the-automator-llc)
*   [](/feed/)

![Find Expired Windows Certificates Instantly via PowerShell](https://adamtheautomator.com/wp-content/uploads/publisher/3ec5d9c85b2b8198b651eec70c447ee1/bc27e65fd261774769a0da850c0fe0f594bb2c6ba5bfcf317eadea5471382804.jpg)

# Find Expired Windows Certificates Instantly via PowerShell

[![](https://secure.gravatar.com/avatar/d0b9d42e21e5622713f8b693aa5c0f9244d5f7dd200ed29b8398f52dee5de337?s=192&d=mm&r=g)Adam Bertram](https://adamtheautomator.com/author/adam-bertram/)7 October 202611 min. read

Categories: [Information Security](/category/infosec/)

Tags:[PowerShell](/tag/powershell/)[Certificates](/tag/certificates/)[Security](/tag/security/)[Windows](/tag/windows/)

Table of Contents

*   [Find Every Expired Certificate Instantly with One Command](#find-every-expired-certificate-instantly-with-one-command)
*   [Four Ways to Read Certificate Stores, and When Each Wins](#four-ways-to-read-certificate-stores-and-when-each-wins)
*   [The Cert: PSDrive and the Store Locations That Matter](#the-cert-psdrive-and-the-store-locations-that-matter)
*   [LocalMachine or CurrentUser: Where a Service Looks](#localmachine-or-currentuser-where-a-service-looks)
*   [Filter on NotAfter and NotBefore with Get-Date.AddDays](#filter-on-notafter-and-notbefore-with-get-dateadddays)
*   [Why the Already-Expired Certificate Falls Out of Your Alert](#why-the-already-expired-certificate-falls-out-of-your-alert)
*   [Build a Tiered Expiry Report You Can Sort and Export](#build-a-tiered-expiry-report-you-can-sort-and-export)
*   [Turn the Countdown into Tiers](#turn-the-countdown-into-tiers)
*   [Scan the Whole Fleet with Invoke-Command](#scan-the-whole-fleet-with-invoke-command)
*   [Throttle, Envelope Size, and Second-Hop Limits](#throttle-envelope-size-and-second-hop-limits)
*   [Check a Remote TLS Endpoint’s Certificate](#check-a-remote-tls-endpoints-certificate)
*   [Alert on Expiring Certificates and Schedule the Scan](#alert-on-expiring-certificates-and-schedule-the-scan)
*   [Register the Daily Task](#register-the-daily-task)
*   [Send the Alert Through a Webhook](#send-the-alert-through-a-webhook)
*   [Common Errors and Fixes](#common-errors-and-fixes)
*   [The Culture and Date-Parsing Failure](#the-culture-and-date-parsing-failure)
*   [Remote Scanning Fails Before the Command Runs](#remote-scanning-fails-before-the-command-runs)
*   [The Certificate Is Present but the Service Cannot Use It](#the-certificate-is-present-but-the-service-cannot-use-it)
*   [Wrapping Up Your Certificate Expiry Sweep](#wrapping-up-your-certificate-expiry-sweep)

How many certificates are already expired in the stores on your servers right now, and could you answer that without opening Certificate Manager on each machine? If you have to walk the host list to find out, you are carrying a risk you cannot see: a renewal that slipped past its owner, a service account whose client certificate aged out, or an internal root that expired and took a chain of dependent logins with it. A silent expiry surfaces as a failing login or a refused TLS handshake after users are already locked out, almost never as a warning.

This tutorial replaces that manual walk with a PowerShell scan you can run in seconds, then grows it into something a fleet can run unattended. You will pull every expired certificate out of the `Cert:` PSDrive with one command, then narrow the scan to the store locations that carry authentication material. From there you build a tiered report, push it across every server with `Invoke-Command`, check a remote TLS endpoint directly, and schedule the whole thing to alert you before a renewal window closes. Every command uses built-in Windows tooling, so there is nothing to install.

## Find Every Expired Certificate Instantly with One Command

The PowerShell [Certificate provider](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-7.6) mounts the Windows certificate namespace as a drive named `Cert:` and reads it with the same `Get-ChildItem` cmdlet you use on a folder. One command lists everything that has already expired in both the machine and user stores:

```powershell
Get-ChildItem Cert:\LocalMachine, Cert:\CurrentUser -Recurse |
    Where-Object { -not $_.PSIsContainer -and $_.NotAfter -lt (Get-Date) } |
    Select-Object @{ n = 'Store'; e = { $_.PSParentPath -replace '.*::', '' } },
                  Subject, Thumbprint, NotAfter |
    Sort-Object NotAfter
```

The `-Recurse` switch walks every store under both locations. The `Where-Object` filter drops the store containers, which are directories rather than certificates, and keeps each certificate whose `NotAfter` value falls before the current time. Sorting by `NotAfter` puts the oldest failures first.

On Windows PowerShell 3.0 through 5.1 and on PowerShell 7.1 or later, the provider filters faster if you push the date comparison into it:

```powershell
Get-ChildItem Cert:\LocalMachine, Cert:\CurrentUser -Recurse -ExpiringInDays 0 |
    Where-Object { -not $_.PSIsContainer } |
    Select-Object PSParentPath, Subject, Thumbprint, NotAfter
```

A value of `0` returns certificates that have already expired. Keep the pipeline version as your default, because PowerShell 7.0 shipped without the `-ExpiringInDays` parameter and Microsoft restored it in 7.1.

### Four Ways to Read Certificate Stores, and When Each Wins

| Method | Reads | Version support | Reach for it when |
| --- | --- | --- | --- |
| `Get-ChildItem Cert:` with date math | Both locations, every store | Windows PowerShell 2.0 and later, PowerShell 7 | You want one portable command that always works |
| `Get-ChildItem Cert: -ExpiringInDays` | Both locations, every store | PowerShell 3.0 through 5.1, PowerShell 7.1 and later | You want the provider to filter and know your build |
| .NET `X509Store` class | One store per object | Any host with .NET | You are already inside a .NET or C# tool |
| `certutil -store` | One named store | Any Windows build | You need a quick look from a batch file |

Microsoft steers scripted work away from the last row. The [`certutil` documentation](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil) says the tool “isn’t recommended to be used in any production code,” and points to PowerShell commands when you need to match a specific certificate type.

Expired certificates are the emergency. The next section covers which stores hold authentication material, so you can scope the scan.

## The Cert: PSDrive and the Store Locations That Matter

The `Cert:` drive has two levels. The top level is a store location, and it is either `LocalMachine` or `CurrentUser`. The level below is a store, such as `My` (Personal), `Root` (Trusted Root Certification Authorities), `CA` (Intermediate Certification Authorities), or `WebHosting` for IIS. Microsoft documents these in [System store locations](https://learn.microsoft.com/en-us/windows/win32/seccrypto/system-store-locations) and draws the distinction that matters: machine certificates are global to every user and live under `HKEY_LOCAL_MACHINE`, while user certificates belong to one account and live under `HKEY_CURRENT_USER`.

The two locations are separate namespaces, so a scan that reads only `Cert:\LocalMachine` misses the client and smart-card certificates in `Cert:\CurrentUser\My`. There is one wrinkle before you deduplicate results. Microsoft’s [Local machine and current user certificate stores](https://learn.microsoft.com/en-us/windows-hardware/drivers/install/local-machine-and-current-user-certificate-stores) page states: “All current user certificate stores, except the Current User -> Personal store, inherit the contents of the local machine certificate stores.” A root in `Cert:\LocalMachine\Root` also appears in `Cert:\CurrentUser\Root`, so a naive count double-counts roots and intermediates.

### LocalMachine or CurrentUser: Where a Service Looks

IIS, HTTP.sys, SQL Server, and most services running under a service account read `LocalMachine`. A server certificate imported into `Cert:\CurrentUser\My` is invisible to IIS, and the symptom is an HTTPS binding that refuses to start even though the certificate is present in Certificate Manager. Check the machine Personal store first when a service cannot find its identity.

* * *

_**Key Insight: A certificate in _**`CurrentUser\My`**_ is invisible to a service that reads _**`LocalMachine`**_. Audit both locations, or your scan comes back clean on a machine whose HTTPS binding is still broken.**_

* * *

Scoping settles which stores to read. The next decision is how you define “expiring soon,” and this is where most monitoring scripts drop the worst case.

## Filter on NotAfter and NotBefore with Get-Date.AddDays

`NotAfter` and `NotBefore` define a certificate’s validity window, and both surface as `[datetime]` properties on each object, so you compare them directly instead of formatting them into strings. [`Get-Date`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-date) returns the current time, and its `AddDays` method produces a cutoff for any warning window:

```powershell
$cutoff = (Get-Date).AddDays(30)
Get-ChildItem Cert:\LocalMachine\My |
    Where-Object { $_.NotAfter -lt $cutoff } |
    Select-Object Subject, Thumbprint, NotAfter, NotBefore
```

The comparison `$_.NotAfter -lt $cutoff` matches two groups at once: certificates that expire inside the next 30 days and certificates that have already expired, because an expired certificate’s `NotAfter` is in the past.

### Why the Already-Expired Certificate Falls Out of Your Alert

The forward-only test in most examples deletes the worst case:

```powershell
$daysLeft = ($_.NotAfter - (Get-Date)).Days
if ($daysLeft -gt 0 -and $daysLeft -le 30) { <# alert #> }
```

A certificate that expired yesterday produces a negative `daysLeft`, fails the `-gt 0` check, and drops out of the alert at the exact moment it is causing a live outage. Always include the expired case in the filter, then classify it.

One more trap sits in the time zone. `NotAfter` is displayed in the local time zone, while the X.509 value is stored in UTC. Convert the certificate with `.ToUniversalTime()` but leave `Get-Date` in local time, and you shift the boundary by your UTC offset, creating a few hours a day where a certificate flips between states. Compare both in local time, or convert both.

* * *

_**Warning: A forward-only window that checks _**`daysLeft -gt 0`**_ drops certificates that have already expired. Your alert goes silent exactly when the outage starts.**_

* * *

The date math gives you a boolean. The next section turns it into a report with tiers you can act on.

## Build a Tiered Expiry Report You Can Sort and Export

Raw output tells you a certificate is expiring but says nothing about how urgently to act. Wrap the filter in a function that stamps each certificate with a status and a whole-day countdown:

```powershell
function Get-CertificateExpiryReport {
    [CmdletBinding()]
    param(
        [string[]]$StorePath = @(
            'Cert:\LocalMachine\My',
            'Cert:\LocalMachine\WebHosting',
            'Cert:\LocalMachine\Root',
            'Cert:\LocalMachine\CA',
            'Cert:\CurrentUser\My'
        ),
        [int]$WarningDays = 30,
        [int]$CriticalDays = 7
    )

    $now = Get-Date
    foreach ($store in $StorePath) {
        if (-not (Test-Path -Path $store)) { continue }
        Get-ChildItem -Path $store | ForEach-Object {
            $daysLeft = [math]::Floor(($_.NotAfter - $now).TotalDays)
            if ($daysLeft -le $WarningDays) {
                $status = if ($daysLeft -lt 0) { 'Expired' }
                          elseif ($daysLeft -le $CriticalDays) { 'Critical' }
                          else { 'Warning' }
                [pscustomobject]@{
                    Status        = $status
                    DaysLeft      = $daysLeft
                    NotAfter      = $_.NotAfter
                    NotBefore     = $_.NotBefore
                    Subject       = $_.Subject
                    Thumbprint    = $_.Thumbprint
                    HasPrivateKey = $_.HasPrivateKey
                    Store         = $store
                }
            }
        }
    }
}
```

### Turn the Countdown into Tiers

The `$daysLeft -lt 0` branch catches expired certificates first, so nothing falls through the gap. The tiers map to a policy you can defend in a change review:

| Tier | Days left | What it means |
| --- | --- | --- |
| `Expired` | Less than 0 | Authentication is failing now; treat it as an incident |
| `Critical` | 0 to 7 | The renewal window is closing; act this week |
| `Warning` | 8 to 30 | Schedule the renewal before it reaches Critical |
| (not reported) | More than 30 | Outside the warning window |

Run the function, sort the output, and write it to disk:

```powershell
New-Item -ItemType Directory -Path C:\Reports -Force | Out-Null
$report = Get-CertificateExpiryReport -WarningDays 30 -CriticalDays 7
$report | Sort-Object DaysLeft | Format-Table Status, DaysLeft, NotAfter, Subject, Store -AutoSize
$report | Sort-Object DaysLeft | Export-Csv -Path C:\Reports\certificate-expiry.csv -NoTypeInformation
```

Every object it emits is built from the [`X509Certificate2`](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2) class that backs each certificate, so you can pipe the result to [`Export-Csv`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/export-csv), `ConvertTo-Json`, or an HTML report. `NotAfter` and `NotBefore` stay `[datetime]` values, which is what lets `Sort-Object DaysLeft` do the ordering for you.

![Certificate expiry report](https://adamtheautomator.com/wp-content/uploads/publisher/35ace756343245c15afb8ef752e0545fe0868a46e165eac37df6cf404791fa97.jpg)

A single machine’s report is useful. A fleet inventory of seven-day failures is what prevents an outage, and that means running the scan everywhere at once.

## Scan the Whole Fleet with Invoke-Command

[`Invoke-Command`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-7.6) runs a script block on each target, so the `Cert:` path resolves against that machine’s stores instead of the caller’s:

```powershell
$servers = Get-Content -Path C:\Inventory\servers.txt
$cutoff  = (Get-Date).AddDays(30)

$expiring = Invoke-Command -ComputerName $servers -ThrottleLimit 16 -ErrorAction SilentlyContinue -ScriptBlock {
    $cutoff = $using:cutoff
    $stores = 'Cert:\LocalMachine\My','Cert:\LocalMachine\WebHosting','Cert:\LocalMachine\Root'
    foreach ($store in $stores) {
        if (-not (Test-Path -Path $store)) { continue }
        Get-ChildItem -Path $store | Where-Object { $_.NotAfter -lt $cutoff } | ForEach-Object {
            [pscustomobject]@{
                Status     = if ($_.NotAfter -lt (Get-Date)) { 'Expired' } else { 'Expiring' }
                DaysLeft   = [math]::Floor(($_.NotAfter - (Get-Date)).TotalDays)
                NotAfter   = $_.NotAfter
                Subject    = $_.Subject
                Thumbprint = $_.Thumbprint
                Store      = $store
            }
        }
    }
}

$expiring | Select-Object PSComputerName, Status, DaysLeft, NotAfter, Subject, Store |
    Sort-Object DaysLeft | Format-Table -AutoSize
```

The `$using:` scope modifier passes the local `$cutoff` value into each remote session, because a script block running on another computer cannot see your local variables. `-ThrottleLimit` caps how many targets run at once, and `-ErrorAction SilentlyContinue` lets one unreachable server fail without aborting the sweep. Filter the merged output on `PSComputerName`, which `Invoke-Command` attaches to every returned object.

### Throttle, Envelope Size, and Second-Hop Limits

*   Throttle limit: `-ThrottleLimit` caps concurrent connections. Set it explicitly, because a 500-server sweep at full concurrency can saturate the management network.
    
*   Envelope size: WinRM caps the SOAP payload of a single response, and a recursive scan can exceed the default. Raise it on the collector with `Set-Item -Path WSMan:\localhost\MaxEnvelopeSizekb -Value 4096`, the setting documented in the [WinRM configuration reference](https://learn.microsoft.com/en-us/windows/win32/winrm/installation-and-configuration-for-windows-remote-management).
    
*   Delegation: a plain `Invoke-Command` session does not delegate credentials to a further hop. Reading stores is a local operation and works fine, but extending the script to delete keys through `Remove-Item -DeleteKey` needs delegated credentials. The [remote requirements](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_requirements?view=powershell-7.6) page lists the prerequisite checks and the errors that follow.
    

Enable remoting on any target that has it off with `Enable-PSRemoting -Force` from an elevated prompt, and confirm the caller is a member of Administrators or Remote Management Users on that host.

* * *

_**Pro Tip: Raise _**`MaxEnvelopeSizekb`**_ on the collector before a large fleet scan. A recursive sweep that overflows the WinRM envelope fails with a transport error that looks like a network problem.**_

* * *

The stores on the servers are only half the picture. Certificates that terminate TLS in front of a service never appear in a local store at all, so the next check reaches out to the endpoint.

## Check a Remote TLS Endpoint’s Certificate

Some certificates you depend on live on someone else’s machine, and no local store scan will ever see them. Read the certificate a TLS endpoint presents without a browser by opening a socket, negotiating the handshake, and reading the `RemoteCertificate` property:

```powershell
function Get-RemoteCertificate {
    param(
        [Parameter(Mandatory)][string]$HostName,
        [int]$Port = 443
    )
    $tcp = [System.Net.Sockets.TcpClient]::new($HostName, $Port)
    $callback = [System.Net.Security.RemoteCertificateValidationCallback] {
        param($sender, $cert, $chain, $sslPolicyErrors) $true
    }
    $ssl  = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, $callback)
    $ssl.AuthenticateAsClient($HostName)
    $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]$ssl.RemoteCertificate
    $result = [pscustomobject]@{
        HostName = $HostName
        Subject  = $cert.Subject
        Issuer   = $cert.Issuer
        NotAfter = $cert.NotAfter
        DaysLeft = [math]::Floor(($cert.NotAfter - (Get-Date)).TotalDays)
    }
    $ssl.Dispose()
    $tcp.Dispose()
    $result
}

Get-RemoteCertificate -HostName 'www.contoso.com'
```

The validation callback returns `$true` unconditionally, which is deliberate: you are reading the certificate for its dates rather than validating trust, so an expired or self-signed certificate should still return its `NotAfter` instead of throwing. The [`SslStream`](https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslstream) documentation confirms that `RemoteCertificate` surfaces the server’s certificate after `AuthenticateAsClient` completes. Feed a list of endpoints into this function, and a remote expiry joins the same report as a local one.

![Local and remote reads](https://adamtheautomator.com/wp-content/uploads/publisher/cb935ca779981b6c3c04dead4895feb0bdf54a728038aaaa7d68f818ffd314e6.jpg)

Reading the certificate is the hard part. Making sure the read happens every day without anyone remembering is the part people skip.

## Alert on Expiring Certificates and Schedule the Scan

A scan that runs only when someone remembers misses the expiries that land between runs. Save the report function as `C:\Scripts\Get-CertificateExpiryReport.ps1`, then register a task that runs it as `SYSTEM` every morning.

### Register the Daily Task

[`Register-ScheduledTask`](https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/register-scheduledtask) builds the task from action, trigger, principal, and settings objects:

```powershell
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Get-CertificateExpiryReport.ps1'
$trigger = New-ScheduledTaskTrigger -Daily -At 6:00AM
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew

Register-ScheduledTask -TaskName 'CertificateExpiryScan' -Action $action -Trigger $trigger `
    -Principal $principal -Settings $settings -Description 'Daily scan for expired and expiring certificates'
```

The **SYSTEM** principal has the access the scan needs without a stored password, and `-StartWhenAvailable` runs the task after a missed schedule instead of skipping the day. Point `-Execute` at `pwsh.exe` if the target has PowerShell 7 installed.

### Send the Alert Through a Webhook

The task writes the CSV. The alert is what gets someone’s attention, and a webhook beats an email nobody reads. Append this to the script so it fires only when something is actually wrong:

```powershell
$report  = Get-CertificateExpiryReport -WarningDays 30 -CriticalDays 7
$expired = @($report | Where-Object { $_.Status -eq 'Expired' })

if ($expired.Count -gt 0) {
    $payload = @{ text = "Expired certificates on $env:COMPUTERNAME: $($expired.Count)" } | ConvertTo-Json
    Invoke-RestMethod -Uri $env:CERT_WEBHOOK_URL -Method Post -ContentType 'application/json' -Body $payload
}
```

`Invoke-RestMethod` posts the same `{ "text": "..." }` payload to [Microsoft Teams incoming webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook) and to Slack incoming webhooks, so one script covers both. Keep the URL in an environment variable so the script file carries no secret.

Confirm the task exists and force one run to prove the wiring:

```powershell
Get-ScheduledTask -TaskName 'CertificateExpiryScan' | Get-ScheduledTaskInfo
Start-ScheduledTask -TaskName 'CertificateExpiryScan'
```

`Get-ScheduledTaskInfo` returns `LastRunTime` and `LastTaskResult`, so a `0x0` result on a manual run is your evidence that the scheduled path works before you trust it unwatched.

## Common Errors and Fixes

The commands are short, but the failures are specific.

### The Culture and Date-Parsing Failure

A format or parse error usually means a certificate date was formatted to a string and parsed back under a non-US culture. Parse any unavoidable string with an explicit culture:

```powershell
[datetime]::Parse('2026-11-30T12:00:00Z', [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal)
```

Comparing `$_.NotAfter` as a `[datetime]` avoids the parser entirely, which is why every example in this post does it that way.

### Remote Scanning Fails Before the Command Runs

When `Invoke-Command` fails across the fleet, the script block is rarely the problem. Work through the transport instead:

*   `Access is denied` means the caller is not in Administrators or Remote Management Users on the target, or the target has no WinRM listener. Run `Enable-PSRemoting -Force` on the target once. [about\_Remote\_Troubleshooting](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_troubleshooting?view=powershell-7.6) maps each error to its fix.
    
*   A workgroup target fails Kerberos authentication. Add the collector to `TrustedHosts` on the target or configure an HTTPS listener, and treat that list as a temporary measure.
    
*   A transport error on a large result set means the response exceeded the envelope cap. Raise `MaxEnvelopeSizekb` on the collector or narrow the remote script.
    
*   `The term '-ExpiringInDays' is not recognized` on PowerShell 7.0 means you hit the one release that dropped the parameter. Switch to the `NotAfter` date math.
    

### The Certificate Is Present but the Service Cannot Use It

A listed certificate can still fail the service that needs it. Confirm the location first, because a service reading `LocalMachine` cannot see a certificate in `CurrentUser\My`. Then confirm the service account can reach the private key, whose permission sits on a file system ACL rather than the certificate. Scheduling the scan under **SYSTEM** gives it consistent access to every store and private key without a stored password.

## Wrapping Up Your Certificate Expiry Sweep

The whole point of this scan is to see an expiry while it is still cheap to fix, and every piece you built serves that one goal. One `Get-ChildItem` against the [Cert: PSDrive](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-7.6) gives you the expired inventory, and the `NotAfter` filter gives you the ones closing in. Scoping to the machine Personal, Root, and CA stores plus `CurrentUser\My` keeps the report honest. The tiered function turns the dates into priority, `Invoke-Command` spreads it across the fleet, `SslStream` covers the endpoints you do not own, and the scheduled task with its webhook runs the whole thing without you.

Two habits keep this useful after the first cleanup. Run the report on a schedule even when the list is empty, because an empty daily report is your evidence that nothing was missed. And when a certificate is renewed, delete the old entry from the store instead of leaving it behind, so the next scan does not raise an alert about a certificate that was replaced months ago.

Share this article

[Share on X](https://twitter.com/intent/tweet?url=https%3A%2F%2Fadamtheautomator.com%2Ffind-expired-certificates-powershell%2F&text=Find%20Expired%20Windows%20Certificates%20Instantly%20via%20PowerShell)[Share on Facebook](https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fadamtheautomator.com%2Ffind-expired-certificates-powershell%2F)[Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fadamtheautomator.com%2Ffind-expired-certificates-powershell%2F)

## Related Posts

![](https://adamtheautomator.com/wp-content/uploads/publisher/3ec5d9c85b2b81c8a7f8e65f33231fa4/606223a2505919db35cb97858e8eafa9a96eb8d297bab790394df6d592d2af99.webp)

### [Sysmon: Detect Hidden Malware in Windows Event Logs](/sysmon-detect-hidden-malware/)

Deploy Sysinternals Sysmon with a tuned config, then use Get-WinEvent and Sysmon event IDs to catch process injection and C2 beaconing.

![](https://adamtheautomator.com/wp-content/uploads/publisher/2e05d9c85b2b81c080b8d45ec1cdfbb3/71dd588b2369d52e1695c95c1493383c6c0ed2adb4aac7eba04c1ab324f3a6a6.webp)

### [Stop Service Principal Sprawl in Entra ID with PowerShell](/stop-service-principal-sprawl-entra-id-powershell/)

Inventory every app registration and service principal, then automate ownership, rotation, and safe disablement with PowerShell and Microsoft Graph.

![](https://adamtheautomator.com/wp-content/uploads/2026/06/featured_image-13.png)

### [Taming AI Tool Sprawl: A PowerShell Guide to Auditing and Governing Unauthorized AI Applications](/taming-ai-tool-sprawl-powershell-guide-auditing/)

Detect and govern unauthorized AI tools with PowerShell, Microsoft Graph, Entra ID, and Defender for Cloud Apps.

## Categories

*   [IT Ops](/category/it-ops/)
*   [Cloud](/category/cloud/)
*   [DevOps](/category/devops/)
*   [Home Ops](/category/home-ops/)
*   [Information Security](/category/infosec/)
*   [Software Development](/category/software-development/)

## Site

*   [Home](/)
*   [Tutorials](/tutorials/)
*   [Instructors](/author/)
*   [Advertising](/advertising/)
*   [Recommended Resources](/resources/)
*   [About Adam](/about-adam/)

Copyright 2026© ATA Learning | [Privacy Policy](/privacy/)
