Most Windows certificate guides are either too shallow to help in production or so obsessed with PKI theory that they ignore what I actually do at 2 a.m. when a binding is broken, a PFX will not import, or I need to find every certificate expiring in the next month. This is the practical side of PowerShell certificate work: listing stores, filtering on expiry, importing and exporting the right formats, checking remote servers, and fixing IIS after renewal.
I still use the GUI sometimes. Certificate Manager and IIS Manager are fine for one-off checks. But once I need repeatability, auditing, or a quick answer across multiple servers, I reach for PowerShell. It is faster, easier to script, and a lot less error-prone than clicking through five MMC panes and hoping I picked the right store.
If you already understand certificate basics, good. If not, it helps to know the difference between PEM, CER, and PFX before you start moving files around. The certificate formats guide and the more specific PKCS#12/PFX troubleshooting article cover that background well. This article stays focused on the commands.
Start with the certificate provider
The PowerShell certificate provider exposes Windows certificate stores as a drive. That is the part many Linux-first admins miss the first time they land on a Windows box.
To list the top-level stores:
Get-ChildItem -Path Cert:\
On a normal server I care most about these two roots:
Cert:\LocalMachinefor server certificates, service identities, and machine-wide trustCert:\CurrentUserfor certificates tied to the account running the current session
I usually inspect both because people import into the wrong place more often than they admit.
Get-ChildItem -Path Cert:\LocalMachine
Get-ChildItem -Path Cert:\CurrentUser
The store I check most often for web server work is LocalMachine\My, which is the machine personal store:
Get-ChildItem -Path Cert:\LocalMachine\My
If I am dealing with trust problems, I also check the CA and Root stores:
Get-ChildItem -Path Cert:\LocalMachine\CA
Get-ChildItem -Path Cert:\LocalMachine\Root
LocalMachine vs CurrentUser: the mistake I see most
When IIS, HTTP.sys, SQL Server, or another service cannot find a certificate, the problem is often not the certificate itself. It is in the wrong store.
- Use
LocalMachinefor services that run outside your interactive session. - Use
CurrentUserfor user-specific certificates, smart cards, user authentication, and scripts running under that identity.
If you import a web server certificate into CurrentUser\My, IIS will not magically use it. It needs to live under Cert:\LocalMachine\My.
This is also why I prefer to script imports explicitly instead of double-clicking a file in Explorer. The GUI makes it easy to click through the wizard and land in the wrong store.
Listing certificates with useful output
The raw default table is okay, but I nearly always select the fields I care about:
Get-ChildItem -Path Cert:\LocalMachine\My |
Select-Object Subject, FriendlyName, Thumbprint, NotBefore, NotAfter, HasPrivateKey
For more readable output on a busy machine:
Get-ChildItem -Path Cert:\LocalMachine\My |
Sort-Object NotAfter |
Format-Table Subject, Thumbprint, NotAfter, HasPrivateKey -AutoSize
I sort by NotAfter because expiring certificates are usually the reason I am looking at the store in the first place.
Filter certificates by expiry date
This is one of the commands I use constantly:
$cutoff = (Get-Date).AddDays(30)
Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.NotAfter -lt $cutoff } |
Sort-Object NotAfter |
Select-Object Subject, Thumbprint, NotAfter
That shows any certificate expiring in the next 30 days. If I want to search every major machine store instead of only My, I expand the search:
$cutoff = (Get-Date).AddDays(30)
Get-ChildItem -Path Cert:\LocalMachine -Recurse |
Where-Object {
$_.PSIsContainer -eq $false -and
$_.NotAfter -and
$_.NotAfter -lt $cutoff
} |
Select-Object PSParentPath, Subject, Thumbprint, NotAfter |
Sort-Object NotAfter
The PSIsContainer check matters. Without it, recursive searches through Cert:\ get noisy.
Find by subject or SAN hint
Subject matching is useful, but it is also messy because names vary. I usually do a wildcard search and then confirm with the thumbprint:
Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Subject -like '*example.com*' } |
Select-Object Subject, Thumbprint, NotAfter
If I need to inspect SANs, I expand extensions:
Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Subject -like '*example.com*' } |
ForEach-Object {
[PSCustomObject]@{
Subject = $_.Subject
Thumbprint = $_.Thumbprint
NotAfter = $_.NotAfter
SAN = ($_.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' }).Format($true)
}
}
That is not pretty, but it works when I need a quick answer without opening the certificate viewer.
Thumbprint lookup when I need certainty
Thumbprint lookup is the safest way to work with a certificate in scripts because the subject can change, contain spaces, or match more than one cert.
$thumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Thumbprint -eq $thumbprint }
I also use this pattern when rebinding IIS sites or exporting a specific certificate:
$thumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
$cert = Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Thumbprint -eq $thumbprint }
if (-not $cert) {
throw 'Certificate not found in LocalMachine\\My'
}
$cert | Select-Object Subject, Thumbprint, NotAfter, HasPrivateKey
One practical note: Windows sometimes displays thumbprints with hidden spaces when copied from the GUI. If I paste one from MMC and the script cannot find it, I strip whitespace first.
$thumbprint = '01 23 45 67 89 AB CD EF 01 23 45 67 89 AB CD EF 01 23 45 67' -replace '\s',''
Importing a PFX into the right store
For server work, importing PFX files is routine. The main points are:
- use
Import-PfxCertificate - choose the correct store
- protect the password as a
SecureString - confirm the imported certificate has a private key
$pfxPath = 'C:\Certificates\example-com.pfx'
$pfxPassword = ConvertTo-SecureString 'ReplaceWithRealPassword' -AsPlainText -Force
$cert = Import-PfxCertificate -FilePath $pfxPath -Password $pfxPassword -CertStoreLocation 'Cert:\LocalMachine\My' -Exportable
$cert | Select-Object Subject, Thumbprint, HasPrivateKey, NotAfter
I only use -Exportable when I actually need it. On production servers, making private keys exportable is convenient for admins and attackers alike. If I do not need to move that key again, I leave the key non-exportable.
Common import failures
When a PFX import fails, I usually check these in order:
- wrong password
- imported to
CurrentUserinstead ofLocalMachine - insufficient permissions
- corrupt or incomplete PFX
- key provider or algorithm mismatch on older Windows builds
To confirm the imported certificate really includes a private key:
Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Thumbprint -eq $cert.Thumbprint } |
Select-Object Subject, Thumbprint, HasPrivateKey, EnhancedKeyUsageList
If HasPrivateKey is False, that is not an IIS-ready server certificate import.
For broader Windows certificate deployment work, the IIS installation guide is a good companion to this article because it covers the GUI and site-level pieces around the import.
Exporting certificates: CER vs PFX
I split exports into two categories:
- public certificate only: use
Export-Certificate - certificate plus private key: use
Export-PfxCertificate
Export public certificate to CER
$thumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
$cert = Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Thumbprint -eq $thumbprint }
Export-Certificate -Cert $cert -FilePath 'C:\Certificates\example-com.cer'
That exports the public certificate only. No private key. It is safe for sharing with clients, load balancers, or trust stores when needed.
Export to PFX with private key
$thumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
$cert = Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Thumbprint -eq $thumbprint }
$pfxPassword = ConvertTo-SecureString 'ReplaceWithRealPassword' -AsPlainText -Force
Export-PfxCertificate -Cert $cert -FilePath 'C:\Certificates\example-com-export.pfx' -Password $pfxPassword
If the private key was marked non-exportable during import or enrollment, export will fail. That is expected behavior, not PowerShell being difficult.
Security rule I follow with exported PFX files
If I export a PFX, I treat it like a credential dump. I do not leave it on desktop folders, temp directories, file shares, or random admin boxes. I move it where it needs to go, import it, and delete the copy securely according to the environment policy.
Converting formats: PowerShell vs OpenSSL
PowerShell handles Windows store operations very well. It does not replace OpenSSL for every format conversion task.
What PowerShell is good at:
- importing into Windows stores
- exporting CER and PFX
- enrolling from AD CS
- automating IIS binding tasks
What OpenSSL is still better at:
- PEM/DER conversions
- splitting PFX into separate key and certificate files
- inspecting chains and detailed extensions across platforms
- cross-platform workflows
If I need a .cer from the Windows store, PowerShell is perfect:
Export-Certificate -Cert $cert -FilePath 'C:\Certificates\example-com.cer'
If I need to extract PEM files from a PFX for Nginx or HAProxy, I use OpenSSL instead:
openssl pkcs12 -in C:\Certificates\example-com.pfx -clcerts -nokeys -out C:\Certificates\example-com.crt
openssl pkcs12 -in C:\Certificates\example-com.pfx -nocerts -nodes -out C:\Certificates\example-com.key
That is one of those places where forcing everything through native Windows tooling makes life harder than it needs to be. I keep both options available. The OpenSSL conversion guide is what I hand to teammates who keep getting lost between PEM, CRT, and PFX.
Requesting a certificate from an internal CA
In Active Directory environments, Get-Certificate is useful for requesting certificates from an internal Microsoft CA without touching the GUI.
A basic example:
Get-Certificate -Template 'WebServer' -DnsName 'app01.example.internal','app01' -CertStoreLocation 'Cert:\LocalMachine\My' -Url 'ldap:'
In some environments I specify the enrollment policy server or CA config explicitly, but the defaults often work if auto-enrollment is set up correctly.
Things I verify before blaming the command:
- the template exists and is published
- the server account or user has enroll permission on the template
- the CA is reachable
- the template EKUs match the intended use
- the subject/SAN rules match template policy
A lot of internal CA problems are permissions or template design issues, not PowerShell issues.
Creating self-signed certificates that are actually usable
New-SelfSignedCertificate is fine for labs, internal tools, temporary testing, and certain mTLS cases. It is not a replacement for a publicly trusted CA on internet-facing sites.
A practical example for local IIS or internal API testing:
New-SelfSignedCertificate -DnsName 'app01.example.internal','app01' -CertStoreLocation 'Cert:\LocalMachine\My' -KeyUsage DigitalSignature,KeyEncipherment -KeyAlgorithm RSA -KeyLength 2048 -NotAfter (Get-Date).AddYears(2) -FriendlyName 'Internal App01 TLS'
The parameters that matter most in real use are:
DnsName: sets subject alternative names people usually forgetCertStoreLocation: decides whether services can see itKeyUsage: should match server-auth useNotAfter: keeps you from creating a cert that expires sooner than intended
If I need to include enhanced key usage explicitly, I use -TextExtension:
New-SelfSignedCertificate -DnsName 'api.example.internal' -CertStoreLocation 'Cert:\LocalMachine\My' -KeyUsage DigitalSignature,KeyEncipherment -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.1') -NotAfter (Get-Date).AddMonths(12)
That OID is Server Authentication. It avoids some awkward interoperability issues with software that expects EKUs to be present.
Deprecated or weak practices I avoid
I still see old examples online that use short key sizes or treat CN-only certificates as acceptable. I avoid both.
- Do not rely on CN alone; use SANs through
DnsName. - Do not create new 1024-bit RSA certificates.
- Do not hand out self-signed certificates for public production sites.
- Do not forget trust distribution; a self-signed cert is useless if clients do not trust it.
If your need is only local testing, it is fine. If this is a production service, use a real PKI.
Checking a remote server certificate with PowerShell
When I need to inspect the certificate presented by a remote host, I prefer SslStream today. Still, a lot of Windows admins know the older callback trick using [Net.ServicePointManager]::ServerCertificateValidationCallback, and it can be handy in a pinch.
Here is the old-style approach:
$target = 'https://example.com/'
$script:RemoteCert = $null
$oldCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
param($sender, $certificate, $chain, $sslPolicyErrors)
$script:RemoteCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certificate
return $true
}
try {
Invoke-WebRequest -Uri $target -UseBasicParsing | Out-Null
$script:RemoteCert | Select-Object Subject, Issuer, Thumbprint, NotBefore, NotAfter
}
finally {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $oldCallback
}
It works, but it changes a global callback in the current process. I do not like leaving that around in bigger scripts.
A cleaner current method is TcpClient plus SslStream:
$tcp = [System.Net.Sockets.TcpClient]::new('example.com', 443)
try {
$ssl = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, ({ $true }))
$ssl.AuthenticateAsClient('example.com')
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($ssl.RemoteCertificate)
$cert | Select-Object Subject, Issuer, Thumbprint, NotAfter
}
finally {
$tcp.Close()
}
I use this when I want to confirm what a load balancer, proxy, or CDN edge is actually serving, not what I think it should be serving.
Pair the certificate check with connectivity testing
A TLS problem is not always a certificate problem. Sometimes the host is down, a firewall is in the way, or a listener is missing. I usually test TCP reachability first:
Test-NetConnection -ComputerName example.com -Port 443
For IIS troubleshooting I often run both checks back to back: first Test-NetConnection, then the certificate query.
Script to find expiring certificates across the store
This is the kind of report I actually schedule on Windows servers:
param(
[int]$Days = 30
)
$cutoff = (Get-Date).AddDays($Days)
$stores = @(
'Cert:\LocalMachine\My',
'Cert:\LocalMachine\WebHosting',
'Cert:\CurrentUser\My'
)
$results = foreach ($store in $stores) {
if (Test-Path $store) {
Get-ChildItem -Path $store | Where-Object {
$_.NotAfter -lt $cutoff
} | ForEach-Object {
[PSCustomObject]@{
Store = $store
Subject = $_.Subject
Thumbprint = $_.Thumbprint
NotAfter = $_.NotAfter
DaysRemaining = [math]::Floor(($_.NotAfter - (Get-Date)).TotalDays)
HasPrivateKey = $_.HasPrivateKey
}
}
}
}
$results |
Sort-Object NotAfter |
Format-Table Store, Subject, Thumbprint, NotAfter, DaysRemaining, HasPrivateKey -AutoSize
A few lessons from production:
- Include
WebHostingif the server uses it. Some environments do. - Keep the output simple enough to email or log.
- Sort by expiry date, not subject.
- Negative
DaysRemainingvalues are helpful because they show already-expired certs immediately.
If you want machine-readable output for monitoring or inventory:
$results | ConvertTo-Json -Depth 3
Certificate expiry monitoring should not depend on a human remembering to run a command. I still use ad hoc checks, but for real coverage I pair scripts like this with centralized monitoring. The certificate expiry alerting guide and the companion Zabbix certificate monitoring walkthrough are worth using once you move beyond single-server checks.
Renewing IIS certificate bindings with PowerShell
This is where people often renew the certificate successfully and still leave the site serving the old one.
First, import the renewed certificate and confirm the new thumbprint:
Get-ChildItem -Path Cert:\LocalMachine\My |
Where-Object { $_.Subject -like '*example.com*' } |
Sort-Object NotAfter |
Select-Object Subject, Thumbprint, NotAfter
Then use the WebAdministration module to bind the new certificate to the site.
Import-Module WebAdministration
$siteName = 'Default Web Site'
$thumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
$binding = Get-WebBinding -Name $siteName -Protocol 'https'
$binding.AddSslCertificate($thumbprint, 'My')
If the site has multiple HTTPS bindings, filter by binding information instead of taking the first one blindly:
Import-Module WebAdministration
$siteName = 'ExampleSite'
$bindingInformation = '*:443:example.com'
$thumbprint = '0123456789ABCDEF0123456789ABCDEF01234567'
$binding = Get-WebBinding -Name $siteName -Protocol 'https' |
Where-Object { $_.bindingInformation -eq $bindingInformation }
$binding.AddSslCertificate($thumbprint, 'My')
For lower-level HTTP.sys listeners, netsh still matters:
netsh http show sslcert
netsh http delete sslcert ipport=0.0.0.0:443
netsh http add sslcert ipport=0.0.0.0:443 certhash=0123456789ABCDEF0123456789ABCDEF01234567 appid='{4dc3e181-e14b-4a21-b022-59fc669b0914}' certstorename=MY
I only use netsh when I am dealing with HTTP.sys bindings directly or something outside normal IIS site management. For ordinary IIS websites, WebAdministration is cleaner.
Validate after rebinding
I never assume a binding change worked just because PowerShell stayed quiet.
- inspect the IIS binding again
- browse locally with
Invoke-WebRequest - query the remote certificate from another system if possible
- confirm the old certificate is no longer presented
A local check:
Invoke-WebRequest -Uri 'https://example.com/' -UseBasicParsing
An external check from another host is even better, because it confirms what clients really receive.
Troubleshooting patterns I keep seeing
The certificate imported fine, but IIS cannot use it
Usually one of these:
- cert is in
CurrentUserinstead ofLocalMachine - no private key present
- wrong cert selected during binding update
- permissions problem on the private key
The server presents the old certificate after renewal
Usually one of these:
- IIS binding still points to the old thumbprint
- SNI binding for the hostname was missed
- upstream load balancer or CDN is terminating TLS instead
- another listener on the same port owns the binding
Remote check shows a different certificate than the one in MMC
That nearly always means you are not looking at the termination point that clients use. I have run into this with reverse proxies, ADCs, security appliances, and CDN front ends. The Windows server can hold the “right” cert and still not be the thing speaking TLS to the outside world.
Final thoughts
PowerShell is not just a nicer way to browse the certificate store. It is the tool I use when I need answers I can trust: what is expiring, what thumbprint is actually bound, whether the private key exists, whether the remote endpoint presents what I expect, and whether an import went to the right store.
The biggest operational wins are simple ones. Use thumbprints instead of guesswork. Import into LocalMachine\My for services. Verify HasPrivateKey. Rebind IIS explicitly after renewal. And when format conversions get awkward, stop fighting PowerShell and use OpenSSL for the parts OpenSSL still does better.
That combination has saved me a lot of time, and more than a few avoidable outages.