Problem
When you have a lot of App Service instances running in your Azure subscription, managing their SSL certificate status could be a problem. Azure portal itself does have certain monitoring and advising feature to visualize the certificate status and warn you before a certificate expires. However, in some cases, we prefer this to be done in command line environment. That's why I put together this handy PowerShell script that uses Azure CLI to keep tabs on all your App Service SSL certificates.
Solution
On Windows machines, just run this command in your PowerShell 5.1/7.x:
irm https://scripts.edi.wang/Check-AppServiceCertificates.ps1 | iex
The script is pretty straightforward, it crawls through all your Azure resource groups, finds SSL certificates, and tells you which ones are about to expire (or already have).
Key Steps
1. Get All Your Resource Groups
az group list
First, we need to get every resource group in your subscription. Because SSL certificates live at the resource group level, so we need to check them one by one. By iterating through resource groups, we catch certificates bound to multiple apps, certificates that exist but aren't currently bound and certificates in resource groups we might have forgotten about.
2. Check SSL Certificates in Each Resource Group
az webapp config ssl list --resource-group <resource-group-name>
This is the core command! For each resource group, we ask Azure CLI to show us all the SSL certificates. You will get JSON like this:
{
"expirationDate": "2025-12-31T23:59:59+00:00",
"issueDate": "2024-01-01T00:00:00+00:00",
"subjectName": "CN=yourdomain.com",
"thumbprint": "ABC123...",
"selfSigned": false,
"issuer": "Let's Encrypt Authority X3"
}
3. Parse the Certificate Data
The script takes the JSON response and then extract the information:
- Subject name (your domain)
- Expiration date
- Thumbprint
- Whether it's self-signed
The Azure CLI response doesn't include which resource group the certificate came from. Since we're aggregating certificates from multiple resource groups, we need to track the source for proper reporting.
$certWithRG = $cert | Add-Member -NotePropertyName "resourceGroupName" -NotePropertyValue $rg.name -PassThru
4. Calculate Days Until Expiration
$daysToExpiry = [Math]::Round(($expiryDate - $currentDate).TotalDays, 0)
Compare the certificate's expiration date with today's date to see how much time it has left. PowerShell's TimeSpan
object gives us precise calculations including fractional days. We round to whole days.
Complete Code
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Checks the SSL certificate status of Azure App Services using Azure CLI
.DESCRIPTION
This script retrieves all App Service SSL certificates and groups them by expiration status.
It highlights certificates that are expired or about to expire within a specified threshold.
.PARAMETER DaysThreshold
Number of days to consider as "about to expire" (default: 30)
.PARAMETER SubscriptionId
Azure subscription ID (optional - uses current subscription if not specified)
.PARAMETER Debug
Enable detailed debugging output
#>
param(
[int]$DaysThreshold = 30,
[string]$SubscriptionId = $null,
[switch]$Debug
)
# Function to write colored output
function Write-ColorOutput {
param(
[string]$Message,
[string]$Color = "White"
)
switch ($Color) {
"Red" { Write-Host $Message -ForegroundColor Red }
"Yellow" { Write-Host $Message -ForegroundColor Yellow }
"Green" { Write-Host $Message -ForegroundColor Green }
"Cyan" { Write-Host $Message -ForegroundColor Cyan }
"Magenta" { Write-Host $Message -ForegroundColor Magenta }
"Gray" { Write-Host $Message -ForegroundColor Gray }
default { Write-Host $Message }
}
}
# Function to write debug output
function Write-DebugOutput {
param([string]$Message)
if ($Debug) {
Write-ColorOutput "DEBUG: $Message" "Gray"
}
}
# Function to execute Azure CLI command with error capture
function Invoke-AzCommand {
param(
[string]$Command,
[string]$Description
)
Write-DebugOutput "Executing: $Command"
# Capture both stdout and stderr
$tempFile = [System.IO.Path]::GetTempFileName()
$errorFile = [System.IO.Path]::GetTempFileName()
try {
# Execute command and capture output
$process = Start-Process -FilePath "az" -ArgumentList $Command.Split(' ', [StringSplitOptions]::RemoveEmptyEntries)[1..($Command.Split(' ').Length - 1)] -RedirectStandardOutput $tempFile -RedirectStandardError $errorFile -Wait -PassThru -NoNewWindow
$output = Get-Content $tempFile -Raw -ErrorAction SilentlyContinue
$errorOutput = Get-Content $errorFile -Raw -ErrorAction SilentlyContinue
Write-DebugOutput "Exit code: $($process.ExitCode)"
if ($process.ExitCode -ne 0) {
Write-ColorOutput "FAILED to execute: $Description" "Red"
Write-ColorOutput "Command: $Command" "Red"
Write-ColorOutput "Exit Code: $($process.ExitCode)" "Red"
if ($errorOutput) {
Write-ColorOutput "Error Output:" "Red"
Write-ColorOutput $errorOutput "Red"
}
if ($output) {
Write-ColorOutput "Standard Output:" "Yellow"
Write-ColorOutput $output "Yellow"
}
return $null
}
Write-DebugOutput "Command succeeded"
if ($Debug -and $output) {
Write-DebugOutput "Output length: $($output.Length) characters"
}
return $output
}
finally {
# Cleanup temp files
if (Test-Path $tempFile) { Remove-Item $tempFile -Force -ErrorAction SilentlyContinue }
if (Test-Path $errorFile) { Remove-Item $errorFile -Force -ErrorAction SilentlyContinue }
}
}
# Function to format certificate info
function Format-CertificateInfo {
param($cert, $webAppName, $resourceGroup, $status, $daysToExpiry)
$expiryDate = if ($cert.expirationDate) {
[DateTime]::Parse($cert.expirationDate).ToString("yyyy-MM-dd HH:mm:ss UTC")
}
else {
"Unknown"
}
$issueDate = if ($cert.issueDate) {
[DateTime]::Parse($cert.issueDate).ToString("yyyy-MM-dd")
}
else {
"Unknown"
}
return [PSCustomObject]@{
WebApp = $webAppName
ResourceGroup = $resourceGroup
SubjectName = $cert.subjectName
Thumbprint = $cert.thumbprint
IssueDate = $issueDate
ExpirationDate = $expiryDate
DaysToExpiry = $daysToExpiry
Status = $status
Issuer = $cert.issuer
SelfSigned = $cert.selfSigned
}
}
try {
Write-ColorOutput "Azure App Service SSL Certificate Status Checker" "Cyan"
Write-ColorOutput "===================================================" "Cyan"
Write-Host ""
# Check PowerShell version
Write-DebugOutput "PowerShell Version: $($PSVersionTable.PSVersion)"
Write-DebugOutput "OS: $($PSVersionTable.OS)"
# Check if Azure CLI is installed and get version
Write-ColorOutput "Checking Azure CLI..." "Yellow"
$azVersionOutput = Invoke-AzCommand "az version" "Getting Azure CLI version"
if (-not $azVersionOutput) {
Write-ColorOutput "Azure CLI is not installed or not accessible" "Red"
Write-ColorOutput "Please install Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" "Red"
exit 1
}
try {
$azVersionJson = $azVersionOutput | ConvertFrom-Json
Write-ColorOutput "Azure CLI Version: $($azVersionJson.'azure-cli')" "Green"
Write-DebugOutput "Full version info: $azVersionOutput"
}
catch {
Write-ColorOutput "Azure CLI is installed (version parsing failed)" "Green"
Write-DebugOutput "Raw version output: $azVersionOutput"
}
# Check if logged in
Write-ColorOutput "Checking authentication..." "Yellow"
$accountOutput = Invoke-AzCommand "az account show" "Getting current account"
if (-not $accountOutput) {
Write-ColorOutput "Not logged in to Azure CLI" "Red"
Write-ColorOutput "Please run 'az login' first" "Red"
exit 1
}
try {
$account = $accountOutput | ConvertFrom-Json
Write-ColorOutput "Logged in as: $($account.user.name)" "Green"
Write-DebugOutput "Account type: $($account.user.type)"
Write-DebugOutput "Tenant ID: $($account.tenantId)"
}
catch {
Write-ColorOutput "Failed to parse account information" "Red"
Write-ColorOutput "Raw output: $accountOutput" "Red"
exit 1
}
# Set subscription if provided
if ($SubscriptionId) {
Write-ColorOutput "Setting subscription to: $SubscriptionId" "Yellow"
$setSubOutput = Invoke-AzCommand "az account set --subscription $SubscriptionId" "Setting subscription"
if (-not $setSubOutput -and $setSubOutput -ne "") {
Write-ColorOutput "Failed to set subscription" "Red"
exit 1
}
}
# Get current subscription details
$currentSubscriptionOutput = Invoke-AzCommand "az account show" "Getting current subscription"
if (-not $currentSubscriptionOutput) {
Write-ColorOutput "Failed to get current subscription" "Red"
exit 1
}
try {
$currentSubscription = $currentSubscriptionOutput | ConvertFrom-Json
Write-ColorOutput "Current subscription: $($currentSubscription.name) ($($currentSubscription.id))" "Green"
Write-DebugOutput "Subscription state: $($currentSubscription.state)"
}
catch {
Write-ColorOutput "Failed to parse subscription information" "Red"
exit 1
}
Write-Host ""
# First, let's try to get all SSL certificates at once
Write-ColorOutput "Retrieving all SSL certificates..." "Yellow"
# Method 1: Try to get all certificates from all resource groups
$allCertificates = @()
# Get all resource groups
$resourceGroupsOutput = Invoke-AzCommand "az group list" "Getting resource groups"
if ($resourceGroupsOutput) {
try {
$resourceGroups = $resourceGroupsOutput | ConvertFrom-Json
Write-ColorOutput "Found $($resourceGroups.Count) resource group(s)" "Green"
Write-DebugOutput "Resource groups: $($resourceGroups.name -join ', ')"
# Check SSL certificates in each resource group
foreach ($rg in $resourceGroups) {
Write-ColorOutput "Checking SSL certificates in resource group: $($rg.name)" "Yellow"
Write-DebugOutput "Resource group: $($rg.name), Location: $($rg.location)"
$sslOutput = Invoke-AzCommand "az webapp config ssl list --resource-group $($rg.name)" "Getting SSL certificates for resource group $($rg.name)"
if ($sslOutput) {
try {
$sslCerts = $sslOutput | ConvertFrom-Json
if ($sslCerts -and $sslCerts.Count -gt 0) {
Write-ColorOutput "Found $($sslCerts.Count) SSL certificate(s) in $($rg.name)" "Green"
Write-DebugOutput "Certificates in $($rg.name): $($sslCerts.subjectName -join ', ')"
foreach ($cert in $sslCerts) {
# Add resource group info to certificate
$certWithRG = $cert | Add-Member -NotePropertyName "resourceGroupName" -NotePropertyValue $rg.name -PassThru
$allCertificates += $certWithRG
}
}
else {
Write-DebugOutput "No SSL certificates found in resource group: $($rg.name)"
}
}
catch {
Write-DebugOutput "Failed to parse SSL certificates for resource group $($rg.name): $($_.Exception.Message)"
if ($Debug) {
Write-DebugOutput "Raw SSL output for $($rg.name): $($sslOutput.Substring(0, [Math]::Min(200, $sslOutput.Length)))"
}
}
}
else {
Write-DebugOutput "No SSL certificates found in resource group: $($rg.name) or command failed"
}
}
}
catch {
Write-ColorOutput "Failed to parse resource groups" "Red"
exit 1
}
}
else {
Write-ColorOutput "Failed to get resource groups" "Red"
exit 1
}
Write-Host ""
if (-not $allCertificates -or $allCertificates.Count -eq 0) {
Write-ColorOutput "No SSL certificates found across all resource groups" "Yellow"
Write-ColorOutput "This could mean:" "White"
Write-ColorOutput "• No custom SSL certificates are configured" "White"
Write-ColorOutput "• All web apps are using default *.azurewebsites.net certificates" "White"
Write-ColorOutput "• You don't have permissions to view SSL configurations" "White"
Write-ColorOutput "• SSL certificates are managed through Azure Key Vault or other services" "White"
Write-Host ""
# Let's also try to check if there are any web apps
Write-ColorOutput "Checking for web apps in subscription..." "Yellow"
$webAppsOutput = Invoke-AzCommand "az webapp list" "Getting web apps"
if ($webAppsOutput) {
try {
$webApps = $webAppsOutput | ConvertFrom-Json
if ($webApps -and $webApps.Count -gt 0) {
Write-ColorOutput "Found $($webApps.Count) web app(s) in subscription:" "Green"
foreach ($app in $webApps) {
Write-ColorOutput " • $($app.name) (Resource Group: $($app.resourceGroup))" "White"
}
Write-Host ""
Write-ColorOutput "These web apps might be using:" "Cyan"
Write-ColorOutput " • Default *.azurewebsites.net certificates (free)" "White"
Write-ColorOutput " • App Service Managed Certificates" "White"
Write-ColorOutput " • Certificates from Azure Key Vault" "White"
}
else {
Write-ColorOutput "No web apps found in subscription" "Yellow"
}
}
catch {
Write-DebugOutput "Failed to parse web apps output"
}
}
exit 0
}
Write-ColorOutput "Found $($allCertificates.Count) SSL certificate(s) across all resource groups" "Green"
if ($Debug) {
Write-DebugOutput "Certificate summary:"
foreach ($cert in $allCertificates) {
Write-DebugOutput " - $($cert.subjectName) in RG: $($cert.resourceGroupName)"
}
}
# Current date for comparison
$currentDate = Get-Date
Write-DebugOutput "Current date: $currentDate"
# Arrays to store categorized certificates
$expiredCerts = @()
$aboutToExpireCerts = @()
$validCerts = @()
# Process each certificate
foreach ($cert in $allCertificates) {
Write-DebugOutput "Processing certificate: $($cert.subjectName) in RG: $($cert.resourceGroupName)"
if ($cert.expirationDate) {
try {
$expiryDate = [DateTime]::Parse($cert.expirationDate)
$daysToExpiry = [Math]::Round(($expiryDate - $currentDate).TotalDays, 0)
Write-DebugOutput "Certificate $($cert.subjectName) expires on $expiryDate (in $daysToExpiry days)"
if ($daysToExpiry -lt 0) {
$expiredCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "EXPIRED" $daysToExpiry
}
elseif ($daysToExpiry -le $DaysThreshold) {
$aboutToExpireCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "EXPIRING SOON" $daysToExpiry
}
else {
$validCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "VALID" $daysToExpiry
}
}
catch {
Write-DebugOutput "Failed to parse expiration date for $($cert.subjectName): $($cert.expirationDate)"
$validCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "UNKNOWN" "N/A"
}
}
else {
Write-DebugOutput "Certificate $($cert.subjectName) has no expiration date"
$validCerts += Format-CertificateInfo $cert "Multiple/Unknown" $cert.resourceGroupName "UNKNOWN" "N/A"
}
}
# Display results
Write-ColorOutput "SSL CERTIFICATE STATUS SUMMARY" "Cyan"
Write-ColorOutput "==================================" "Cyan"
Write-ColorOutput "• Expired: $($expiredCerts.Count)" "Red"
Write-ColorOutput "• Expiring Soon (≤$DaysThreshold days): $($aboutToExpireCerts.Count)" "Yellow"
Write-ColorOutput "• Valid: $($validCerts.Count)" "Green"
Write-Host ""
# Display expired certificates
if ($expiredCerts.Count -gt 0) {
Write-ColorOutput "EXPIRED CERTIFICATES" "Red"
Write-ColorOutput "========================" "Red"
$expiredCerts | Sort-Object DaysToExpiry | Format-Table -Property ResourceGroup, SubjectName, ExpirationDate, DaysToExpiry, SelfSigned, Thumbprint -AutoSize
Write-Host ""
}
# Display certificates about to expire
if ($aboutToExpireCerts.Count -gt 0) {
Write-ColorOutput "CERTIFICATES EXPIRING SOON (≤$DaysThreshold days)" "Yellow"
Write-ColorOutput "================================================" "Yellow"
$aboutToExpireCerts | Sort-Object DaysToExpiry | Format-Table -Property ResourceGroup, SubjectName, ExpirationDate, DaysToExpiry, SelfSigned, Thumbprint -AutoSize
Write-Host ""
}
# Display valid certificates
if ($validCerts.Count -gt 0) {
Write-ColorOutput "VALID CERTIFICATES" "Green"
Write-ColorOutput "=====================" "Green"
$validCerts | Sort-Object DaysToExpiry | Format-Table -Property ResourceGroup, SubjectName, ExpirationDate, DaysToExpiry, SelfSigned, Thumbprint -AutoSize
Write-Host ""
}
# Summary recommendations
Write-ColorOutput "RECOMMENDATIONS" "Cyan"
Write-ColorOutput "==================" "Cyan"
if ($expiredCerts.Count -gt 0) {
Write-ColorOutput "URGENT: $($expiredCerts.Count) certificate(s) have already expired and need immediate attention!" "Red"
}
if ($aboutToExpireCerts.Count -gt 0) {
Write-ColorOutput "WARNING: $($aboutToExpireCerts.Count) certificate(s) will expire within $DaysThreshold days. Plan renewal soon." "Yellow"
}
if ($expiredCerts.Count -eq 0 -and $aboutToExpireCerts.Count -eq 0) {
Write-ColorOutput "All certificates are valid and not expiring soon. Great job!" "Green"
}
Write-Host ""
Write-ColorOutput "To manage SSL certificates:" "Cyan"
Write-ColorOutput " • Azure Portal: App Services > [Your App] > TLS/SSL settings" "White"
Write-ColorOutput " • Azure CLI: az webapp config ssl --help" "White"
Write-ColorOutput " • List certificates by resource group: az webapp config ssl list --resource-group <rg-name>" "White"
}
catch {
Write-ColorOutput "An unexpected error occurred: $($_.Exception.Message)" "Red"
Write-ColorOutput "Stack trace: $($_.ScriptStackTrace)" "Red"
exit 1
}
Tips
- You can use the
-Debug
flag if something blows up - If you're getting errors about commands not being recognized, double-check you're using the right Azure CLI syntax. The Azure CLI team loves to keep us on our toes with command changes!
Comments