7 minute read

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:

  1. Azure PowerShell Module: Install the Microsoft Graph PowerShell SDK
    Install-Module Microsoft.Graph -Scope CurrentUser
    
  2. Required Permissions: You need the following Microsoft Graph API permissions:
    • Application.Read.All - To read Service Principal and application information
    • Mail.Send - Only if you plan to use the email notification feature (optional)
  3. Appropriate Azure Role: Ensure your account has sufficient permissions to read Service Principal information (e.g., Application Administrator, Cloud Application Administrator, or Global Administrator)

  4. 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.Send permission in addition to Application.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-MailMessage cmdlet is deprecated. Use Microsoft Graph API’s Send-MgUserMail for modern authentication support.

Option 2: Microsoft Teams Webhook (Adaptive Card)

Note: To get your Teams Webhook URL:

  1. In your Teams channel, click the ••• menu
  2. Select “Connectors” or “Workflows”
  3. Add an “Incoming Webhook” connector
  4. 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:

  1. Triggers on a schedule or file upload to Azure Storage
  2. Reads the report data
  3. Sends notifications via email, Teams, or ServiceNow
  4. 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:

  1. Create an Automation Account in the Azure Portal
  2. Import Required Modules: Add Microsoft.Graph.Authentication and Microsoft.Graph.Applications modules from the gallery
  3. Configure Managed Identity: Enable system-assigned or user-assigned managed identity for the Automation Account
  4. 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
    
  5. Create a Runbook with the monitoring script
  6. 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

  1. Regular Monitoring: Run the script at least weekly to catch expiring credentials in time
  2. 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)
  3. Automated Rotation: Consider implementing automated credential rotation for non-critical applications
  4. Audit Trail: Keep historical reports to demonstrate compliance and track credential management
  5. Least Privilege: Use the minimum required permissions (Application.Read.All is read-only)
  6. 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.All permission 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 -AutoSize or 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.

Additional Resources

Comments