M365 EntraID | Lizenzlücke bei Conditional Access ausrechnen ⏱ 6 Min.

M365 EntraID | Lizenzlücke bei Conditional Access ausrechnen

Der gelbe Hinweis auf der Übersichtsseite des bedingten Zugriffs nennt keine Zahl. Er sagt, dass deine Richtlinien mehr Benutzer schützen, als deine Lizenzen abdecken, und lässt dich damit allein. Wer daraufhin beim Reseller bestellt, kauft eine Zahl, die niemand geprüft hat.

Die Zahl lässt sich ausrechnen. Die Datenbasis dafür liegt vollständig in Entra ID: Jede Conditional-Access-Richtlinie trägt in ihrer conditions-Eigenschaft, welche Benutzer, Gruppen und Verzeichnisrollen sie einschließt und welche sie ausnimmt. Löst du diese Zuweisungen bis auf einzelne Konten auf und vergleichst das Ergebnis mit dem tatsächlichen Lizenzbestand, bekommst du eine belastbare Differenz statt eines Bauchgefühls.

Warum die Warnung überhaupt erscheint

Conditional Access setzt pro erfasstem Benutzer eine Entra ID P1 voraus. Die Durchsetzung erfolgt nicht technisch, sondern lizenzrechtlich: Eine Richtlinie greift auch bei einem unlizenzierten Benutzer. Microsoft prüft im Hintergrund die Deckung und blendet den Hinweis ein, sobald die Zahl der erfassten Konten die Zahl der zugewiesenen P1-Berechtigungen übersteigt.

Der häufigste Auslöser ist keine Unterlizenzierung, sondern eine unsaubere Zuweisung. Eine Richtlinie zielt auf "Alle Benutzer", und im Tenant hängen Dienstkonten, Raumpostfächer, Gastkonten oder stillgelegte Konten mit drin, die niemand jemals bewusst in den Geltungsbereich genommen hat.

Passend dazu: M365 EntraID | Admin Center meldet Lizenzlücke bei Conditional Access, dort steht die Einordnung der Warnung.

Voraussetzungen

Du brauchst das Modul Microsoft.Graph und Leserechte auf Richtlinien, Verzeichnis und Benutzer.

Connect-MgGraph -Scopes "Policy.Read.All","Directory.Read.All",
    "User.Read.All","Group.Read.All","RoleManagement.Read.Directory"

Entra ID P2 trägt die Plan-ID eec0eb4f-6444-4f95-aba0-50c24d67f998 und schließt P1 in der Regel mit ein. Wer in einer gemischten Umgebung sauber rechnen will, berücksichtigt beide. Beide Kennungen stehen in der Referenz Product names and service plan identifiers for licensing, die Microsoft laufend pflegt. Prüfe sie vor dem Produktivlauf gegen deinen Tenant:

(Get-MgSubscribedSku).ServicePlans | Where-Object { $_.ServicePlanName -like 'AAD_PREMIUM*' }

Schritt 1: Aktive Richtlinien einsammeln

$policies = Get-MgIdentityConditionalAccessPolicy -All |
    Where-Object { $_.State -eq "enabled" }

$policies | Select-Object DisplayName, State

Richtlinien im Zustand enabledForReportingButNotEnforced zählen für die Lizenzprüfung mit, weil sie Auswertungen erzeugen. Ob du sie in deine Rechnung aufnimmst, hängt davon ab, ob du den Report-Only-Modus dauerhaft betreibst oder nur zum Testen. Für eine konservative Zahl nimmst du sie mit hinein.

Schritt 2: Zuweisungen bis auf Konten auflösen

Der Kern der Auswertung. Gruppen werden transitiv aufgelöst, weil verschachtelte Gruppen sonst durchrutschen.

$erfasst = [System.Collections.Generic.HashSet[string]]::new()

foreach ($p in $policies) {
    $u = $p.Conditions.Users

    # Direkt zugewiesene Benutzer
    foreach ($id in $u.IncludeUsers) {
        if ($id -notin @("All","None","GuestsOrExternalUsers")) {
            [void]$erfasst.Add($id)
        }
    }

    # Gruppen, transitiv aufgelöst
    foreach ($gid in $u.IncludeGroups) {
        Get-MgGroupTransitiveMember -GroupId $gid -All |
            Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.user' } |
            ForEach-Object { [void]$erfasst.Add($_.Id) }
    }

    # Verzeichnisrollen
    foreach ($rid in $u.IncludeRoles) {
        $role = Get-MgDirectoryRole -Filter "roleTemplateId eq '$rid'" -ErrorAction SilentlyContinue
        if ($role) {
            Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All |
                ForEach-Object { [void]$erfasst.Add($_.Id) }
        }
    }
}

Zwei Fallstricke stecken hier drin. Verzeichnisrollen liefert Get-MgDirectoryRole nur zurück, wenn die Rolle im Tenant aktiviert ist. Eine in der Richtlinie referenzierte, aber nie aktivierte Rolle findest du über diesen Weg nicht. Und IncludeUsers kann den Wert All tragen. In diesem Fall ist die Einzelauflösung sinnlos, dann gilt der gesamte Benutzerbestand.

$alleBenutzerPolicy = $policies | Where-Object { $_.Conditions.Users.IncludeUsers -contains "All" }

if ($alleBenutzerPolicy) {
    Write-Warning "Mindestens eine Richtlinie zielt auf Alle Benutzer: $($alleBenutzerPolicy.DisplayName -join ', ')"
}

Schritt 3: Ausschlüsse abziehen

Ausschlüsse gewinnen in Conditional Access immer gegen Einschlüsse. Wer sie in der Rechnung vergisst, kauft Lizenzen für Konten, die nie erfasst werden. Das betrifft typischerweise die Break-Glass-Konten.

$ausgeschlossen = [System.Collections.Generic.HashSet[string]]::new()

foreach ($p in $policies) {
    $u = $p.Conditions.Users

    foreach ($id in $u.ExcludeUsers) { [void]$ausgeschlossen.Add($id) }

    foreach ($gid in $u.ExcludeGroups) {
        Get-MgGroupTransitiveMember -GroupId $gid -All |
            Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.user' } |
            ForEach-Object { [void]$ausgeschlossen.Add($_.Id) }
    }
}

Ein Ausschluss gilt allerdings nur für die Richtlinie, in der er steht. Ein Konto, das in Richtlinie A ausgenommen und in Richtlinie B eingeschlossen ist, bleibt erfasst. Die folgende Vereinfachung, alle Ausschlüsse global abzuziehen, rechnet dein Ergebnis deshalb kleiner, als es ist. Für eine exakte Zahl musst du pro Richtlinie rechnen und die Ergebnismengen anschließend vereinigen. Für die Abschätzung gegenüber dem Reseller reicht die Näherung, solange du weißt, in welche Richtung sie abweicht.

Schritt 4: Gegen den Lizenzbestand prüfen

$p1Plan = "41781fb2-bc02-4b7c-bd55-b576c07bb09d"
$p2Plan = "eec0eb4f-6444-4f95-aba0-50c24d67f998"

$kandidaten = $erfasst | Where-Object { $_ -notin $ausgeschlossen }

$luecke = foreach ($id in $kandidaten) {
    $user = Get-MgUser -UserId $id -Property Id, DisplayName, UserPrincipalName,
        AccountEnabled, UserType, AssignedPlans -ErrorAction SilentlyContinue

    if (-not $user) { continue }

    $hatPremium = $user.AssignedPlans | Where-Object {
        $_.ServicePlanId -in @($p1Plan, $p2Plan) -and $_.CapabilityStatus -eq "Enabled"
    }

    if (-not $hatPremium) {
        [PSCustomObject]@{
            Name     = $user.DisplayName
            UPN      = $user.UserPrincipalName
            Aktiv    = $user.AccountEnabled
            Typ      = $user.UserType
        }
    }
}

$luecke | Sort-Object Typ, Name | Format-Table -AutoSize
"Lizenzlücke: $($luecke.Count) Konten"

Schritt 5: Das Ergebnis lesen, bevor du bestellst

Die Zahl aus Schritt 4 ist keine Bestellmenge, sondern eine Arbeitsliste. Gehe sie in dieser Reihenfolge durch.

Deaktivierte Konten mit Aktiv = False gehören nicht lizenziert, sondern gelöscht oder aus dem Geltungsbereich genommen. Gastkonten mit Typ = Guest fallen unter die External Identities und werden separat abgerechnet, nicht über P1 pro Konto. Dienstkonten und Raumpostfächer gehören nicht in eine Benutzerrichtlinie. Für Workload Identities gibt es Conditional Access for Workload Identities als eigenes Produkt.

Was nach dieser Bereinigung übrig bleibt, sind echte Benutzer ohne P1-Deckung. Erst diese Menge ist die Bestellgrundlage.

Passend dazu: M365 | Admin Konto: Lizenzieren oder nicht?, weil Adminkonten hier regelmäßig auftauchen.

Fazit

Die Warnung im Admin Center ist ein Symptom, keine Diagnose. In den Umgebungen, die ich gesehen habe, war die tatsächliche Lizenzlücke nach der Bereinigung regelmäßig kleiner als die ursprüngliche Differenz, weil sich in den Geltungsbereichen Konten sammeln, die dort nichts zu suchen haben.

Zwei Einschränkungen zur Auswertung. Erstens rechnet die vereinfachte Variante mit global abgezogenen Ausschlüssen und liefert damit eine Untergrenze. Wer eine belastbare Obergrenze braucht, etwa für ein Lizenzaudit, muss pro Richtlinie auswerten. Zweitens ändert sich die Datenbasis mit jeder Gruppenmitgliedschaft. Eine einmalige Auswertung ist eine Momentaufnahme, kein Dauerzustand. Sinnvoll ist ein monatlicher Lauf, dessen Ergebnis du archivierst, weil du damit gleichzeitig einen Nachweis für den Auditor hast.

Der eigentliche Gewinn liegt nicht in der Lizenzzahl. Er liegt darin, dass du nach dieser Übung weißt, welche Konten deine Conditional-Access-Richtlinien tatsächlich erfassen. Das ist eine Frage, die in Sicherheitsaudits gestellt wird und auf die erstaunlich wenige Organisationen eine geprüfte Antwort haben.

Weiterlesen: M365 EntraID | Warum P1 oder P2 für Sicherheit sorgen, für die Entscheidung zwischen beiden Plänen.

PS1 Script zum Download



Teilen:
Noch keine Kommentare

Sei der Erste und starte die Diskussion mit einem hilfreichen Beitrag.

Kommentar hinterlassen

Dein Beitrag wird vor der Veröffentlichung kurz geprüft — fachlich, respektvoll und auf den Punkt ist hier genau richtig.

E-Mail Adresse wird nicht veröffentlicht.