Microsoft Azureintermediate

Azure Service Principal Credential Rotation: Security Best Practices

Learn how to rotate Azure service principal secrets and certificates, implement automated rotation, migrate to managed identities, and maintain zero-downtime credential updates.

15 min readUpdated 2026-01-13

Service principals are identity objects in Azure Active Directory (Microsoft Entra ID) that applications use to authenticate and access Azure resources. Unlike user accounts, service principals use credentials (secrets or certificates) that can be easily leaked, exposed in code, or compromised. Regular credential rotation is essential for maintaining security. This guide covers rotation strategies, automation, and the transition to managed identities.

This article is part of our comprehensive cloud security tips guide, focusing specifically on Azure service principal security.

Overview

Service principal security requires attention to:

  • Credential types: Understanding secrets vs. certificates
  • Rotation frequency: Balancing security with operational burden
  • Zero-downtime rotation: Updating credentials without service interruption
  • Managed identities: Eliminating credential management where possible
  • Monitoring: Detecting credential misuse and expiration

Prerequisites

Before implementing credential rotation, ensure you have:

  • Application Administrator or Cloud Application Administrator role in Azure AD
  • Azure CLI (2.50.0+) with signed-in session
  • Access to applications that use the service principal
  • Understanding of application deployment processes
  • Key Vault access (for secure credential storage)

Part 1: Inventory Existing Service Principals

List All Service Principals

# List all service principals in your tenant
az ad sp list \
  --all \
  --query "[?servicePrincipalType=='Application'].{Name:displayName, AppId:appId, ObjectId:id}" \
  --output table

# Filter to find service principals with expiring credentials
az ad sp list \
  --all \
  --query "[?servicePrincipalType=='Application']" | \
  jq -r '.[] | select(.passwordCredentials != null) |
    {name: .displayName, appId: .appId,
     credentials: [.passwordCredentials[] | {id: .keyId, expiry: .endDateTime}]}'

Check Credential Expiration

# Get credentials for a specific service principal
az ad sp credential list \
  --id "your-service-principal-app-id" \
  --query "[].{KeyId:keyId, Type:type, StartDate:startDateTime, EndDate:endDateTime}" \
  --output table

# Find credentials expiring within 30 days
EXPIRY_DATE=$(date -d "+30 days" +%Y-%m-%d)

az ad sp list --all --query "[?servicePrincipalType=='Application']" | \
  jq -r --arg expiry "$EXPIRY_DATE" '.[] |
    select(.passwordCredentials != null) |
    select(.passwordCredentials[] | .endDateTime < $expiry) |
    {name: .displayName, appId: .appId, expiringCreds: [.passwordCredentials[] | select(.endDateTime < $expiry) | .endDateTime]}'

Using PowerShell

# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All"

# Get all app registrations with credential expiry info
$apps = Get-MgApplication -All

$expiringCredentials = foreach ($app in $apps) {
    $expiringSoon = $app.PasswordCredentials | Where-Object {
        $_.EndDateTime -lt (Get-Date).AddDays(30)
    }

    if ($expiringSoon) {
        [PSCustomObject]@{
            AppName = $app.DisplayName
            AppId = $app.AppId
            ExpiringCredentials = $expiringSoon | ForEach-Object {
                [PSCustomObject]@{
                    KeyId = $_.KeyId
                    EndDate = $_.EndDateTime
                    DaysRemaining = ($_.EndDateTime - (Get-Date)).Days
                }
            }
        }
    }
}

$expiringCredentials | Format-Table -AutoSize

Part 2: Rotate Client Secrets

Manual Secret Rotation

Step 1: Add New Secret (Keep Old Active)

# Add new secret with 90-day expiry
NEW_SECRET=$(az ad sp credential reset \
  --id "your-service-principal-app-id" \
  --append \
  --years 0 \
  --end-date $(date -d "+90 days" +%Y-%m-%d) \
  --query password -o tsv)

echo "New secret: $NEW_SECRET"
# IMPORTANT: Store this securely immediately - it won't be shown again

Step 2: Store New Secret in Key Vault

# Store in Azure Key Vault
az keyvault secret set \
  --vault-name "kv-app-secrets" \
  --name "sp-myapp-secret" \
  --value "$NEW_SECRET" \
  --expires $(date -d "+90 days" +%Y-%m-%dT%H:%M:%SZ)

# Add metadata tags
az keyvault secret set-attributes \
  --vault-name "kv-app-secrets" \
  --name "sp-myapp-secret" \
  --tags "service-principal=myapp" "rotation-date=$(date +%Y-%m-%d)"

Step 3: Update Applications

Update your applications to use the new secret from Key Vault:

# Python example using Azure SDK
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient

# Fetch secret from Key Vault
kv_uri = "https://kv-app-secrets.vault.azure.net/"
secret_client = SecretClient(vault_url=kv_uri, credential=DefaultAzureCredential())
sp_secret = secret_client.get_secret("sp-myapp-secret").value

# Use the secret for service principal authentication
credential = ClientSecretCredential(
    tenant_id="your-tenant-id",
    client_id="your-service-principal-app-id",
    client_secret=sp_secret
)

Step 4: Verify and Remove Old Secret

# List all credentials
az ad sp credential list \
  --id "your-service-principal-app-id" \
  --output table

# After confirming new secret works, remove old credential
az ad sp credential delete \
  --id "your-service-principal-app-id" \
  --key-id "old-credential-key-id"

Automated Secret Rotation with Azure Automation

# Azure Automation Runbook for secret rotation
param(
    [Parameter(Mandatory=$true)]
    [string]$ServicePrincipalAppId,

    [Parameter(Mandatory=$true)]
    [string]$KeyVaultName,

    [Parameter(Mandatory=$true)]
    [string]$SecretName,

    [int]$ExpiryDays = 90
)

# Connect using managed identity
Connect-AzAccount -Identity

# Connect to Microsoft Graph
Connect-MgGraph -Identity

# Generate new secret
$endDate = (Get-Date).AddDays($ExpiryDays)
$passwordCredential = @{
    displayName = "AutoRotated-$(Get-Date -Format 'yyyyMMdd')"
    endDateTime = $endDate
}

$newCredential = Add-MgApplicationPassword -ApplicationId $ServicePrincipalAppId -PasswordCredential $passwordCredential

# Store in Key Vault
$secret = ConvertTo-SecureString $newCredential.SecretText -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -SecretValue $secret -Expires $endDate

Write-Output "New secret created and stored in Key Vault"
Write-Output "Secret expires: $endDate"
Write-Output "Key ID: $($newCredential.KeyId)"

# Clean up old credentials (keep last 2)
$allCredentials = Get-MgApplication -ApplicationId $ServicePrincipalAppId |
    Select-Object -ExpandProperty PasswordCredentials |
    Sort-Object EndDateTime -Descending

if ($allCredentials.Count -gt 2) {
    $credentialsToRemove = $allCredentials | Select-Object -Skip 2
    foreach ($cred in $credentialsToRemove) {
        Remove-MgApplicationPassword -ApplicationId $ServicePrincipalAppId -KeyId $cred.KeyId
        Write-Output "Removed old credential: $($cred.KeyId)"
    }
}

Part 3: Rotate Certificates

Certificates provide stronger security than secrets.

Generate and Upload New Certificate

Step 1: Generate Certificate

# Generate self-signed certificate (for dev/test)
openssl req -x509 \
  -newkey rsa:2048 \
  -keyout sp-key.pem \
  -out sp-cert.pem \
  -days 365 \
  -nodes \
  -subj "/CN=myapp-service-principal"

# Create PFX for Windows applications
openssl pkcs12 -export \
  -out sp-cert.pfx \
  -inkey sp-key.pem \
  -in sp-cert.pem \
  -passout pass:

# For production, use Azure Key Vault to generate certificates
az keyvault certificate create \
  --vault-name "kv-app-certs" \
  --name "sp-myapp-cert" \
  --policy @cert-policy.json

cert-policy.json:

{
  "issuerParameters": {
    "name": "Self"
  },
  "keyProperties": {
    "exportable": true,
    "keySize": 2048,
    "keyType": "RSA",
    "reuseKey": false
  },
  "secretProperties": {
    "contentType": "application/x-pkcs12"
  },
  "x509CertificateProperties": {
    "keyUsage": ["digitalSignature", "keyEncipherment"],
    "subject": "CN=myapp-service-principal",
    "validityInMonths": 12
  },
  "lifetimeActions": [
    {
      "trigger": {
        "daysBeforeExpiry": 30
      },
      "action": {
        "actionType": "AutoRenew"
      }
    }
  ]
}

Step 2: Upload Certificate to Service Principal

# Upload certificate (append to existing)
az ad sp credential reset \
  --id "your-service-principal-app-id" \
  --cert @sp-cert.pem \
  --append

# Or upload from Key Vault
CERT_VALUE=$(az keyvault certificate show \
  --vault-name "kv-app-certs" \
  --name "sp-myapp-cert" \
  --query "cer" -o tsv)

az ad sp credential reset \
  --id "your-service-principal-app-id" \
  --cert "$CERT_VALUE" \
  --append

Step 3: Update Application Configuration

# Python example using certificate authentication
from azure.identity import CertificateCredential

credential = CertificateCredential(
    tenant_id="your-tenant-id",
    client_id="your-service-principal-app-id",
    certificate_path="/path/to/sp-cert.pem"
)

# Or with PFX
credential = CertificateCredential(
    tenant_id="your-tenant-id",
    client_id="your-service-principal-app-id",
    certificate_path="/path/to/sp-cert.pfx",
    password="pfx-password"  # If password-protected
)

Part 4: Migrate to Managed Identities

Managed identities eliminate credential management entirely.

System-Assigned Managed Identity

# Enable on Azure VM
az vm identity assign \
  --name myvm \
  --resource-group rg-security

# Enable on App Service
az webapp identity assign \
  --name mywebapp \
  --resource-group rg-security

# Enable on Azure Function
az functionapp identity assign \
  --name myfunctionapp \
  --resource-group rg-security

# Get the principal ID
PRINCIPAL_ID=$(az vm identity show \
  --name myvm \
  --resource-group rg-security \
  --query principalId -o tsv)

# Grant permissions using RBAC
az role assignment create \
  --role "Storage Blob Data Contributor" \
  --assignee-object-id $PRINCIPAL_ID \
  --scope "/subscriptions/{sub-id}/resourceGroups/rg-security/providers/Microsoft.Storage/storageAccounts/mystorageaccount"

User-Assigned Managed Identity

For sharing identity across multiple resources:

# Create user-assigned managed identity
az identity create \
  --name mi-shared-app-identity \
  --resource-group rg-security \
  --location eastus

# Get identity details
MI_ID=$(az identity show \
  --name mi-shared-app-identity \
  --resource-group rg-security \
  --query id -o tsv)

MI_CLIENT_ID=$(az identity show \
  --name mi-shared-app-identity \
  --resource-group rg-security \
  --query clientId -o tsv)

MI_PRINCIPAL_ID=$(az identity show \
  --name mi-shared-app-identity \
  --resource-group rg-security \
  --query principalId -o tsv)

# Assign to multiple resources
az vm identity assign \
  --name myvm1 \
  --resource-group rg-security \
  --identities $MI_ID

az webapp identity assign \
  --name mywebapp \
  --resource-group rg-security \
  --identities $MI_ID

Update Application Code

# Python SDK with managed identity (no credentials needed!)
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient

# DefaultAzureCredential automatically uses managed identity when running in Azure
credential = DefaultAzureCredential()

blob_service = BlobServiceClient(
    account_url="https://mystorageaccount.blob.core.windows.net",
    credential=credential
)

# Works seamlessly - no secrets to manage!
container_client = blob_service.get_container_client("mycontainer")

Part 5: Monitoring and Alerts

Monitor Service Principal Sign-ins

// Azure AD Sign-in logs for service principals
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(24h)
| where ResultType != 0  // Failed sign-ins
| project TimeGenerated, ServicePrincipalName, AppId, IPAddress, Location, ResultType, ResultDescription
| order by TimeGenerated desc

Alert on Credential Expiration

# Create alert for expiring credentials
az monitor scheduled-query create \
  --name "ServicePrincipalCredentialExpiry" \
  --resource-group rg-security \
  --scopes "/subscriptions/{subscription-id}" \
  --condition "count > 0" \
  --condition-query "
    AuditLogs
    | where TimeGenerated > ago(1d)
    | where OperationName == 'Update application - Certificates and secrets management'
    | where Result == 'success'
  " \
  --description "Alert when service principal credentials are modified" \
  --evaluation-frequency "1h" \
  --window-size "1h" \
  --severity 2 \
  --action-groups "/subscriptions/{sub-id}/resourceGroups/rg-security/providers/microsoft.insights/actionGroups/SecurityAlerts"

PowerShell Script for Expiry Report

# Generate credential expiry report
Connect-MgGraph -Scopes "Application.Read.All"

$report = Get-MgApplication -All | ForEach-Object {
    $app = $_

    $passwordCreds = $app.PasswordCredentials | ForEach-Object {
        [PSCustomObject]@{
            AppName = $app.DisplayName
            AppId = $app.AppId
            CredentialType = "Secret"
            KeyId = $_.KeyId
            EndDate = $_.EndDateTime
            DaysRemaining = [math]::Round(($_.EndDateTime - (Get-Date)).TotalDays)
            Status = if ($_.EndDateTime -lt (Get-Date)) { "EXPIRED" }
                     elseif ($_.EndDateTime -lt (Get-Date).AddDays(30)) { "EXPIRING SOON" }
                     else { "OK" }
        }
    }

    $keyCreds = $app.KeyCredentials | ForEach-Object {
        [PSCustomObject]@{
            AppName = $app.DisplayName
            AppId = $app.AppId
            CredentialType = "Certificate"
            KeyId = $_.KeyId
            EndDate = $_.EndDateTime
            DaysRemaining = [math]::Round(($_.EndDateTime - (Get-Date)).TotalDays)
            Status = if ($_.EndDateTime -lt (Get-Date)) { "EXPIRED" }
                     elseif ($_.EndDateTime -lt (Get-Date).AddDays(30)) { "EXPIRING SOON" }
                     else { "OK" }
        }
    }

    $passwordCreds + $keyCreds
}

# Export report
$report | Where-Object { $_.Status -ne "OK" } |
    Export-Csv -Path "credential-expiry-report.csv" -NoTypeInformation

# Display summary
$report | Group-Object Status | Select-Object Name, Count

Terraform Configuration

terraform {
  required_providers {
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.47"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.85"
    }
  }
}

# Application registration
resource "azuread_application" "app" {
  display_name = "my-secure-application"
}

# Service principal
resource "azuread_service_principal" "sp" {
  client_id = azuread_application.app.client_id
}

# Certificate-based credential (recommended)
resource "azuread_application_certificate" "cert" {
  application_id = azuread_application.app.id
  type           = "AsymmetricX509Cert"
  value          = filebase64("path/to/certificate.cer")
  end_date       = timeadd(timestamp(), "8760h") # 1 year
}

# Secret-based credential (with rotation)
resource "azuread_application_password" "secret" {
  application_id = azuread_application.app.id
  display_name   = "Terraform-managed-${formatdate("YYYY-MM-DD", timestamp())}"
  end_date       = timeadd(timestamp(), "2160h") # 90 days

  rotate_when_changed = {
    rotation = time_rotating.secret_rotation.id
  }
}

# Time-based rotation trigger
resource "time_rotating" "secret_rotation" {
  rotation_days = 85 # Rotate 5 days before expiry
}

# Store secret in Key Vault
resource "azurerm_key_vault_secret" "sp_secret" {
  name         = "sp-${azuread_application.app.display_name}-secret"
  value        = azuread_application_password.secret.value
  key_vault_id = azurerm_key_vault.main.id

  expiration_date = azuread_application_password.secret.end_date

  tags = {
    service-principal = azuread_application.app.display_name
    managed-by        = "terraform"
  }
}

# User-assigned managed identity (preferred over service principals)
resource "azurerm_user_assigned_identity" "app_identity" {
  name                = "mi-${var.app_name}"
  location            = var.location
  resource_group_name = var.resource_group_name
}

# Assign RBAC role
resource "azurerm_role_assignment" "storage_access" {
  scope                = azurerm_storage_account.main.id
  role_definition_name = "Storage Blob Data Contributor"
  principal_id         = azurerm_user_assigned_identity.app_identity.principal_id
}

Best Practices Summary

  1. Use managed identities whenever the Azure resource supports them
  2. Prefer certificates over secrets for service principal authentication
  3. Rotate secrets every 90 days or less
  4. Store credentials in Key Vault, never in code or config files
  5. Use separate service principals for different environments (dev, staging, prod)
  6. Implement least privilege - grant minimum required permissions
  7. Monitor sign-in logs for unusual activity
  8. Automate rotation to ensure consistency and reduce human error
  9. Maintain overlap - always have two valid credentials during rotation

Troubleshooting

Issue: Application Fails After Credential Rotation

# Verify the credential is properly configured
az ad sp credential list --id "app-id" --output table

# Check if app is using correct credential
# Review application logs for authentication errors

# Test authentication with new credential
az login --service-principal \
  --username "app-id" \
  --password "new-secret" \
  --tenant "tenant-id"

Issue: Managed Identity Not Working

# Verify managed identity is enabled
az vm identity show --name myvm --resource-group rg-security

# Check RBAC assignments
az role assignment list \
  --assignee "managed-identity-principal-id" \
  --all \
  --output table

# Test from within the VM
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

Frequently Asked Questions

Find answers to common questions

Microsoft recommends rotating service principal secrets at least every 90 days, with certificates rotated annually. However, for highly sensitive environments, consider more frequent rotation (30-60 days for secrets). The key is balancing security with operational overhead. Automated rotation helps maintain short credential lifetimes without manual burden.

Azure Infrastructure Experts

Comprehensive Azure management including architecture, migration, security, and 24/7 operations.