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!