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 TypeSecurity LevelRecommended Use
CertificatesHigherProduction workloads, automation
Client SecretsLowerDevelopment, 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

  1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
  2. Go to IdentityApplicationsApp registrations
  3. Click All applications
  4. 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:

IssueSeverityAction Required
Expired credentialsCriticalRemove or rotate immediately
Credentials expiring < 30 daysHighPlan rotation
Secrets older than 1 yearHighRotate and consider certificates
Secrets with 2+ year validityMediumRotate with shorter expiry
No credential rotation policyMediumImplement monitoring
Using secrets instead of certificatesLowPlan migration to certificates

Step 4: Remove Expired Credentials

Via Entra Admin Center

  1. Navigate to App registrations → Select application
  2. Go to Certificates & secrets
  3. Under Client secrets or Certificates, find expired credentials
  4. Click the Delete (trash) icon
  5. 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

  1. Navigate to App registrations → Select application
  2. Go to Certificates & secrets
  3. Click + New client secret
  4. Provide a description (e.g., "Production - Q1 2025")
  5. Select expiration:
    • Recommended: 6 months for production, 3 months for development
    • Maximum: 24 months (avoid using this maximum)
  6. Click Add
  7. Immediately copy the secret value (shown only once)
  8. Store securely in Azure Key Vault or secrets manager

Update Application Configuration

  1. Update the application's configuration with the new secret
  2. Test the application with the new credential
  3. 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

  1. Navigate to App registrations → Select application
  2. Go to Certificates & secrets
  3. Click Certificates tab
  4. Click Upload certificate
  5. Select your .cer file (public key only)
  6. 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

  1. Navigate to Microsoft Entra admin center
  2. Go to IdentityMonitoring & healthWorkbooks
  3. Find App credential expiration workbook
  4. Pin to your dashboard for ongoing visibility

Step 8: Document Credential Rotation Policy

Establish and document these standards:

Credential TypeMaximum ValidityRotation Frequency
Client Secrets6 monthsEvery 90-180 days
Certificates12 monthsEvery 6-12 months
ProductionShorter validityMore frequent rotation
DevelopmentCan be longerAs 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:

  1. Immediately create a new credential
  2. Update application configuration
  3. 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:

  1. First add a new credential
  2. Update all application instances to use the new credential
  3. Wait for the old credential to no longer appear in sign-in logs
  4. 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:

  1. The old secret is still valid; continue using it
  2. Create a new secret and copy it immediately
  3. Update your application with the new secret
  4. Delete the secret you couldn't copy

Issue: Certificate upload fails

Cause: Certificate format may be incorrect or contain private key.

Solution:

  1. Export certificate as .cer file (public key only)
  2. Ensure certificate is X.509 format
  3. Check certificate validity dates
  4. 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:

  1. Managed identities are ideal for Azure-hosted workloads
  2. For on-premises or non-Azure workloads, continue using service principals
  3. Consider Azure Arc for extending managed identity to hybrid scenarios
  4. 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:

  1. Create separate app registrations for each team/workload
  2. Apply principle of least privilege to each
  3. Assign separate owners to each
  4. Migrate workloads to their dedicated service principals
  5. Retire the shared service principal

Related Resources


Last updated: January 2025