Monitoring Azure App Client Secrets and SSL Certificates Expiration
Step-by-step guide on how to integrate Microsoft Graph and NetCrunch to automatically detect expiring client secrets and certificates in your Azure apps—before they cause outages
Managing Azure applications at scale involves a hidden risk: expired client secrets and SSL certificates. These silent failures can lead to sudden authentication errors, service disruptions, and frustrating downtime.
This guide walks you through integrating PowerShell, Microsoft Graph API, and NetCrunch seamlessly. You’ll learn how to extract expiration data from Azure, transform it into actionable metrics, and configure NetCrunch to alert you before issues occur - so that you can stay one step ahead, every time.
1. Install Microsoft Graph Module
Open PowerShell as Administrator and run:
Install-Module Microsoft.Graph
2. Configure Azure (Microsoft Graph)
a. Log in to Azure Portal.
b. Assign required permissions (assuming you already have a registered application):
- Go to Azure Active Directory → App registrations → [Your Application] → API permissions → Add permission → Microsoft Graph → Application permissions
- Select
Application.Read.All
. - Click Add permissions.
- Click Grant admin consent (requires Global Administrator role).
You need these credentials:
TenantId
: from Azure AD OverviewAppId
(Client ID): from App registration OverviewClientSecret
: existing secret from your app registration
3. PowerShell Script (Get-AppCredentialsExpiry.ps1)
An anonymized example script to fetch Client Secrets and SSL certificate expiry:
<#
.SYNOPSIS
Retrieves Client Secret and SSL certificate expiration dates for all Microsoft Entra ID applications.
.DESCRIPTION
This script connects to Microsoft Graph API, fetches all applications, and checks expiration dates
of Client Secrets and SSL certificates. Results are saved to a JSON file in UTF-8 without BOM.
.NOTES
WARNING: Hardcoding credentials in scripts is insecure. For production, use Azure Key Vault or environment variables.
#>
# Authentication data (REPLACE WITH YOUR VALUES)
$TenantId = "7d9e17ec-0011-4642-ad4e-1351beed5880" # e.g., "contoso.onmicrosoft.com" or GUID
$AppId = "cc14f6ad-c967-431f-a702-1c4f00b62c04" # Application (Client) ID
$ClientSecret = "0Wd8Q~UkSn.qUkRHqHYANWxz01c6K4ANx5TtRaC6" # Secret value (visible only once when created)
$OutputFile = "credentials_expiry_report.json"
# Function to get access token
function Get-AccessToken {
param (
[string]$TenantId,
[string]$AppId,
[string]$ClientSecret
)
$tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$body = @{
grant_type = "client_credentials"
client_id = $AppId
client_secret = $ClientSecret
scope = "https://graph.microsoft.com/.default"
}
try {
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body
return $response.access_token
} catch {
Write-Error "Failed to get access token: $_"
exit 1
}
}
# Function to fetch all applications
function Get-AllApplications {
param (
[string]$AccessToken
)
$headers = @{
Authorization = "Bearer $AccessToken"
}
$url = "https://graph.microsoft.com/v1.0/applications" # Corrected endpoint
$allApps = @()
try {
do {
$response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get
$allApps += $response.value
$url = $response.'@odata.nextLink'
} while ($null -ne $url)
return $allApps
} catch {
Write-Error "Failed to retrieve applications: $_"
exit 1
}
}
# Main execution
$accessToken = Get-AccessToken -TenantId $TenantId -AppId $AppId -ClientSecret $ClientSecret
$applications = Get-AllApplications -AccessToken $accessToken
# Prepare results
$result = @()
foreach ($app in $applications) {
$appData = @{
appId = $app.appId
displayName = $app.displayName
clientSecrets = @()
keyCredentials = @() # SSL certificates
}
# Process Client Secrets
if ($app.passwordCredentials) {
foreach ($secret in $app.passwordCredentials) {
$appData.clientSecrets += @{
displayName = $secret.displayName
startDateTime = $secret.startDateTime
endDateTime = $secret.endDateTime
daysUntilExpiry = if ($secret.endDateTime) { (New-TimeSpan -Start (Get-Date) -End ([DateTime]::Parse($secret.endDateTime))).Days } else { $null }
isExpired = if ($secret.endDateTime) { [DateTime]::Parse($secret.endDateTime) -lt (Get-Date) } else { $null }
}
}
}
# Process SSL certificates (Key Credentials)
if ($app.keyCredentials) {
foreach ($cert in $app.keyCredentials) {
$appData.keyCredentials += @{
displayName = $cert.displayName
startDateTime = $cert.startDateTime
endDateTime = $cert.endDateTime
type = $cert.type # e.g., "AsymmetricX509Cert"
usage = $cert.usage # e.g., "Verify"
daysUntilExpiry = if ($cert.endDateTime) { (New-TimeSpan -Start (Get-Date) -End ([DateTime]::Parse($cert.endDateTime))).Days } else { $null }
isExpired = if ($cert.endDateTime) { [DateTime]::Parse($cert.endDateTime) -lt (Get-Date) } else { $null }
}
}
}
$result += $appData
}
# Save to JSON (UTF-8 without BOM)
try {
$jsonContent = $result | ConvertTo-Json -Depth 10
$outputPath = Join-Path -Path $PSScriptRoot -ChildPath $OutputFile
[System.IO.File]::WriteAllText($outputPath, $jsonContent, [System.Text.UTF8Encoding]::new($false))
Write-Host "Report saved to: $outputPath (UTF-8 without BOM)"
} catch {
Write-Error "Failed to save file: $_"
exit 1
}
4. NetCrunch Configuration
a. Open NetCrunch, go to Settings → Data Parsers
b. Create a new parser (JavaScript type) and paste:
const doc = typeof data === 'string' ? JSON.parse(data) : data;
result = result;
for (let i = 0; i < doc.length; i++) {
const app = doc[i];
const appName = app.displayName || `App${i}`;
if (Array.isArray(app.clientSecrets)) {
app.clientSecrets.forEach((secret, ix) => {
if (secret.daysUntilExpiry !== undefined) {
const days = Math.floor(secret.daysUntilExpiry);
const counterName = 'Client Secret/Days To Expiration';
const secretName = secret.displayName || `Secret${i + ix}`;
result = result.counter(counterName, days, `${appName} - ${secretName}`);
}
});
}
if (Array.isArray(app.keyCredentials)) {
app.keyCredentials.forEach((cert, ix) => {
if (cert.daysUntilExpiry !== undefined) {
const days = Math.floor(cert.daysUntilExpiry);
const certName = cert.displayName || `Cert #${ix + 1}`;
const counterName = 'Certificates/Days To Expiration';
result = result.counter(counterName, days, `${appName} - ${certName}`);
}
});
}
}
c. In Test Data, paste the JSON content from credentials_expiry_report.json
.

5. Add Script Sensor in NetCrunch
Important File Location Note:
Make sure that the PowerShell script (Get-AppCredentialsExpiry.ps1
) and the resulting JSON file (credentials_expiry_report.json
) are placed on the same machine where the NetCrunch Server is installed. This ensures that the Script Sensor can access the script and read the JSON output directly. The file names used here are examples and can be changed to suit your environment.
-
Select a Node for Azure Apps Secrets and SSL Certificates monitoring.
-
Add a new Script Sensor:

- Sensor Name: e.g.,
AzureAppCredentialsMonitoring
- Script Type: PowerShell
- Specify the path and script name (e.g.,
Get-AppCredentialsExpiry.ps1
). - Check Read result from file, specify the path to the JSON file (e.g.,
credentials_expiry_report.json
). - Select the previously created data parser.
6. Configure Alerts
-
Add a new Alert:
- Event Trigger: Monitoring Sensor Counter

- Choose severity and description.
- Select Counter → change "average value" to "value", set "less than" and enter threshold days (e.g., 30).

- Counter Selection:
From the Performance Object, select Certificates or Client Secret. You can monitor a single application or all monitored applications by selecting the appropriate instance option (e.g., selecting All instances will monitor every application, whereas selecting a specific instance will limit the alert to that application).


-
Confirm your selection and create the Alert.
-
Associate your preferred Alerting Script if needed.
8. Completion
NetCrunch will now generate alerts when any Azure App Client Secret or SSL certificate approaches the expiration threshold you've configured, notifying you accordingly.
- [20.07.2020] Setting up secure access to the monitoring server via Windows IIS Reverse Proxy with SSL certificate
A reverse proxy protects applications against cybercriminals and malicious software. It also allows limiting access to applications based on username, IP, domain, or geographical location.
- [02.07.2020] How to set up secure remote access to monitoring server using a NginX Reverse Proxy with SSL certificate
A reverse proxy protects applications against cybercriminals and malicious software. It also allows limiting access to applications based on username, IP, domain, or geographical location.
- [26.09.2019] Advanced SSL Certificate monitoring
Nowadays secure connections are common, sites/servers without certificates are flagged as 'unsafe' and people tend to avoid such places on the web. This article will demonstrate how you can easily monitor not only if certificates are about to expire or expired but several other properties included in it.
- [12.09.2018]Monitoring SSL Certificate expiration date with NetCrunch
Learn how easy you can monitor an SSL certificate with NetCrunch. Use it to be informed about SSL certificate expiration time and the properties of such certificate.
- [12.04.2018]Generate NetCrunch SSL certificate with Microsoft Certificate Authority server
Learn how to use certificates generated by Microsoft Certificate Authority to secure Web Access connection to the NetCrunch server.