Monitor Azure Service Principal Password Expiration with PowerShell
Managing Service Principal (SPN) credentials is a critical aspect of Azure security. Expired passwords can cause service disruptions, while timely monitoring helps maintain operational continuity and security compliance. This article demonstrates how to use Azure PowerShell with Microsoft Graph to monitor all existing password credentials on Service Principals and their expiration status.
Overview
Service Principals in Azure are identities used by applications, services, and automation tools to access Azure resources. These identities often use password credentials (client secrets) that have expiration dates. Monitoring these expiration dates is crucial for:
- Preventing Service Disruptions: Avoid application failures due to expired credentials
- Security Compliance: Ensure regular credential rotation according to security policies
- Proactive Management: Get advance notice before credentials expire
- Audit and Reporting: Maintain visibility of all credential expiration dates
Prerequisites
Before running the monitoring script, ensure you have:
- Azure PowerShell Module: Install the Microsoft Graph PowerShell SDK
Install-Module Microsoft.Graph -Scope CurrentUser - Required Permissions: You need the following Microsoft Graph API permissions:
Application.Read.All- To read Service Principal and application informationMail.Send- Only if you plan to use the email notification feature (optional)
-
Appropriate Azure Role: Ensure your account has sufficient permissions to read Service Principal information (e.g., Application Administrator, Cloud Application Administrator, or Global Administrator)
- Email Requirements (for notification scenarios): If using Microsoft Graph for email notifications, ensure the sending account has an Exchange Online mailbox or use application-level permissions
The Monitoring Script
Here’s a comprehensive PowerShell script that connects to Microsoft Graph, retrieves all Service Principals with password credentials, and generates an expiration report:
# Connect to Microsoft Graph if not already connected
if (!(Get-MgContext)) { Connect-MgGraph -Scopes "Application.Read.All" }
# Get all Service Principals with password credentials
# Note: This may take several minutes in large tenants with many Service Principals
$allSPNs = Get-MgServicePrincipal -All -Property "Id", "DisplayName", "AppId", "PasswordCredentials"
$report = foreach ($spn in $allSPNs) {
# Only process SPNs that have password credentials
if ($spn.PasswordCredentials.Count -gt 0) {
foreach ($credential in $spn.PasswordCredentials) {
$expiryDate = $credential.EndDateTime
$daysLeft = ($expiryDate - (Get-Date)).Days
[PSCustomObject]@{
DisplayName = $spn.DisplayName
AppId = $spn.AppId
KeyId = $credential.KeyId
ExpirationDate = $expiryDate
DaysRemaining = $daysLeft
Status = if ($daysLeft -le 0) { "Expired" } elseif ($daysLeft -le 30) { "Urgent" } else { "Healthy" }
}
}
}
}
# Display results in a sortable table (requires graphical interface)
# For non-GUI environments, use: $report | Sort-Object DaysRemaining | Format-Table -AutoSize
$report | Sort-Object DaysRemaining | Out-GridView -Title "SPN Secret Expiration Report"
Understanding the Script
Let’s break down what each section of the script does:
1. Authentication
if (!(Get-MgContext)) { Connect-MgGraph -Scopes "Application.Read.All" }
This checks if you’re already connected to Microsoft Graph. If not, it prompts for authentication with the required scope to read application information.
2. Retrieve Service Principals
$allSPNs = Get-MgServicePrincipal -All -Property "Id", "DisplayName", "AppId", "PasswordCredentials"
This retrieves all Service Principals in your tenant, specifically requesting the properties we need for our report.
3. Generate Report
The script iterates through each Service Principal and its password credentials, creating a custom object with:
- DisplayName: The friendly name of the Service Principal
- AppId: The unique application ID
- KeyId: The unique identifier for the specific credential
- ExpirationDate: When the credential expires
- DaysRemaining: Number of days until expiration
- Status: Categorized as “Expired” (≤0 days), “Urgent” (≤30 days), or “Healthy” (>30 days)
4. Display Results
$report | Sort-Object DaysRemaining | Out-GridView -Title "SPN Secret Expiration Report"
Results are sorted by days remaining and displayed in an interactive grid view for easy filtering and sorting.
Setting Up Notifications
To make this monitoring solution more proactive, you can extend it to send notifications. Here are several approaches:
Option 1: Email Notifications with Microsoft Graph
Note: This method requires:
- The
Mail.Sendpermission in addition toApplication.Read.All- The sending user account must have an Exchange Online mailbox
- Connect with:
Connect-MgGraph -Scopes "Application.Read.All", "Mail.Send"
# Filter for credentials expiring soon or already expired
$criticalCredsEmail = $report | Where-Object { $_.DaysRemaining -le 30 }
if ($criticalCredsEmail.Count -gt 0) {
$emailBody = $criticalCredsEmail | ConvertTo-Html -Property DisplayName, AppId, ExpirationDate, DaysRemaining, Status | Out-String
# Using Microsoft Graph API to send email (recommended method)
$mailParams = @{
Message = @{
Subject = "⚠️ Azure SPN Credentials Expiring Soon"
Body = @{
ContentType = "HTML"
Content = $emailBody
}
ToRecipients = @(
@{
EmailAddress = @{
Address = "[email protected]"
}
}
)
}
SaveToSentItems = $false
}
Send-MgUserMail -UserId "[email protected]" -BodyParameter $mailParams
}
Note: The legacy
Send-MailMessagecmdlet is deprecated. Use Microsoft Graph API’sSend-MgUserMailfor modern authentication support.
Option 2: Microsoft Teams Webhook (Adaptive Card)
Note: To get your Teams Webhook URL:
- In your Teams channel, click the ••• menu
- Select “Connectors” or “Workflows”
- Add an “Incoming Webhook” connector
- Name it and copy the generated webhook URL
$criticalCredsTeams = $report | Where-Object { $_.DaysRemaining -le 30 }
if ($criticalCredsTeams.Count -gt 0) {
$teamsWebhook = "YOUR_TEAMS_WEBHOOK_URL"
# Using Adaptive Card format (recommended over MessageCard)
$adaptiveCard = @{
type = "message"
attachments = @(
@{
contentType = "application/vnd.microsoft.card.adaptive"
content = @{
type = "AdaptiveCard"
body = @(
@{
type = "TextBlock"
size = "Large"
weight = "Bolder"
text = "⚠️ Service Principal Credentials Expiring Soon"
color = "Attention"
}
@{
type = "FactSet"
facts = $criticalCredsTeams | ForEach-Object {
@{
title = $_.DisplayName
value = "Expires in $($_.DaysRemaining) days - $($_.ExpirationDate.ToString('yyyy-MM-dd'))"
}
}
}
)
# Note: $schema is a JSON property name, not a PowerShell variable
'$schema' = "http://adaptivecards.io/schemas/adaptive-card.json"
version = "1.4"
}
}
)
}
Invoke-RestMethod -Uri $teamsWebhook -Method Post -Body ($adaptiveCard | ConvertTo-Json -Depth 10) -ContentType "application/json"
}
Option 3: Azure Logic Apps Integration
For a more robust solution, export the report to a file and trigger an Azure Logic App:
# Export to CSV
$report | Export-Csv -Path "spn-expiration-report.csv" -NoTypeInformation
# Or export to JSON for Logic App consumption
$report | ConvertTo-Json -Depth 10 | Out-File "spn-expiration-report.json"
You can then create an Azure Logic App that:
- Triggers on a schedule or file upload to Azure Storage
- Reads the report data
- Sends notifications via email, Teams, or ServiceNow
- Creates tickets for expired or soon-to-expire credentials
Automation with Azure Automation
To run this monitoring automatically, deploy it as an Azure Automation Runbook:
- Create an Automation Account in the Azure Portal
- Import Required Modules: Add
Microsoft.Graph.AuthenticationandMicrosoft.Graph.Applicationsmodules from the gallery - Configure Managed Identity: Enable system-assigned or user-assigned managed identity for the Automation Account
- Grant Permissions to Managed Identity: Use PowerShell to grant Application.Read.All permission:
# Connect to Microsoft Graph as admin Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" # Get the Managed Identity's service principal $managedIdentity = Get-MgServicePrincipal -Filter "displayName eq 'YOUR-AUTOMATION-ACCOUNT-NAME'" # Get Microsoft Graph's service principal $graphSP = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" # Get the Application.Read.All role $appRole = $graphSP.AppRoles | Where-Object { $_.Value -eq "Application.Read.All" } # Assign the role to the managed identity New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $managedIdentity.Id ` -PrincipalId $managedIdentity.Id ` -AppRoleId $appRole.Id ` -ResourceId $graphSP.Id - Create a Runbook with the monitoring script
- Schedule the Runbook to run daily or weekly
Example runbook script:
# Import required modules
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Applications
try {
# Connect using Managed Identity
Connect-MgGraph -Identity -NoWelcome
# Get all Service Principals with password credentials
$allSPNs = Get-MgServicePrincipal -All -Property "Id", "DisplayName", "AppId", "PasswordCredentials"
# Generate the report (same logic as the main script)
$report = foreach ($spn in $allSPNs) {
if ($spn.PasswordCredentials.Count -gt 0) {
foreach ($credential in $spn.PasswordCredentials) {
$expiryDate = $credential.EndDateTime
$daysLeft = ($expiryDate - (Get-Date)).Days
[PSCustomObject]@{
DisplayName = $spn.DisplayName
AppId = $spn.AppId
KeyId = $credential.KeyId
ExpirationDate = $expiryDate
DaysRemaining = $daysLeft
Status = if ($daysLeft -le 0) { "Expired" } elseif ($daysLeft -le 30) { "Urgent" } else { "Healthy" }
}
}
}
}
# Export results to Automation Account output
Write-Output "Report generated: $($report.Count) credentials monitored"
Write-Output "Expired: $(($report | Where-Object {$_.Status -eq 'Expired'}).Count)"
Write-Output "Urgent: $(($report | Where-Object {$_.Status -eq 'Urgent'}).Count)"
# Add your notification logic here (Teams, Email, etc.)
} catch {
Write-Error "Error occurred: $_"
throw
} finally {
Disconnect-MgGraph | Out-Null
}
Best Practices
- Regular Monitoring: Run the script at least weekly to catch expiring credentials in time
- Advance Notice: Set your “Urgent” threshold based on your organization’s credential rotation process time (e.g., 60 days if the approval process takes weeks)
- Automated Rotation: Consider implementing automated credential rotation for non-critical applications
- Audit Trail: Keep historical reports to demonstrate compliance and track credential management
- Least Privilege: Use the minimum required permissions (
Application.Read.Allis read-only) - Multiple Notification Channels: Implement redundant notification methods to ensure alerts are received
Troubleshooting
Common Issues
Issue: “Insufficient privileges to complete the operation”
- Solution: Ensure your account has the
Application.Read.Allpermission granted and admin consent is given
Issue: Script runs but returns no results
- Solution: Verify that Service Principals actually have password credentials configured. Some SPNs only use certificate credentials or managed identities
Issue: Out-GridView doesn’t display
- Solution: Out-GridView requires a graphical interface. On Windows Server Core, Linux, or in Azure Automation runbooks, use alternative output methods like
$report | Sort-Object DaysRemaining | Format-Table -AutoSizeor export to CSV with$report | Export-Csv -Path "report.csv" -NoTypeInformation
Conclusion
Monitoring Service Principal password expiration is essential for maintaining secure and reliable Azure operations. By implementing this PowerShell-based monitoring solution with automated notifications, you can proactively manage credential lifecycles and prevent service disruptions.
The script provided gives you a solid foundation that can be extended with:
- Custom notification logic
- Integration with ITSM tools
- Automated credential rotation workflows
- Compliance reporting
Remember to regularly review and update your Service Principal credentials as part of your overall security hygiene practices.
Comments