Skip to main content
Systems Engineering

Find Expiring Certificates on Windows Servers with PowerShell

Report certificates expiring within a chosen window across Windows certificate stores with the PowerShell Certificate provider, sweep a fleet, export a CSV, and clean up expired certificates with a backup and rollback.

Close-up of tower servers in a data center with blue and red lighting.

In this guide

Share

To find expiring certificates on a Windows server with PowerShell, query the Cert: drive with the Certificate provider’s -ExpiringInDays parameter: Get-ChildItem -Path Cert:\LocalMachine\My -ExpiringInDays 30 returns every certificate in the machine’s Personal store that expires in 30 days or fewer, including any that have already expired. A value of 0 returns only certificates that have expired. The key constraint is version: the Certificate provider exists only on Windows, and -ExpiringInDays, -DnsName, -Eku and -SSLServerAuthentication work in Windows PowerShell 5.1 and in PowerShell 7.1 or later, where Microsoft reintroduced them. Below, that one-liner becomes a per-server report, a fleet sweep, a CSV for the change board and an optional, reversible clean-up.

#Context: why certificate expiry still causes outages

Certificates fail on a fixed date that is written into them. The NotAfter property holds that date, and the .NET X509Certificate2 class returns it in local time. Nothing on the server warns you as the date approaches unless something reads that property, so the usual failure is a TLS endpoint or a client authentication flow that stops working on a known day nobody was watching.

Certificate management tools track certificates that were enrolled through them. The gap is the certificate imported by hand or left behind by an installer. A read-only PowerShell report closes that gap because it reads what is actually in each store, not what a register says should be there. Like a TCP retransmission rate diagnostic tool, it collects evidence first and decides second.

Assumptions to check against your estate:

  • The servers run Windows with Windows PowerShell 5.1 or PowerShell 7.1 or later.
  • You have administrative rights on each server for the remoting sweep and for any change to a LocalMachine store.
  • PowerShell remoting (WinRM) is already enabled for the fleet sweep.
  • You test the clean-up on a non-production server before using it anywhere else.

#Architecture: the Cert: drive, stores and filters

The Certificate provider exposes certificates as a hierarchy. The Cert: drive contains two store locations, CurrentUser and LocalMachine. Each location contains physical stores, such as My (Personal), WebHosting, Root and CA, and each store contains certificates identified by their thumbprints. Each certificate is an X509Certificate2 object, so the standard properties are available: Subject, Issuer, Thumbprint, NotAfter and HasPrivateKey.

The provider adds dynamic parameters to Get-ChildItem that filter inside the provider instead of in your pipeline:

Certificate provider filters used in this toolkit task
ParameterWhat it returnsUse in this task
-ExpiringInDays <int>Certificates expiring in or before that many days; 0 returns expired certificatesThe report window and the clean-up candidate list
-SSLServerAuthenticationCertificates with Server Authentication in their enhanced key usage listNarrowing the report to TLS server certificates
-DnsName <pattern>Certificates whose DNS name list matches the pattern; wildcards allowedFinding every copy of one site’s certificate
-Eku <pattern>Certificates whose enhanced key usage list matches the text or patternSeparating client and server authentication certificates

Note that EKU filters also return certificates whose EKU list is empty, because such a certificate can be used for all purposes, and that DnsNameList comes from the Subject Alternative Name extension, or from the Subject when that extension is empty.

The task has three layers: a collector that reads a fixed list of stores and flattens the result, a fleet wrapper that runs it through Invoke-Command, and a CSV export. Only the optional clean-up changes anything, and it is kept separate so the report can run on a schedule without risk of change.

#Implementation: report, sweep and export

Start with one server. The collector reads the machine’s Personal and Web Hosting stores, which is where server authentication certificates are normally kept, and adds the computer name, the store path and an Expired flag, because a 30-day window also returns certificates that have already expired:

1$Days   = 30
2$Stores = 'Cert:\LocalMachine\My', 'Cert:\LocalMachine\WebHosting'
3$Now    = Get-Date
4$Report = foreach ($Store in $Stores) {
5    Get-ChildItem -Path $Store -ExpiringInDays $Days |
6        Select-Object @{ Name = 'Computer'; Expression = { $env:COMPUTERNAME } },
7                      @{ Name = 'Store';    Expression = { $Store } },
8                      Subject, Issuer, Thumbprint, NotAfter, HasPrivateKey,
9                      @{ Name = 'Expired';  Expression = { $_.NotAfter -lt $Now } }
10}
11$Report

No output means no certificate in those stores expires within the window. If the server has no WebHosting store, that path returns an error for that store only; remove it from $Stores on that server.

For a wider view, the provider documentation shows a recursive search of every store. It also returns certificates in trust stores such as Root and CA, which are maintained differently from the certificates your services present, so keep that output as a separate list:

1Get-ChildItem -Path Cert:\* -Recurse -ExpiringInDays 30 |
2    Select-Object Subject, Thumbprint, NotAfter

To sweep a fleet, run the collector through remoting. Invoke-Command adds a PSComputerName property to every returned object and, unless you set -ThrottleLimit, runs against at most 32 computers at a time:

1$Servers = 'Srv01', 'Srv02'
2$Fleet = Invoke-Command -ComputerName $Servers -ScriptBlock {
3    $Now = Get-Date
4    foreach ($Store in 'Cert:\LocalMachine\My', 'Cert:\LocalMachine\WebHosting') {
5        Get-ChildItem -Path $Store -ExpiringInDays 30 |
6            Select-Object @{ Name = 'Store'; Expression = { $Store } },
7                          Subject, Issuer, Thumbprint, NotAfter, HasPrivateKey,
8                          @{ Name = 'Expired'; Expression = { $_.NotAfter -lt $Now } }
9    }
10}
11$Fleet |
12    Select-Object PSComputerName, Store, Subject, Issuer, Thumbprint, NotAfter, HasPrivateKey, Expired |
13    Export-Csv -Path .\cert-expiry.csv -NoTypeInformation

In PowerShell 6 and later, -NoTypeInformation is not required because Export-Csv no longer writes the #TYPE header; it is kept here so the same script produces the same file in Windows PowerShell 5.1. In PowerShell 7 the default encoding for the CSV file is UTF-8 without a byte order mark. The CSV is the change board’s evidence: one row per certificate, with the server and store that hold it.

If you run the report on PowerShell 7, record the version in your toolkit documentation and keep an eye on its support date; KBY tracks the PowerShell 7.6 LTS end-of-support deadline for teams standardised on that release.

Close-up shot of a hand marking a date on a calendar with a pen, emphasizing planning and scheduling.
Photo by Towfiqu barbhuiya on Pexels

#
Optional clean-up of expired certificates

This task does not renew anything; that belongs to the service owner. Removing an expired certificate is optional and reduces noise in later reports. Treat it as a change: one certificate at a time, backed up first. Never add -DeleteKey to a clean-up. Without it, Remove-Item deletes the certificate but leaves its private key in place; with it, the private key is deleted too, and that cannot be undone unless the key was exportable and backed up.

  1. Precondition. Confirm the certificate is expired and that its owner agrees it is no longer needed. Set the thumbprint and store from the report; the certificate must appear in that store’s expired list:
    1$Thumb = '<thumbprint>'
    2Get-ChildItem -Path Cert:\LocalMachine\My -ExpiringInDays 0
    3$Cert  = Get-Item -Path "Cert:\LocalMachine\My\$Thumb"

    Replace My with WebHosting in every step if that is where the certificate lives.

  2. Back up a certificate without a private key (HasPrivateKey is False) as a DER-encoded file:
    Export-Certificate -Cert $Cert -FilePath "D:\CertBackup\$Thumb.cer" -NoClobber

    D:\CertBackup stands for an existing folder that only administrators can read.

  3. Back up a certificate with a private key as a password-protected PFX, encrypted with AES-256 rather than the TripleDES default. By default the whole chain is exported, and a PFX restore imports every certificate in the file into the target store, so export the end-entity certificate only:
    1$Pfx = Get-Credential -UserName 'PFX password' -Message 'Enter a PFX password'
    2Export-PfxCertificate -Cert $Cert -FilePath "D:\CertBackup\$Thumb.pfx" `
    3    -Password $Pfx.Password -CryptoAlgorithmOption AES256_SHA256 `
    4    -ChainOption EndEntityCertOnly -NoClobber

    If the key is not exportable, the export fails. Stop and leave the certificate in place; there is no complete backup to roll back to.

  4. Preview the removal: Remove-Item -Path "Cert:\LocalMachine\My\$Thumb" -WhatIf. The output names the single certificate path that would be removed.
  5. Remove it: Remove-Item -Path "Cert:\LocalMachine\My\$Thumb", then record the thumbprint, store, backup path and time in the change record.

Keep the clean-up away from trust stores such as Root and CA. Trust changes belong with whoever owns your trust policy.

#Validation: proving the report and the change

Validate the report before anyone relies on it. On a test server, create a test certificate whose expiry falls inside the window and confirm the report lists it:

1New-SelfSignedCertificate -DnsName 'expiry-test.contoso.com' `
2    -CertStoreLocation 'Cert:\LocalMachine\My' -NotAfter (Get-Date).AddDays(10)

Afterwards, remove it with Remove-Item and -DeleteKey: its key exists only for the test, which is the one place in this task where deleting a key is right. Then compare the report with what the Certificates MMC snap-in shows; Invoke-Item on a certificate path opens that snap-in on the certificate itself.

For the fleet sweep, compare the distinct PSComputerName values in the CSV with $Servers, allowing for servers with nothing expiring. A server missing because of a remoting error is not the same as a server with nothing to report, so check the errors Invoke-Command wrote.

After a clean-up, three checks define success:

  • Get-Item -Path "Cert:\LocalMachine\My\$Thumb" now returns an error because the path no longer exists.
  • The backup file exists. For a PFX, Get-PfxData -FilePath "D:\CertBackup\$Thumb.pfx" -Password $Pfx.Password opens it without error; for a .cer, the file has a non-zero size.
  • The service that used the store still starts and serves requests, confirmed by its owner.

#Failure modes and what they look like

  • PowerShell does not recognise -ExpiringInDays. Either the session runs a version that does not provide it (it is documented for Windows PowerShell 5.1 and PowerShell 7.1 and later), or no Cert: path was given, and dynamic parameters exist only in the provider’s drive. Check the version and pass a Cert: path.
  • The report is empty but you know a certificate is expiring. It sits in a store the collector does not read, such as a CurrentUser store or a custom LocalMachine store. Run the recursive sweep once to find where it lives, then add that store to $Stores.
  • A filtered report includes unrelated certificates. EKU filters also return certificates with an empty EKU list. Inspect EnhancedKeyUsageList before acting on the result.
  • Servers are missing from the CSV. Remoting failed for those computers. Rerun against the missing names and read the error records rather than treating silence as a clean result.
  • Export-PfxCertificate fails. The private key is not exportable. Leave the certificate where it is and record why it was not cleaned up.
  • A service fails after a clean-up. Something still referenced the removed certificate by thumbprint. Restore it from backup as described in the recovery section, then ask the service owner to move to its renewed certificate.
Overhead view of financial charts, laptop, and magnifying glass, ideal for business analysis themes.
Photo by Leeloo The First on Pexels

#Security: least privilege and the backup files

The report is read-only, but its output names every certificate in your estate. Store the CSV where only the operations team can read it. The report exports no private key material.

The PFX backups are the sensitive part. Each holds a private key protected only by its password, so keep them on restricted storage, away from the password, and delete them when retention ends. The PFX default encryption is TripleDES with SHA1; choosing AES256_SHA256 is the stronger documented option. If your domain supports it, -ProtectTo can protect a PFX to named accounts instead of a shared password; Microsoft documents that this requires a Windows Server 2012 or later domain controller.

Do not enable CredSSP for this task. Microsoft documents that delegation for deleting private keys on remote computers and asks you to weigh its risks first; this workflow never deletes a production key, so it never needs it.

#Recovery: restoring a removed certificate

A removal is reversed by importing the backup into the same store. Confirm the store path and thumbprint from the change record first.

  1. For a certificate without a private key: Import-Certificate -FilePath "D:\CertBackup\$Thumb.cer" -CertStoreLocation Cert:\LocalMachine\My.
  2. For a certificate with a private key: Import-PfxCertificate -FilePath "D:\CertBackup\$Thumb.pfx" -CertStoreLocation Cert:\LocalMachine\My -Password $Pfx.Password. Without -Exportable the restored key cannot be exported again; add it only if the original key was exportable and that is still wanted.
  3. Confirm the restore with Get-Item -Path "Cert:\LocalMachine\My\$Thumb" and check that HasPrivateKey matches the value in the original report.
  4. Ask the service owner to confirm the service works again, then plan the move to a renewed certificate.

Both import cmdlets support -WhatIf for a preview. The backup-first rule applies to any certificate change, just as a DNSSEC chain-of-trust validator checks each link before trusting the next.

#Running the certificate expiry report as a routine check

Once validated, run the report on a schedule and treat its output as a queue. Rows with Expired set to True need an owner decision this week; rows inside the window need a named owner and a renewal date.

  • Keep the window wider than your longest renewal lead time, such as 60 days where a change freeze slows renewals.
  • Compare each run with the previous CSV by thumbprint, so a certificate that disappears without a change record is noticed.
  • Keep the clean-up manual and one certificate at a time; never schedule removals.
  • Before any removal, the boundary is the same every time: an expired certificate, an owner’s agreement, a backup that opens, and no -DeleteKey.

The next safe decision after the first full run is not deletion but assigning an owner to every row, because an unowned certificate is the one most likely to expire unnoticed.

About this page

Credited to

Dates

Published

Sources

This page lists no external sources.

Corrections

No corrections are listed for this page. Some review sweeps corrected pages without naming each one; those are in the corrections log.

Reader Interaction

Comments

Add a thoughtful note on Find Expiring Certificates on Windows Servers with PowerShell. Comments are checked for spam and held for moderation before appearing.

Loading comments...

Discover more

Learn More About KBY

Was this useful?

Reports go to the site owner and are used to correct pages. Corrections are listed at /corrections.

Engineering insights, direct to you.

Receive the latest Systems Engineering tutorials, production guides, Engineering Labs and operational best practices.