APP-05: Service Principal Credential Hygiene
Overview
Service principal credentials (secrets and certificates) are the authentication mechanism for non-interactive applications accessing Microsoft 365 resources. Poor credential hygiene, including long-lived secrets, expired certificates, and lack of rotation, creates significant security risks. This guide helps you audit, remediate, and maintain proper credential hygiene for service principals.
Prerequisites
Required Roles
- Global Administrator - Full access to manage all credentials
- Application Administrator - Can manage credentials for most applications
- Cloud Application Administrator - Can manage credentials (excluding app proxy)
- Application Owner - Can manage credentials for owned applications
Required Licenses
- Microsoft Entra ID (any tier)
- Azure Key Vault (recommended for certificate management)
Time Estimate
- Credential Audit: 30-60 minutes
- Per-Application Remediation: 15-30 minutes
- Ongoing Monitoring Setup: 30 minutes
Step-by-Step Instructions
Step 1: Understand Credential Types
Service principals can use two credential types:
| Credential Type | Security Level | Recommended Use |
|---|---|---|
| Certificates | Higher | Production workloads, automation |
| Client Secrets | Lower | Development, short-term use |
Why Certificates are Preferred
- Cannot be easily copied or shared
- Support for hardware security modules (HSMs)
- Better audit trail
- Stronger cryptographic authentication
Step 2: Audit Existing Credentials
Method A: Entra Admin Center
- Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
- Go to Identity → Applications → App registrations
- Click All applications
- For each application:
- Click on the application
- Go to Certificates & secrets
- Review expiration dates for all credentials
Method B: PowerShell Comprehensive Audit
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All"
# Get all applications with credential information
$apps = Get-MgApplication -All
$credentialReport = @()
$today = Get-Date
foreach ($app in $apps) {
# Check password credentials (secrets)
foreach ($secret in $app.PasswordCredentials) {
$daysUntilExpiry = ($secret.EndDateTime - $today).Days
$credentialReport += [PSCustomObject]@{
ApplicationName = $app.DisplayName
ApplicationId = $app.AppId
CredentialType = "Secret"
KeyId = $secret.KeyId
DisplayName = $secret.DisplayName
StartDate = $secret.StartDateTime
EndDate = $secret.EndDateTime
DaysUntilExpiry = $daysUntilExpiry
Status = if ($daysUntilExpiry -lt 0) { "Expired" }
elseif ($daysUntilExpiry -lt 30) { "Expiring Soon" }
elseif ($daysUntilExpiry -gt 365) { "Long-Lived" }
else { "OK" }
}
}
# Check key credentials (certificates)
foreach ($cert in $app.KeyCredentials) {
$daysUntilExpiry = ($cert.EndDateTime - $today).Days
$credentialReport += [PSCustomObject]@{
ApplicationName = $app.DisplayName
ApplicationId = $app.AppId
CredentialType = "Certificate"
KeyId = $cert.KeyId
DisplayName = $cert.DisplayName
StartDate = $cert.StartDateTime
EndDate = $cert.EndDateTime
DaysUntilExpiry = $daysUntilExpiry
Status = if ($daysUntilExpiry -lt 0) { "Expired" }
elseif ($daysUntilExpiry -lt 30) { "Expiring Soon" }
elseif ($daysUntilExpiry -gt 730) { "Long-Lived" }
else { "OK" }
}
}
}
# Display summary
Write-Host "`nCredential Summary:" -ForegroundColor Cyan
Write-Host "==================="
Write-Host "Expired: $($credentialReport | Where-Object Status -eq 'Expired' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "Expiring Soon (< 30 days): $($credentialReport | Where-Object Status -eq 'Expiring Soon' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "Long-Lived: $($credentialReport | Where-Object Status -eq 'Long-Lived' | Measure-Object | Select-Object -ExpandProperty Count)"
Write-Host "OK: $($credentialReport | Where-Object Status -eq 'OK' | Measure-Object | Select-Object -ExpandProperty Count)"
# Export to CSV
$credentialReport | Export-Csv -Path "CredentialAudit.csv" -NoTypeInformation
Write-Host "`nReport exported to CredentialAudit.csv"
Step 3: Identify Credential Issues
Prioritize remediation based on severity:
| Issue | Severity | Action Required |
|---|---|---|
| Expired credentials | Critical | Remove or rotate immediately |
| Credentials expiring < 30 days | High | Plan rotation |
| Secrets older than 1 year | High | Rotate and consider certificates |
| Secrets with 2+ year validity | Medium | Rotate with shorter expiry |
| No credential rotation policy | Medium | Implement monitoring |
| Using secrets instead of certificates | Low | Plan migration to certificates |
Step 4: Remove Expired Credentials
Via Entra Admin Center
- Navigate to App registrations → Select application
- Go to Certificates & secrets
- Under Client secrets or Certificates, find expired credentials
- Click the Delete (trash) icon
- Confirm deletion
Via PowerShell
Connect-MgGraph -Scopes "Application.ReadWrite.All"
# Remove expired secret
$appId = "your-app-object-id"
$keyId = "expired-key-id"
Remove-MgApplicationPassword -ApplicationId $appId -KeyId $keyId
Write-Host "Expired credential removed"
Step 5: Rotate Client Secrets
Generate New Secret with Appropriate Expiry
- Navigate to App registrations → Select application
- Go to Certificates & secrets
- Click + New client secret
- Provide a description (e.g., "Production - Q1 2025")
- Select expiration:
- Recommended: 6 months for production, 3 months for development
- Maximum: 24 months (avoid using this maximum)
- Click Add
- Immediately copy the secret value (shown only once)
- Store securely in Azure Key Vault or secrets manager
Update Application Configuration
- Update the application's configuration with the new secret
- Test the application with the new credential
- Once verified, delete the old secret
PowerShell Secret Rotation
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$appObjectId = "your-app-object-id"
# Create new secret with 180-day expiry
$passwordCredential = @{
displayName = "Rotated $(Get-Date -Format 'yyyy-MM-dd')"
endDateTime = (Get-Date).AddDays(180)
}
$newSecret = Add-MgApplicationPassword -ApplicationId $appObjectId -PasswordCredential $passwordCredential
Write-Host "New secret created. Value: $($newSecret.SecretText)"
Write-Host "Expires: $($newSecret.EndDateTime)"
Write-Host "`n** Store this secret immediately - it cannot be retrieved later **"
Step 6: Migrate to Certificate-Based Authentication
Option A: Generate Self-Signed Certificate (Development/Testing)
# Generate self-signed certificate
$certName = "MyApp-Auth-Cert"
$cert = New-SelfSignedCertificate `
-Subject "CN=$certName" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-KeySpec Signature `
-KeyLength 2048 `
-KeyAlgorithm RSA `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(1)
# Export public key (for Entra ID)
$certPath = ".\$certName.cer"
Export-Certificate -Cert $cert -FilePath $certPath
# Export with private key (for application - store securely!)
$pfxPath = ".\$certName.pfx"
$password = ConvertTo-SecureString -String "YourSecurePassword" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $password
Write-Host "Certificate created: $($cert.Thumbprint)"
Write-Host "Public key: $certPath"
Write-Host "Private key (PFX): $pfxPath"
Option B: Use Azure Key Vault (Recommended for Production)
# Create certificate in Key Vault
$vaultName = "your-keyvault-name"
$certName = "app-auth-cert"
$policy = New-AzKeyVaultCertificatePolicy `
-SubjectName "CN=MyApp-Auth" `
-IssuerName "Self" `
-ValidityInMonths 12 `
-ReuseKeyOnRenewal
Add-AzKeyVaultCertificate -VaultName $vaultName -Name $certName -CertificatePolicy $policy
Upload Certificate to App Registration
- Navigate to App registrations → Select application
- Go to Certificates & secrets
- Click Certificates tab
- Click Upload certificate
- Select your
.cerfile (public key only) - Click Add
PowerShell Certificate Upload
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$appObjectId = "your-app-object-id"
$certPath = ".\MyCert.cer"
# Read certificate
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath)
$certBase64 = [System.Convert]::ToBase64String($cert.RawData)
# Add to application
$keyCredential = @{
type = "AsymmetricX509Cert"
usage = "Verify"
key = [System.Text.Encoding]::UTF8.GetBytes($certBase64)
displayName = $cert.Subject
endDateTime = $cert.NotAfter
startDateTime = $cert.NotBefore
}
Update-MgApplication -ApplicationId $appObjectId -KeyCredentials @($keyCredential)
Write-Host "Certificate uploaded successfully"
Step 7: Implement Credential Monitoring
Set Up Expiration Alerts
Create an Azure Logic App or PowerShell scheduled task:
# Run weekly via scheduled task or Azure Automation
Connect-MgGraph -Scopes "Application.Read.All"
$apps = Get-MgApplication -All
$warningThreshold = 30 # days
$expiringCreds = @()
foreach ($app in $apps) {
foreach ($secret in $app.PasswordCredentials) {
$daysLeft = ($secret.EndDateTime - (Get-Date)).Days
if ($daysLeft -gt 0 -and $daysLeft -le $warningThreshold) {
$expiringCreds += [PSCustomObject]@{
App = $app.DisplayName
Type = "Secret"
DaysLeft = $daysLeft
Expiry = $secret.EndDateTime
}
}
}
foreach ($cert in $app.KeyCredentials) {
$daysLeft = ($cert.EndDateTime - (Get-Date)).Days
if ($daysLeft -gt 0 -and $daysLeft -le $warningThreshold) {
$expiringCreds += [PSCustomObject]@{
App = $app.DisplayName
Type = "Certificate"
DaysLeft = $daysLeft
Expiry = $cert.EndDateTime
}
}
}
}
if ($expiringCreds.Count -gt 0) {
# Send notification
$body = $expiringCreds | ConvertTo-Html -Fragment
Send-MailMessage -To "it-security@company.com" `
-Subject "Credential Expiration Warning" `
-Body $body -BodyAsHtml
}
Enable Workbook Reporting
- Navigate to Microsoft Entra admin center
- Go to Identity → Monitoring & health → Workbooks
- Find App credential expiration workbook
- Pin to your dashboard for ongoing visibility
Step 8: Document Credential Rotation Policy
Establish and document these standards:
| Credential Type | Maximum Validity | Rotation Frequency |
|---|---|---|
| Client Secrets | 6 months | Every 90-180 days |
| Certificates | 12 months | Every 6-12 months |
| Production | Shorter validity | More frequent rotation |
| Development | Can be longer | As needed |
Verification Checklist
After completing credential remediation:
- All expired credentials have been removed
- Credentials expiring within 30 days are scheduled for rotation
- Long-lived secrets (>1 year) have been rotated
- Critical applications use certificate authentication
- New secrets use appropriate expiry (6 months or less)
- Secrets are stored in Azure Key Vault or approved secrets manager
- Automated expiration monitoring is configured
- Credential rotation procedures are documented
- Application owners are aware of rotation schedules
- Rotation SOP is included in runbooks
Troubleshooting
Issue: Application breaks after credential rotation
Cause: Old credential was removed before new one was configured.
Solution:
- Immediately create a new credential
- Update application configuration
- Follow the "rotate then remove" pattern:
- Add new credential
- Update application to use new credential
- Verify application works
- Remove old credential
Issue: Cannot delete credential - "In use" error
Cause: The credential may still be actively used by the application.
Solution:
- First add a new credential
- Update all application instances to use the new credential
- Wait for the old credential to no longer appear in sign-in logs
- Then delete the old credential
Issue: Secret value was not copied before navigating away
Cause: Client secret values are only shown once at creation.
Solution:
- The old secret is still valid; continue using it
- Create a new secret and copy it immediately
- Update your application with the new secret
- Delete the secret you couldn't copy
Issue: Certificate upload fails
Cause: Certificate format may be incorrect or contain private key.
Solution:
- Export certificate as
.cerfile (public key only) - Ensure certificate is X.509 format
- Check certificate validity dates
- Verify certificate is not corrupted:
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(".\cert.cer") $cert | Format-List Subject, Issuer, NotBefore, NotAfter
Issue: Managed identity is better but cannot convert
Cause: Application architecture may not support managed identities.
Solution:
- Managed identities are ideal for Azure-hosted workloads
- For on-premises or non-Azure workloads, continue using service principals
- Consider Azure Arc for extending managed identity to hybrid scenarios
- Document why managed identity cannot be used
Issue: Multiple teams use the same service principal
Cause: Service principal credentials were shared instead of creating separate apps.
Solution:
- Create separate app registrations for each team/workload
- Apply principle of least privilege to each
- Assign separate owners to each
- Migrate workloads to their dedicated service principals
- Retire the shared service principal
Related Resources
- Manage app credentials
- Best practices for credential management
- Azure Key Vault for secrets
- Certificate-based authentication
Last updated: January 2025