← Toutes les ressources
Ressource13 octobre 2026

Auditer ses service principals Azure en 30 minutes

Une méthode en lecture seule pour inventorier les secrets, les droits et les connexions des service principals, avec les scripts PowerShell, les requêtes KQL et les prompts à copier.

  • Microsoft Entra ID
  • Azure
  • Identités non humaines
  • PowerShell
  • KQL

Pourquoi : 7 minutes, aucune faille, que des droits légitimes

En juin 2026, deux service principals compromis ont supprimé la majorité de plus de 100 comptes de stockage, un Key Vault, une Function App et un App Service plan dans un seul tenant Azure. La phase destructrice a duré environ 7 minutes, après plus de 15 heures de reconnaissance. Microsoft a publié l'analyse le 25 septembre 2026 sous le nom Storm-3168.

Aucune CVE n'a été exploitée. Chaque suppression était autorisée par les rôles déjà attribués aux service principals : Storage Account Contributor via un groupe, Contributor en direct, SQL DB Contributor en direct. L'outil a appliqué la gouvernance. Elle n'existait pas.

Chiffre Ce qu'il dit Source
100+ comptes de stockage ciblés en ~7 min La réponse humaine arrive trop tard Microsoft, WorkOS
15,5 h de reconnaissance, 300+ lectures réussies La détection avait des heures pour agir CSA
30+ clés de comptes de stockage récupérées via ListKeys L'accès survit à la rotation du secret WorkOS
31 % des points d'entrée = NHI compromises, contre 17 % pour le phishing Les identités non humaines sont devenues la porte n° 1 SpyCloud, enquête 750 répondants
95 % pensent avoir la visibilité, 36 % surveillent L'écart que les attaquants exploitent SpyCloud

Les 3 erreurs courantes

  1. Croire qu'un secret supprimé est révoqué. Le secret avait été retiré de l'issue GitHub, mais il restait lisible dans l'historique des modifications. Il fonctionnait toujours.
  2. Donner Contributor « pour que ça marche ». Un compte qui ne fait que lire ne devrait pas pouvoir supprimer, y compris via l'appartenance à un groupe.
  3. Ne surveiller que les humains. Les sign-ins des service principals depuis une infrastructure inconnue étaient le signal, des heures avant les suppressions.

Le changement à opérer

Traiter chaque service principal comme une identité à part entière : un propriétaire, un besoin documenté, des droits minimaux, un secret qui expire (ou pas de secret du tout), et une surveillance de ses connexions.

3 cas d'usage qu'on retrouve dans toutes les PME et ETI

Chaque cas part d'une situation réelle de terrain, puis donne le contrôle qui l'aurait arrêtée.

Cas 1 : le secret qui a fuité « mais on l'a supprimé »

Situation. Un développeur colle un client ID, un secret et un tenant ID dans un ticket, un canal Teams ou une issue GitHub pour débloquer un collègue. Quelqu'un le remarque et efface le message.

Pourquoi c'est un problème. Effacer le message change l'endroit où le secret est visible. Pas sa validité. Entra ID l'échange contre un jeton tant qu'il n'est pas révoqué, et les jetons déjà émis restent valides jusqu'à leur expiration. Dans Storm-3168, Microsoft a vu 5 jetons distincts émis pour le service principal destructeur.

Le contrôle. Révocation du secret le jour même, puis revue des sign-ins et de l'activité Azure sur toute la période d'exposition. À terme : identité managée ou fédération d'identité de charge de travail, donc plus de secret à faire fuiter.

Cas 2 : le service principal « Contributor parce que ça marchait pas »

Situation. Un pipeline CI/CD ou un outil de sauvegarde a besoin d'écrire dans un compte de stockage. Le déploiement échoue, quelqu'un met Contributor sur l'abonnement, ça passe, on n'y touche plus.

Pourquoi c'est un problème. Le jour où ce secret fuit, l'attaquant hérite de tout le périmètre. Dans Storm-3168, les rôles en place couvraient chaque suppression, et seule une erreur de version d'API a sauvé les bases SQL.

Le contrôle. Rôle le plus restreint au plus petit périmètre (resource group ou ressource), verrous CanNotDelete et protection contre la suppression sur les comptes de stockage critiques.

Cas 3 : le service principal orphelin du prestataire

Situation. Une intégration posée par un prestataire il y a trois ans. Le prestataire est parti, le propriétaire de l'app aussi. Le secret n'expire jamais, ou dans 2 ans.

Pourquoi c'est un problème. Personne ne sait à quoi il sert, donc personne n'ose le supprimer, et personne ne regarde ses connexions.

Le contrôle. Un propriétaire nommé pour chaque app registration, une expiration de secret courte, et une désactivation test avant suppression : on coupe, on attend une semaine, on écoute qui se plaint.

La méthode : 5 étapes, 30 minutes, lecture seule

Tout se fait sans rien modifier. Rôles suffisants : Global Reader dans Entra ID, Reader sur les abonnements Azure. Les scripts et requêtes sont dans la section suivante.

  1. Inventorier les identités qui ont un secret ou un certificat (5 min). Lister chaque app registration avec ses credentials, leur date d'expiration et ses propriétaires. Script 1.
  2. Isoler les secrets à risque (5 min). Secrets valides plus de 12 mois, apps sans propriétaire, apps avec plusieurs secrets actifs. C'est votre liste de rotation. Script 1, colonnes filtrées.
  3. Cartographier les droits Azure (10 min). Pour chaque service principal : Owner, Contributor ou User Access Administrator au niveau abonnement, y compris via un groupe. C'est votre liste de réduction de privilèges. Script 2.
  4. Lire les connexions (5 min). Service principals qui se connectent depuis des IP jamais vues, et service principals sans connexion depuis 90 jours. Requêtes KQL 1 et 2.
  5. Vérifier les verrous (5 min). Les comptes de stockage, Key Vaults et coffres de sauvegarde critiques ont-ils un verrou CanNotDelete ? Script 3.

Avant / après

Point de contrôle Avant (constat type) Après l'audit
Secrets Durée de vie longue, personne ne sait où ils sont copiés Expiration courte, rotation planifiée, secrets remplacés par identité managée ou fédération quand c'est possible
Propriétaires App registrations sans propriétaire ou avec un ancien salarié Un propriétaire nommé et joignable par app
Droits Azure Contributor sur l'abonnement « pour que ça marche » Rôle minimal sur le resource group ou la ressource
Ressources critiques Supprimables par toute identité Contributor Verrou CanNotDelete + protection contre la suppression des comptes de stockage
Surveillance Sign-ins humains surveillés, service principals ignorés Alerte sur IP inconnue et sur pic de lectures ou de suppressions
Incident de fuite « On a supprimé le message » Révocation immédiate + revue des logs sur toute la période d'exposition

Les modèles à copier

Tous les scripts sont en lecture seule. Testez-les d'abord sur un tenant de test ou un abonnement hors production.

Script 1 : inventaire des secrets, expirations et propriétaires (Microsoft Graph PowerShell)

PowerShell
Connect-MgGraph -Scopes "Application.Read.All"

$apps = Get-MgApplication -All -Property Id,AppId,DisplayName,PasswordCredentials,KeyCredentials
$now  = Get-Date

$report = foreach ($app in $apps) {
    $owners = (Get-MgApplicationOwner -ApplicationId $app.Id -All |
        ForEach-Object {
            $p = $_.AdditionalProperties
            if ($p.userPrincipalName) { $p.userPrincipalName } else { "$($p.displayName) (SP)" }
        }) -join ";"

    foreach ($cred in @($app.PasswordCredentials) + @($app.KeyCredentials)) {
        if (-not $cred) { continue }
        [pscustomobject]@{
            App          = $app.DisplayName
            AppId        = $app.AppId
            Type         = if ($cred.PSObject.Properties.Name -contains 'Hint') { 'Secret' } else { 'Certificat' }
            DebutValidite = $cred.StartDateTime
            Expiration   = $cred.EndDateTime
            DureeJours   = [int]($cred.EndDateTime - $cred.StartDateTime).TotalDays
            Expire       = $cred.EndDateTime -lt $now
            Proprietaires = if ($owners) { $owners } else { 'AUCUN' }
        }
    }
}

$report | Sort-Object DureeJours -Descending |
    Export-Csv .\audit-spn-secrets.csv -NoTypeInformation -Encoding UTF8

À regarder en premier : DureeJours supérieur à 365, Proprietaires égal à AUCUN, plusieurs secrets actifs sur la même app.

Script 2 : rôles Azure à privilège élevé détenus par des service principals (Az PowerShell)

PowerShell
Connect-AzAccount
$rolesSensibles = 'Owner','Contributor','User Access Administrator'

$result = foreach ($sub in Get-AzSubscription) {
    Set-AzContext -SubscriptionId $sub.Id | Out-Null
    Get-AzRoleAssignment |
        Where-Object { $_.RoleDefinitionName -in $rolesSensibles -and
                       $_.ObjectType -in 'ServicePrincipal','Group' } |
        Select-Object @{n='Abonnement';e={$sub.Name}}, DisplayName, ObjectType,
                      RoleDefinitionName, Scope
}

$result | Export-Csv .\audit-spn-roles.csv -NoTypeInformation -Encoding UTF8

Les lignes de type Group comptent : dans Storm-3168, la suppression des comptes de stockage passait par un rôle hérité d'un groupe. Vérifiez quels service principals sont membres de ces groupes.

Script 3 : comptes de stockage et Key Vaults sans verrou CanNotDelete (Az PowerShell)

PowerShell
Connect-AzAccount

foreach ($sub in Get-AzSubscription) {
    Set-AzContext -SubscriptionId $sub.Id | Out-Null

    $locks  = Get-AzResourceLock | Where-Object { $_.Properties.level -eq 'CanNotDelete' }
    $scopes = $locks | ForEach-Object { ($_.ResourceId -split '/providers/Microsoft.Authorization/locks/')[0] }

    Get-AzResource -ResourceType 'Microsoft.Storage/storageAccounts','Microsoft.KeyVault/vaults' |
        Where-Object {
            $id = $_.ResourceId
            -not ($scopes | Where-Object { $id -eq $_ -or $id -like "$_/*" })
        } |
        Select-Object @{n='Abonnement';e={$sub.Name}}, Name, ResourceType, ResourceGroupName
}

Un verrou posé sur le resource group ou l'abonnement couvre aussi la ressource : le script en tient compte.

Requête KQL 1 : service principals qui se connectent depuis une IP jamais vue (Log Analytics / Sentinel)

Prérequis : les logs ServicePrincipalSignInLogs envoyés vers Log Analytics via les paramètres de diagnostic Entra.

KQL
let reference = AADServicePrincipalSignInLogs
    | where TimeGenerated between (ago(30d) .. ago(1d))
    | distinct ServicePrincipalId, IPAddress;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d)
| where ResultType == "0"
| join kind=leftanti reference on ServicePrincipalId, IPAddress
| summarize Connexions = count(), Premiere = min(TimeGenerated)
    by ServicePrincipalName, ServicePrincipalId, IPAddress
| order by Connexions desc

Requête KQL 2 : service principals silencieux depuis 90 jours

KQL
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(180d)
| summarize DerniereConnexion = max(TimeGenerated) by ServicePrincipalName, ServicePrincipalId
| where DerniereConnexion < ago(90d)
| order by DerniereConnexion asc

Limite : un service principal qui ne s'est jamais connecté n'apparaît pas. Croisez avec l'export du script 1.

Requête KQL 3 : le motif Storm-3168 (rafale de suppressions et de ListKeys)

KQL
AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue endswith "/DELETE"
    or OperationNameValue =~ "MICROSOFT.STORAGE/STORAGEACCOUNTS/LISTKEYS/ACTION"
| where ActivityStatusValue in ("Success", "Start")
| summarize Operations = count(), Ressources = dcount(_ResourceId)
    by Caller, bin(TimeGenerated, 10m)
| where Operations > 10
| order by Operations desc

Caller contient l'identifiant de l'identité appelante. Ajustez le seuil de 10 opérations en 10 minutes à votre volume normal.

Prompt 1 : prioriser les résultats de l'audit

Texte
Tu es un ingénieur IAM senior. Voici deux exports CSV d'un tenant Microsoft Entra / Azure :
1) audit-spn-secrets.csv (secrets, expirations, propriétaires)
2) audit-spn-roles.csv (rôles Azure à privilège élevé détenus par des service principals ou des groupes)

Croise les deux fichiers par nom d'application. Classe chaque service principal en 3 niveaux :
- Critique : rôle Owner/Contributor/User Access Administrator ET (secret > 365 jours OU aucun propriétaire)
- Élevé : un seul des deux critères
- À surveiller : le reste
Pour chaque critique, propose l'action la plus simple qui réduit le risque cette semaine.
Réponds en tableau. N'invente aucune donnée absente des fichiers.

Prompt 2 : réduire les droits d'un service principal

Texte
Ce service principal a le rôle [RÔLE] sur [PORTÉE]. Il sert à : [USAGE EN UNE PHRASE].
Voici ses 30 derniers jours d'opérations AzureActivity : [COLLER L'EXPORT].
Propose le rôle intégré Azure le plus restreint qui couvre exactement ces opérations,
à la portée la plus petite possible. Si aucun rôle intégré ne convient, propose une
définition de rôle personnalisé en JSON. Liste les opérations observées que ton rôle
n'autoriserait plus, pour que je puisse les valider avant d'appliquer.

Prompt 3 : runbook « un secret a fuité »

Texte
Un secret de l'application [NOM / APP ID] a été exposé publiquement le [DATE] sur [LIEU].
Rédige un runbook d'intervention en étapes numérotées pour une équipe IT de PME :
révocation du secret, vérification des jetons déjà émis, revue des sign-ins du service
principal et de l'activité Azure sur la période d'exposition, verrouillage temporaire
des ressources critiques, communication interne. Pour chaque étape, indique le rôle
Entra ou Azure minimal requis et la commande Azure CLI ou PowerShell correspondante.

Avec quoi le faire tourner

L'audit de cette ressource tourne avec des outils que vous avez déjà. La surveillance continue des service principals, elle, demande une licence dédiée chez Microsoft.

Niveau Outil Ce qu'il apporte Coût
Natif Microsoft Graph PowerShell, Az PowerShell, Azure CLI Inventaire, rôles, verrous, rotation des secrets (scripts 1 à 3) Inclus
Natif Log Analytics / Microsoft Sentinel Requêtes KQL 1 à 3, alertes sur les sign-ins des service principals Facturé au volume ingéré
Natif Microsoft Entra Workload ID Premium Accès conditionnel pour les identités de charge de travail (blocage par IP ou par risque), ID Protection pour les service principals Licence dédiée, essai gratuit de 90 jours
Open source Maester, ROADtools, AzureHound Tests de configuration Entra automatisés, cartographie des chemins d'attaque Gratuit
Éditeurs Plateformes de gouvernance des identités non humaines Inventaire multi-cloud et SaaS, détection des secrets exposés Sur devis

Trois points sur Workload ID Premium, d'après la FAQ Microsoft :

  • Une seule licence dans le tenant débloque les fonctions pour toutes les identités de charge de travail, sans attribution individuelle.
  • L'accès conditionnel ne couvre que les applications mono-tenant. Les apps Microsoft et les identités managées en sont exclues.
  • Sans licence, les politiques existantes continuent de fonctionner mais ne peuvent plus être modifiées (documentation).

L'outil applique la gouvernance. Il ne l'invente pas. Commencez par les scripts : la licence n'a de valeur que si vous savez déjà quels service principals protéger.

La suite

Je publie chaque semaine des cas d'usage IAM et Identity Security prêts à appliquer : Entra ID, Active Directory, identités non humaines, NIS2. Suivez-moi sur LinkedIn pour recevoir le prochain.

Sources

Ressources

Un script qui ne passe pas sur votre tenant, un résultat à interpréter ?

Écrivez-moi