Infrastructure

Azure Managed Identity : diagnostiquer une dérive de principal après recréation

Un runbook de production pour prouver qu'une ressource Azure recréée porte un nouveau principalId, retrouver les rôles orphelins et restaurer le moindre privilège sans élargir RBAC.

12 sept. 2026 azuremanaged-identityentra-idrbacidentitykey-vaultiacobservabilitysecurityautomationrunbookrollbackproduction

Une application Azure est redéployée sous le même nom, démarre correctement, puis reçoit des 403 sur Key Vault, Storage ou une API interne. Le code, le hostname et les paramètres semblent identiques. L’équipe propose alors de réappliquer Contributor au niveau du resource group. Pourtant, la ressource peut avoir conservé son nom tout en changeant d’identité.

Le cas fil rouge est une App Service app-orders-prod recréée par l’IaC après un remplacement. Son identité managée assignée par le système a été supprimée avec l’ancienne ressource, puis un nouveau service principal a été créé. Les role assignments visent encore l’ancien principalId. Ce runbook doit mener à une décision bornée : restaurer les rôles minimaux sur le nouveau principal, revenir à la ressource précédente si elle existe encore, ou migrer vers une identité user-assigned lorsque le cycle de vie doit être indépendant du compute.

Figer le contrat d’identité

Commencez par identifier le consommateur, la cible et l’autorisation attendue. Un 403 ne dit pas si le token est absent, émis pour le mauvais principal, accepté puis insuffisamment autorisé, ou refusé par une condition réseau.

yaml managed-identity-drift-incident.yml
incident: inc-20260912-004
consumer:
resource: app-orders-prod
resource_id: /subscriptions/<sub>/resourceGroups/rg-orders-prod/providers/Microsoft.Web/sites/app-orders-prod
identity_type: SystemAssigned
deployment: orders-infra-2026.09.12.2
target:
resource: kv-orders-prod
operation: secrets/get
expected_role: Key Vault Secrets User
expected_scope: /subscriptions/<sub>/resourceGroups/rg-security-prod/providers/Microsoft.KeyVault/vaults/kv-orders-prod
symptom:
first_seen_utc: 2026-09-12T05:42:00Z
status: 403
correlation_id: <request-or-correlation-id>
preserve:
- previous_resource_identity_principal_id
- current_resource_identity_principal_id
- role_assignments_for_both_principals
- deployment_and_activity_timeline
- target_data_plane_logs
- last_known_good_iac_revision

Ne corrigez pas encore les rôles. La première preuve recherchée est un changement de principal, pas une absence générique de permission.

Comparer l’identité de la ressource, pas son nom

Une identité assignée par le système partage le cycle de vie de sa ressource. Supprimer puis recréer cette ressource produit une nouvelle identité, même si le nom Azure et le resource ID textuel sont réutilisés. Les rôles sont attachés au principalId, pas au nom de l’App Service.

bash 01-current-principal-and-deployment.sh
set -eu

RG="rg-orders-prod"
APP="app-orders-prod"
RESOURCE_ID=$(az webapp show -g "$RG" -n "$APP" --query id -o tsv)

az webapp identity show --resource-group "$RG" --name "$APP" --query '{type:type,principalId:principalId,tenantId:tenantId,userAssignedIdentities:userAssignedIdentities}' --output json

az deployment group list --resource-group "$RG" --query '[0:10].{name:name,state:properties.provisioningState,time:properties.timestamp}' --output table

az monitor activity-log list --resource-id "$RESOURCE_ID" --start-time 2026-09-12T04:30:00Z --query '[].{time:eventTimestamp,operation:operationName.value,status:status.value,caller:caller,correlationId:correlationId}' --output table

Récupérez l’ancien principalId depuis la sortie du déploiement précédent, l’état IaC, l’inventaire d’identités ou le dossier de changement. Ne choisissez pas un objet Entra uniquement parce que son display name correspond au nom de la ressource : plusieurs objets supprimés ou actifs peuvent produire une piste trompeuse.

Prouver la dérive dans les role assignments

Comparez les affectations directes des deux identifiants. --assignee-object-id avec la résolution des noms désactivée permet de travailler sur l’identifiant connu sans dépendre d’un objet Entra encore résolvable.

bash 02-compare-role-assignments.sh
set -eu

OLD_PRINCIPAL_ID="<old-principal-id>"
NEW_PRINCIPAL_ID="<new-principal-id>"

for PRINCIPAL_ID in "$OLD_PRINCIPAL_ID" "$NEW_PRINCIPAL_ID"; do
echo "principal=$PRINCIPAL_ID"
az role assignment list   --all   --assignee-object-id "$PRINCIPAL_ID"   --fill-principal-name false   --query '[].{role:roleDefinitionName,roleId:roleDefinitionId,scope:scope,assignmentId:id}'   --output table
done

Le diagnostic est solide lorsque l’ancien principal porte le rôle attendu au scope attendu, que le nouveau ne le porte pas, et que la recréation précède immédiatement les refus. Une affectation affichée comme Identity not found n’est pas une autorisation transférable : elle continue de référencer l’ancien identifiant.

Séparer token, autorisation et réseau

Le changement de principalId ne doit pas devenir une explication universelle. Confirmez que le workload obtient un token, puis que la cible voit le nouveau principal. Sur Key Vault en mode Azure RBAC, les logs data plane permettent de rapprocher l’identité appelante, l’opération et le résultat. En mode access policies, la réparation se fait dans la policy du coffre, pas dans un role assignment Azure.

kusto 03-key-vault-principal-correlation.kql
let StartTime = datetime(2026-09-12T05:30:00Z);
let EndTime = datetime(2026-09-12T06:30:00Z);
let VaultName = "kv-orders-prod";
AzureDiagnostics
| where TimeGenerated between (StartTime .. EndTime)
| where ResourceProvider == "MICROSOFT.KEYVAULT"
| where Resource =~ VaultName
| where ResultSignature in ("403", "Forbidden") or ResultType =~ "Forbidden"
| extend CallerObjectId = tostring(column_ifexists("identity_claim_oid_g", ""))
| project TimeGenerated, OperationName, ResultSignature, CallerObjectId,
        CallerIPAddress, CorrelationId
| order by TimeGenerated asc

Le nom des colonnes dépend du mode de collecte et de la table utilisée. Inspectez une ligne brute avant d’adapter la requête. Si aucun appel n’atteint le coffre, revenez au token, au DNS, au firewall ou au chemin privé ; créer un rôle ne corrigera pas une requête qui n’arrive jamais à la cible.

Restaurer uniquement le contrat prouvé

Ne recopiez pas tous les rôles de l’ancien principal. Certains peuvent être historiques, trop larges ou destinés à une fonction qui n’existe plus. Recréez seulement le tuple validé : nouveau principal, définition de rôle et scope exact.

bash 04-restore-minimal-role.sh
set -eu

NEW_PRINCIPAL_ID="<new-principal-id>"
TARGET_SCOPE="/subscriptions/<sub>/resourceGroups/rg-security-prod/providers/Microsoft.KeyVault/vaults/kv-orders-prod"
ROLE="Key Vault Secrets User"

az role assignment create --assignee-object-id "$NEW_PRINCIPAL_ID" --assignee-principal-type ServicePrincipal --role "$ROLE" --scope "$TARGET_SCOPE"

az role assignment list --assignee-object-id "$NEW_PRINCIPAL_ID" --scope "$TARGET_SCOPE" --include-inherited --fill-principal-name false --query '[].{role:roleDefinitionName,scope:scope,principalId:principalId}' --output table

Dans l’IaC, dérivez l’affectation du principalId réellement produit par la ressource ou l’identité, et gardez un nom de role assignment déterministe incluant scope, rôle et principal. Évitez un identifiant figé copié depuis une ancienne exécution : il transforme chaque remplacement en panne différée.

bicep managed-identity-role-assignment.bicep
param targetScopeId string
param roleDefinitionId string

resource app 'Microsoft.Web/sites@2023-12-01' existing = {
name: 'app-orders-prod'
}

resource target 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: 'kv-orders-prod'
scope: resourceGroup('rg-security-prod')
}

resource access 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(targetScopeId, roleDefinitionId, app.identity.principalId)
scope: target
properties: {
  principalId: app.identity.principalId
  principalType: 'ServicePrincipal'
  roleDefinitionId: roleDefinitionId
}
}

Adaptez les versions d’API et les scopes à votre dépôt IaC. L’objectif est le lien de dépendance explicite, pas le copier-coller du fragment.

Décider entre identité système et user-assigned

Le rollback ne consiste pas à réattacher arbitrairement l’ancien objet. Une identité assignée par le système supprimée avec sa ressource n’est pas un composant indépendant à restaurer. La décision dépend du cycle de vie recherché.

text identity-lifecycle-decision.txt
Conserver l'identité system-assigned
La ressource et ses droits doivent disparaître ensemble
Les remplacements sont rares et l'IaC recrée automatiquement les rôles
Le principal courant est injecté sans valeur figée

Passer à une identité user-assigned
La ressource est recréée ou permutée régulièrement
Les permissions doivent survivre au remplacement du compute
Plusieurs slots ou ressources partagent volontairement le même contrat
L'association de l'identité reste séparée de l'administration de ses rôles

Rollbacker le déploiement
La recréation n'était pas prévue
D'autres états non migrés ont été perdus
L'ancienne ressource existe encore et son retour est testé

Bloquer la restauration des rôles
L'ancien principal ou le scope attendu ne peut pas être prouvé
Le nouveau workload demande plus de droits que l'ancien
Aucun log cible ne relie le 403 au nouveau principal

Une identité user-assigned stabilise le principal, mais augmente aussi la durée de vie de ses autorisations. Elle exige un propriétaire, un inventaire des ressources attachées et une suppression explicite quand le contrat disparaît.

Valider puis nettoyer les affectations orphelines

Attendez la propagation RBAC, obtenez un nouveau token et rejouez une opération de lecture bornée. La validation doit couvrir la cible réelle et une action négative : le nouveau principal lit le secret prévu, mais ne peut pas l’écrire ni lire un autre coffre.

text principal-drift-validation.txt
Valider
Le workload obtient un nouveau token après la correction
Les logs cible montrent le nouveau principalId
L'opération attendue réussit au scope exact
Une opération hors contrat reste refusée
Les probes applicatives reviennent au vert
L'IaC ne propose pas d'élargissement supplémentaire au plan suivant

Nettoyer après validation
Inventorier les role assignments qui visent l'ancien principalId
Vérifier qu'aucun token ou workload actif ne l'utilise encore
Supprimer les affectations orphelines par ID exact
Documenter ancien principal, nouveau principal, cause et preuve

Rollbacker ou suspendre
Le 403 persiste avec le nouveau rôle
Le caller observé n'est pas le nouveau principal
Le réseau ou le mode d'autorisation explique réellement le refus
La migration vers user-assigned change un contrat non validé

Ne lancez pas une suppression globale des identités inconnues pendant l’incident. Nettoyez par assignment ID après validation et revue du scope.

Conclusion

Le nom d’une ressource Azure n’est pas son identité de sécurité. Après une recréation, comparez les principalId, prouvez les anciens et nouveaux role assignments, puis confirmez le caller dans les logs de la cible avant toute correction.

La bonne sortie est explicite : restaurer un rôle minimal sur le nouveau principal et corriger l’IaC, rollbacker une recréation non maîtrisée, ou adopter une identité user-assigned lorsque les permissions doivent survivre au compute. Le runbook est terminé quand l’accès utile fonctionne, que l’accès hors contrat reste refusé et que les affectations orphelines peuvent être nettoyées sans ambiguïté.