Repérer les secrets Entra ID qui expirent avant la panne

Rédigé par l’équipe d’ingénierie de Peculiar CloudPublié le

Les identifiants Entra ID qui expirent se trouvent dans les propriétés passwordCredentials et keyCredentials des inscriptions d’applications et des principaux de service ; ces derniers portent aussi les certificats de signature SAML. Une seule autorisation Microsoft Graph en lecture, Application.Read.All, suffit pour tous les lister et repérer chaque endDateTime qui tombe dans votre fenêtre d’alerte. La solution durable, c’est d’avoir moins de secrets : identités managées ou fédération des identités de charge de travail quand c’est possible, identifiants de courte durée avec un responsable désigné pour le reste.

Un secret client Entra ID qui expire ne réveille personne. L’intégration qu’il alimentait cesse simplement de fonctionner, et le premier indice est un AADSTS7000222: The provided client secret keys are expired dans les journaux de quelqu’un. La date d’expiration était pourtant affichée depuis le début dans la page « Certificats & secrets » de l’application ; rien n’obligeait qui que ce soit à la consulter. Voici comment les passer toutes en revue d’un coup, avec une seule autorisation en lecture — et comment réduire le nombre de dates à surveiller.

Où se trouvent les identifiants

Chaque secret et chaque certificat que vous gérez se trouve dans l’une des deux collections, sur l’un des deux types d’objets, et chaque entrée porte une date endDateTime exprimée en UTC.

Objet Propriété Ce que vous y trouverez
Inscription d’application (application) passwordCredentials Secrets clients
Inscription d’application (application) keyCredentials Certificats
Principal de service (application d’entreprise) passwordCredentials Secrets ajoutés directement au principal de service ; le mot de passe qui protège une clé de signature SAML
Principal de service (application d’entreprise) keyCredentials Certificats de signature SAML ; certificats ajoutés directement

Il en découle deux choses. D’abord, une revue qui se limite aux inscriptions d’applications ne voit rien de ce qui est rattaché à un principal de service, y compris tous les certificats SAML. Ensuite, ces certificats encombrent les données brutes : Graph enregistre un certificat de signature SAML généré sous forme de trois objets sur le principal de service (une clé Sign, une clé Verify et un mot de passe qui protège la clé privée), qui partagent tous le même customKeyIdentifier. Comptez trois lignes par certificat. Entra envoie bien un courriel 60, 30 et 7 jours avant l’expiration d’un certificat SAML, mais par défaut seulement à l’administrateur qui a ajouté l’application.

Laissez de côté les identités managées : Azure renouvelle lui-même leurs identifiants. Selon la documentation actuelle de Microsoft, chaque identifiant est valide 90 jours et remplacé après 45 jours.

Une seule autorisation, en lecture seule

Application.Read.All est l’autorisation Microsoft Graph de moindre privilège pour GET /applications comme pour GET /servicePrincipals, qu’elle soit déléguée ou d’application, et les deux formes exigent le consentement d’un administrateur. Exécutez le script ci-dessous de façon interactive avec le rôle Lecteur général (Global Reader), ou accordez l’autorisation d’application à une tâche planifiée. N’ajoutez une autorisation de lecture des utilisateurs que si le rapport doit nommer les propriétaires : sans elle, Graph renvoie chaque propriétaire sous la forme d’un simple ID d’objet. Quant aux autorisations dont le nom contient ReadWrite, elles n’ont rien à faire dans un rapport.

La requête

En REST, il s’agit de deux lectures de collection avec $select :

GET https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials,keyCredentials&$top=999
GET https://graph.microsoft.com/v1.0/servicePrincipals?$select=id,appId,displayName,servicePrincipalType,passwordCredentials,keyCredentials&$top=100

Suivez @odata.nextLink jusqu’à ce qu’une réponse n’en contienne plus, et filtrez les dates de votre côté. La taille des pages a son importance : les applications acceptent 999 objets par page, les principaux de service seulement 100, et Graph limite à 150 par minute, par locataire, les requêtes qui sélectionnent keyCredentials.

Avec le Microsoft Graph PowerShell SDK, le paramètre -All suit les liens pour vous :

# Installer au préalable le module Microsoft.Graph.Applications (Install-Module -Scope CurrentUser)
param([int]$Days = 30)

Connect-MgGraph -Scopes 'Application.Read.All' -NoWelcome

$now    = [datetime]::UtcNow          # Graph renvoie les dates des identifiants en UTC
$cutoff = $now.AddDays($Days)
$select = 'id', 'appId', 'displayName', 'passwordCredentials', 'keyCredentials'

function Find-Expiring([string]$Kind, $Objects) {
  foreach ($o in $Objects) {
    $creds = @(
      foreach ($c in $o.PasswordCredentials) { @{ Type = 'Secret'; Cred = $c } }
      foreach ($c in $o.KeyCredentials) { @{ Type = "Cert ($($c.Usage))"; Cred = $c } }
    )
    foreach ($x in $creds) {
      $end = $x.Cred.EndDateTime
      if ($null -ne $end -and $end -le $cutoff) {   # inclut les identifiants déjà expirés
        [pscustomobject]@{
          Kind     = $Kind
          Name     = $o.DisplayName
          AppId    = $o.AppId
          Type     = $x.Type
          Label    = $x.Cred.DisplayName
          Expires  = $end.ToString('yyyy-MM-dd')
          DaysLeft = [int][math]::Floor(($end - $now).TotalDays)
        }
      }
    }
  }
}

$apps = Get-MgApplication -All -PageSize 999 -Property $select
$sps  = Get-MgServicePrincipal -All -Property ($select + 'servicePrincipalType') |
  Where-Object ServicePrincipalType -ne 'ManagedIdentity'   # Azure les renouvelle lui-même

$report = @(
  Find-Expiring 'App registration' $apps
  Find-Expiring 'Service principal' $sps
)
$report | Sort-Object DaysLeft | Format-Table -AutoSize

Deux choix sont délibérés. Le SDK renvoie les dates des identifiants en UTC ; la comparaison se fait donc avec UtcNow, et non avec l’heure locale. Et les identifiants déjà expirés restent dans le rapport : sur une application qui existe toujours, un secret expiré est soit une panne en cours, soit du poids mort, et dans les deux cas, c’est au responsable de l’application de trancher. Ajoutez les propriétaires (Get-MgApplicationOwner) avant de diffuser le rapport : une ligne sans nom est une ligne que personne ne corrige.

Microsoft publie sur Learn un script d’exemple pour ce besoin, mais il ne couvre que les inscriptions d’applications : il ne voit donc jamais de certificat SAML.

L’expiration est le symptôme ; la longévité, le vrai problème

La panne, c’est le coût visible. La documentation de Microsoft sur la fédération des identités de charge de travail nomme les deux : ces secrets et certificats présentent un risque pour la sécurité, doivent être stockés de façon sécurisée et renouvelés régulièrement, et leur expiration fait courir un risque d’interruption de service.

Le coût moins visible, c’est la durée de vie. Le centre d’administration limite à 24 mois la validité d’un nouveau secret client, Microsoft recommande moins de 12 mois, et ses consignes précisent que les secrets clients ne devraient pas du tout servir en production. Relancez donc la même boucle avec un autre test — ($end - $x.Cred.StartDateTime).TotalDays -gt 365 sur les secrets — et vous obtenez la liste qui compte pour la sécurité : tous les secrets valides plus longtemps que ce que Microsoft recommande. C’est aussi la durée pendant laquelle une copie oubliée dans un fichier de configuration ou un historique de clavardage reste utilisable par quiconque la détient.

Enfin, si un secret a expiré il y a des mois sans que personne ne s’en aperçoive, voyez-y la preuve que rien n’en a besoin : supprimez-le au lieu de le renouveler.

Le meilleur état final : moins de secrets

Renouveler évite la panne ; supprimer règle le problème à la source.

  1. Les identités managées pour les charges de travail qui s’exécutent dans Azure. Il n’y a aucun identifiant à stocker ni à renouveler ; Microsoft souligne qu’ils ne vous sont même pas accessibles.
  2. La fédération des identités de charge de travail pour GitHub Actions, Kubernetes, AWS, Google Cloud et les autres plateformes qui émettent un jeton OIDC. L’inscription d’application fait confiance aux jetons du fournisseur d’identité externe : aucun secret n’est stocké, et un identifiant d’identité fédérée n’a aucune date d’expiration à suivre.
  3. Des certificats plutôt que des secrets pour ce qui reste, avec des durées de vie courtes, un responsable désigné et un renouvellement confié à l’automatisation et non à un rappel d’agenda.
  4. Des politiques de gestion des applications pour ancrer ces pratiques : passwordLifetime plafonne la durée de vie d’un nouveau secret, et passwordAddition bloque purement et simplement l’ajout de nouveaux secrets. Elles exigent une licence Entra premium.

Automatiser le rapport

Un script qu’on pense à exécuter, c’est un début ; un rapport qui arrive tout seul, c’est un contrôle. Notre Entra Credential Monitor, à code source ouvert, est une façon d’y arriver : un outil Node.js en ligne de commande, sous licence MIT, qui analyse les inscriptions d’applications et les principaux de service à la recherche de secrets clients et de certificats expirés ou sur le point d’expirer, dans un ou plusieurs locataires, puis envoie par courriel un rapport compact en HTML et en texte brut. Il s’exécute depuis GitHub Actions, cron ou tout ordonnanceur capable de fournir des variables d’environnement, utilise des autorisations d’application Graph en lecture seule et surveille aussi l’expiration de son propre identifiant.

L’expiration vous dit quand une application tombera en panne ; ses autorisations Graph vous disent ce que coûterait une copie divulguée de son secret. Une Revue de sécurité infonuagique classe ce second risque avec le reste de votre exposition, et vous remet les correctifs sous forme de code dans votre dépôt — pas sous forme de liste.

Dites-nous ce qui se passe