PowerShell Script: Automate SSL Certificate Renewal Checks

Why Certificates Expire in Well-Managed Environments
Certificate expiry surprises are not just a problem in chaotic environments. They happen in mature, well-managed infrastructure because certificate inventories live in people’s heads, renewal calendars drift, and auto-renewal tooling silently fails. A monitoring gap of even a few days can mean a weekend outage. Automated PowerShell checks that run daily and send tiered alerts at 30-day and 7-day thresholds are the only reliably safe approach — they require no agent, no third-party tool, and no per-server configuration beyond network access.
Quick Answer
Use [Net.HttpWebRequest] to connect to each HTTPS endpoint, extract the certificate’s NotAfter property, compute days remaining with (Get-Date), and send alert emails for anything expiring within your threshold windows.
Probing HTTPS Endpoints with [Net.HttpWebRequest] to Extract Certificate
The .NET HttpWebRequest class gives you direct access to the certificate returned by an HTTPS endpoint — without requiring OpenSSL or any external dependency. The key is capturing the ServicePoint.Certificate after the request completes.
function Get-RemoteCertificate {
param([Parameter(Mandatory)][string]$Url)
# Allow self-signed certs during retrieval (validation is separate concern)
$previousCallback = [Net.ServicePointManager]::ServerCertificateValidationCallback
[Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
try {
$req = [Net.HttpWebRequest]::Create($Url)
$req.Timeout = 10000
$req.AllowAutoRedirect = $false
$null = $req.GetResponse()
$cert = $req.ServicePoint.Certificate
[PSCustomObject]@{
Url = $Url
Subject = $cert.Subject
NotAfter = [DateTime]::Parse($cert.GetExpirationDateString())
NotBefore = [DateTime]::Parse($cert.GetEffectiveDateString())
Issuer = $cert.Issuer
}
}
catch {
Write-Warning "Failed to probe ${Url}: $_"
$null
}
finally {
[Net.ServicePointManager]::ServerCertificateValidationCallback = $previousCallback
}
}
Restore the original validation callback in the finally block. Leaving global SSL validation disabled beyond the probe window creates a security exposure.
Calculating Days Until Expiry with Certificate NotAfter Property
Once you have the certificate object, computing days until expiry is straightforward arithmetic. Wrapping it in a function with a typed return object makes downstream filtering and reporting clean.
function Measure-CertExpiry {
param([Parameter(ValueFromPipeline)][PSCustomObject]$CertInfo)
process {
if ($null -eq $CertInfo) { return }
$daysLeft = ($CertInfo.NotAfter - (Get-Date)).Days
$CertInfo | Add-Member -NotePropertyName DaysRemaining -NotePropertyValue $daysLeft -PassThru |
Add-Member -NotePropertyName Status -NotePropertyValue (
if ($daysLeft -le 0) { 'EXPIRED' }
elseif ($daysLeft -le 7) { 'CRITICAL' }
elseif ($daysLeft -le 30) { 'WARNING' }
else { 'OK' }
) -PassThru
}
}
Checking Local Machine Certificate Store with Get-ChildItem Cert:
For servers hosting IIS or other services, the local certificate store is just as important as remote endpoint checks. The Cert: PSDrive makes store enumeration identical to filesystem navigation.
# Check LocalMachine\My for certificates expiring within 30 days
$threshold = (Get-Date).AddDays(30)
Get-ChildItem -Path 'Cert:\LocalMachine\My' |
Where-Object { $_.NotAfter -lt $threshold -and $_.NotAfter -gt (Get-Date) } |
Select-Object Subject, Thumbprint, NotAfter,
@{N='DaysRemaining'; E={ ($_.NotAfter - (Get-Date)).Days }} |
Sort-Object DaysRemaining
Tiered Alert Logic: 30-Day and 7-Day Thresholds
Tiered alerts prevent alert fatigue. A 30-day warning gives you time to schedule renewal during a maintenance window. A 7-day critical fires when action is genuinely urgent. The logic below processes both remote and local results and routes them to the appropriate alert bucket.
$urls = @(
'https://www.example.com',
'https://portal.example.com',
'https://api.example.com'
)
$results = foreach ($url in $urls) {
Get-RemoteCertificate -Url $url | Measure-CertExpiry
}
$critical = $results | Where-Object { $_.Status -in 'CRITICAL','EXPIRED' }
$warning = $results | Where-Object { $_.Status -eq 'WARNING' }
if ($critical) {
Send-MailMessage -To '[email protected]' `
-Subject "CRITICAL: SSL certificates expiring within 7 days" `
-Body ($critical | Format-Table | Out-String) `
-SmtpServer 'smtp.example.com' -From '[email protected]'
}
if ($warning) {
Send-MailMessage -To '[email protected]' `
-Subject "WARNING: SSL certificates expiring within 30 days" `
-Body ($warning | Format-Table | Out-String) `
-SmtpServer 'smtp.example.com' -From '[email protected]'
}
Generating an HTML Summary Report
For a daily digest that covers all certificate statuses rather than just alerts, generate an HTML report. Color-code rows by status so the table is scannable at a glance. Use ConvertTo-Html with a -Head parameter for inline CSS.
$css = @'
<style>
body { font-family: Arial, sans-serif; font-size: 14px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; }
tr.EXPIRED { background: #ff4c4c; color: white; }
tr.CRITICAL { background: #ff9900; }
tr.WARNING { background: #fff3cd; }
tr.OK { background: #d4edda; }
</style>
'@
$html = $results | ConvertTo-Html -Property Url, Subject, NotAfter, DaysRemaining, Status `
-Head $css -Title 'Certificate Status Report' |
ForEach-Object {
$_ -replace '<tr><td>(EXPIRED|CRITICAL|WARNING|OK)', '<tr class="$1"><td>$1'
}
$html | Out-File -FilePath 'C:\Reports\CertStatus.html' -Encoding UTF8
Scheduling and Email Delivery
Register the script as a Scheduled Task to run daily. Use the Task Scheduler PowerShell cmdlets so the setup is repeatable and source-controllable. Point the task at a wrapper script that calls all the functions above and emails the HTML report.
Common Errors
- SSL verification fails for self-signed certificates. The
HttpWebRequestapproach above disables validation during the probe and restores it afterward. For production scripts, scope that bypass tightly — only within the try block and only for the specific request, not globally for the process lifetime. - Certificate retrieved is the load balancer cert, not the backend. When an endpoint sits behind an F5, Azure Front Door, or similar, the TLS termination happens at the load balancer. The certificate you retrieve belongs to that device. To check backend certificates, connect directly to the server IP on the appropriate port, bypassing the load balancer entirely.
Related Cmdlets / See Also
Wrapping Up
A daily certificate check script built on [Net.HttpWebRequest] and the Cert: PSDrive covers both remote endpoints and local stores with no external dependencies. Tiered 30-day and 7-day alerts give your team enough lead time to renew without urgency — and enough urgency to actually act.


