7 min readMicrosoft Entra · App Registrations · MSP · PowerShell
Entra-App-Inventar über alle Kunden-Tenants: So bauen MSPs den Überblick
Anleitung für MSPs: ein Entra-App-Inventar über alle Kunden-Tenants aufbauen. Welche Felder je App-Registrierung und Service Principal zählen, wie du sie mit Graph PowerShell und REST ausliest, ungenutzte Apps erkennen und ablaufende Secrets und Zertifikate je Kunde sortiert ausgeben.
Ein MSP mit 30 Kunden-Tenants verwaltet schnell mehrere hundert App-Registrierungen. Die meisten hat niemand aus dem eigenen Team angelegt: Backup-Anbieter, Ticket-Connectoren, ein Skript von vor drei Jahren, das SSO eines Fachbereichs. Ohne Inventar siehst du ein ablaufendes Secret erst, wenn die Integration steht. Dieser Beitrag zeigt, welche Daten du sammelst, wie du sie mit Microsoft Graph ausliest und wie das über viele Tenants hinweg funktioniert.
Warum das Inventar wichtig ist
Vier Probleme tauchen in praktisch jedem Tenant auf:
- Verwaiste Apps. Der Ersteller hat die Firma verlassen, die App hat keinen Besitzer, niemand weiß, ob sie noch gebraucht wird.
- Unbekannte Credentials. Eine App-Registrierung darf mehrere Secrets und Zertifikate gleichzeitig tragen. Welches davon in Verwendung ist, steht nirgends.
- Ablaufende Secrets. Für Credentials an App-Registrierungen verschickt Microsoft keine Benachrichtigung. Was beim Ablauf passiert, steht im Beitrag Entra App Registration Secrets laufen ab.
- SAML-Signaturzertifikate. Sie liegen nicht an der App-Registrierung, sondern am Service Principal der Enterprise Application. Wer nur App-Registrierungen prüft, übersieht sie. Läuft das Zertifikat ab, fällt das SSO aus.
Dazu kommen die Compliance-Fragen der Kunden: Welche Apps haben Zugriff auf unser Verzeichnis, mit welchen Berechtigungen, wer ist verantwortlich. Ein Inventar beantwortet das in Minuten statt in Tagen.
Was du je App-Registrierung erfasst
Aus dem application-Objekt in Microsoft Graph:
displayName,appIdundid(die Objekt-ID)createdDateTime: Alter der Registrierungowners: eine eigene Abfrage, siehe untensignInAudience:AzureADMyOrgfür Single-Tenant,AzureADMultipleOrgsfür Multi-TenantpasswordCredentialsmitdisplayName,endDateTimeundkeyIdkeyCredentialsmittype,usageundendDateTimerequiredResourceAccess: welche APIs und Berechtigungen die App anfordert
Den Wert eines Secrets liefert Graph nie zurück, nur Metadaten. Das Inventar enthält also nichts Geheimes.
Was du je Service Principal erfasst
Jede Enterprise Application ist ein servicePrincipal. Relevant sind:
keyCredentials: Hier liegen die SAML-Signaturzertifikate. Entra legt sie als Paar an,AsymmetricX509CertmitusageVerifyundX509CertAndPasswordmitusageSign. Maximale Laufzeit drei Jahre.preferredSingleSignOnMode:saml,password,oidcodernotSupported. Damit filterst du die SSO-Apps heraus.appRoleAssignments: die tatsächlich erteilten Anwendungsberechtigungen, also das, was ein Admin per Consent freigegeben hat.
Auslesen mit Microsoft Graph PowerShell
Für unbeaufsichtigte Läufe nutzt du App-only-Authentifizierung mit Zertifikat. Das Zertifikat muss im Zertifikatspeicher des ausführenden Kontos liegen.
Connect-MgGraph -ClientId $clientId -TenantId $tenantId -CertificateThumbprint $thumbprint -NoWelcome
$apps = Get-MgApplication -All -Property Id, DisplayName, AppId, CreatedDateTime,
SignInAudience, PasswordCredentials, KeyCredentials, RequiredResourceAccess
$inventory = foreach ($app in $apps) {
$owners = (Get-MgApplicationOwner -ApplicationId $app.Id -All |
ForEach-Object { $_.AdditionalProperties['userPrincipalName'] }) -join ';'
$secrets = $app.PasswordCredentials | ForEach-Object { @{ Kind = 'Secret'; Cred = $_ } }
$certs = $app.KeyCredentials | ForEach-Object { @{ Kind = 'Certificate'; Cred = $_ } }
foreach ($c in @($secrets) + @($certs)) {
[pscustomobject]@{
App = $app.DisplayName; AppId = $app.AppId; Created = $app.CreatedDateTime
Owners = $owners; Kind = $c.Kind; Credential = $c.Cred.DisplayName
KeyId = $c.Cred.KeyId; Expires = $c.Cred.EndDateTime
}
}
}
$inventory | Sort-Object Expires
Dasselbe für Service Principals mit SAML, beschränkt auf das Signaturzertifikat:
Get-MgServicePrincipal -All -Property Id, DisplayName, AppId, PreferredSingleSignOnMode, KeyCredentials |
Where-Object { $_.PreferredSingleSignOnMode -eq 'saml' } |
ForEach-Object {
$sp = $_
$sp.KeyCredentials | Where-Object { $_.Usage -eq 'Sign' } | ForEach-Object {
[pscustomobject]@{ App = $sp.DisplayName; Certificate = $_.DisplayName; Expires = $_.EndDateTime }
}
}
Auslesen per Graph REST
Dieselben Daten über HTTP, eine Anfrage je Sammlung:
GET https://graph.microsoft.com/v1.0/applications?$select=id,displayName,appId,createdDateTime,signInAudience,passwordCredentials,keyCredentials,requiredResourceAccess&$top=999
GET https://graph.microsoft.com/v1.0/applications/{id}/owners
GET https://graph.microsoft.com/v1.0/servicePrincipals?$select=id,displayName,appId,preferredSingleSignOnMode,keyCredentials
GET https://graph.microsoft.com/v1.0/servicePrincipals/{id}/appRoleAssignments
/applications liefert standardmäßig 100 Objekte pro Seite, mit $top bis zu 999. /servicePrincipals liefert 100 pro Seite, mehr geht nicht. Solange die Antwort @odata.nextLink enthält, rufst du diese URL unverändert für die nächste Seite auf.
Für die einfache Liste mit $select brauchst du weder ConsistencyLevel: eventual noch $count. Beides ist Pflicht bei $search, bei not und ne, bei $filter in Kombination mit $orderby und bei Abfragen wie dieser:
GET https://graph.microsoft.com/v1.0/applications?$filter=owners/$count eq 0&$count=true&$select=id,displayName
ConsistencyLevel: eventual
Graph übernimmt den Header nicht automatisch in die Folgeseiten; setze ihn bei jedem Aufruf des @odata.nextLink. Und $select auf keyCredentials ist auf 150 Anfragen pro Minute und Tenant gedrosselt.
Über viele Kunden-Tenants
Eine App-Registrierung im eigenen Tenant, signInAudience auf AzureADMultipleOrgs, Anwendungsberechtigung Application.Read.All, Zertifikat hinterlegt. Jeder Kunde erteilt einmal Admin Consent über diese URL:
https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={client-id}
Danach existiert im Kunden-Tenant ein Service Principal deiner App, und dasselbe Zertifikat authentifiziert sich gegen jeden Tenant:
$tenants = Import-Csv .\tenants.csv # Spalten: Name, TenantId
foreach ($t in $tenants) {
Connect-MgGraph -ClientId $clientId -TenantId $t.TenantId -CertificateThumbprint $thumbprint -NoWelcome
Get-TenantInventory | # die Abfragen von oben als Funktion
Add-Member -NotePropertyName Tenant -NotePropertyValue $t.Name -PassThru |
Export-Csv ".\inventory\$($t.Name)-$(Get-Date -Format yyyy-MM-dd).csv" -NoTypeInformation
Disconnect-MgGraph
}
Azure Lighthouse hilft hier nicht. Es delegiert Azure-Abonnements und Ressourcengruppen über Azure RBAC. Deine Techniker arbeiten im Kontext des Kunden-Abonnements, ohne ein Konto im Kunden-Verzeichnis zu besitzen. Genau deshalb reicht die Delegation nicht bis in Entra ID: /applications und /servicePrincipals sind Verzeichnisobjekte, keine Azure-Ressourcen. Für das Inventar brauchst du den Consent im Kunden-Tenant.
Ungenutzte Apps finden
Ob eine App noch verwendet wird, steht im Bericht zur Anmeldeaktivität von Service Principals. Er ist nur am Beta-Endpunkt verfügbar:
GET https://graph.microsoft.com/beta/reports/servicePrincipalSignInActivities
Jeder Eintrag enthält appId und lastSignInActivity.lastSignInDateTime, aufgeschlüsselt nach delegierten und App-only-Flows. Voraussetzungen: die Anwendungsberechtigung AuditLog.Read.All und eine Lizenz Microsoft Entra ID P1 oder P2 im Kunden-Tenant. In PowerShell heißt das Cmdlet Get-MgBetaReportServicePrincipalSignInActivity aus dem Modul Microsoft.Graph.Beta.Reports. Dieselbe Berichtsfamilie bietet unter /beta/reports/appCredentialSignInActivities die letzte Nutzung je Credential, ebenfalls Preview.
Ohne P1-Lizenz bleibt die Heuristik: kein Besitzer, älter als zwei Jahre, Secrets seit Monaten abgelaufen und niemand hat sich beschwert. Solche Apps kommen auf eine Liste, die der Kunde freigibt, bevor du etwas löschst.
Ausgabe: eine Tabelle je Kunde
Eine CSV pro Tenant und Lauf, sortiert nach nächstem Ablauf. Diese Spalten haben sich bewährt:
| Tenant | App | Credential | Art | Läuft ab | Tage | Besitzer | Letzte Anmeldung |
|---|---|---|---|---|---|---|---|
| Contoso | Backup-Connector | Prod 2025 | Secret | 2026-10-09 | 11 | keiner | 2026-09-27 |
| Contoso | HR-Portal SSO | SAML Signing | Zertifikat | 2026-11-30 | 63 | j.mueller | 2026-09-26 |
| Fabrikam | Legacy Sync | key-1 | Secret | 2026-12-15 | 78 | keiner | unbekannt |
Zeilen ohne Besitzer und ohne Anmeldung gehen in die Klärung, Zeilen unter 30 Tagen in die Rotation.
Zeitplan: wöchentlich mit Delta
Ein Lauf pro Woche reicht, wenn die Warnstufen bei 90, 30 und 14 Tagen liegen. Wichtiger als der Vollabzug ist das Delta zum letzten Lauf: neue App-Registrierungen, neue Credentials (Abgleich über keyId), gelöschte Apps, Credentials, die seit dem letzten Lauf abgelaufen sind. Das Delta ist eine kurze Liste, der Vollabzug ist das Archiv für die nächste Compliance-Frage. Als Ausführungsort eignet sich ein Azure Automation Runbook oder ein geplanter Task auf einem Verwaltungsserver, das Zertifikat der Inventar-App inklusive.
Hygiene-Maßnahmen aus dem Inventar
- Besitzer eintragen. Mindestens einen Verantwortlichen im Kunden-Tenant, besser zwei. Die Abfrage
owners/$count eq 0von oben liefert die Liste. - Ungenutzte Apps löschen. Nach Freigabe durch den Kunden, nicht vorher.
- Abgelaufene Credentials entfernen. Sie tun nichts mehr, verstecken aber das aktive Secret in der Liste.
- Laufzeiten verkürzen. Zwölf Monate für Secrets, Zertifikate statt Secrets, wo die Anwendung es zulässt. Die Rotationsregeln stehen im Beitrag zu ablaufenden Secrets.
- Beschreibung pflegen. Ein Secret namens
key-1ist bei der nächsten Rotation nicht zuzuordnen.
Wie SecretExpiry hilft
SecretExpiry übernimmt den zeitkritischen Teil des Inventars: Client Secrets und Zertifikate über alle Kunden-Tenants, dauerhaft synchronisiert. Die Berechtigung ist dieselbe Application.Read.All, einmal per Admin Consent je Kunde erteilt, nur lesend. Warnungen kommen per E-Mail je Ereignis oder als wöchentliche Zusammenfassung, in einen Teams- oder Slack-Kanal per Webhook oder als Kalender-Abo (ICS), mit eigener Empfängeradresse je Tenant und Snooze für bekannte Fälle. Das Dashboard zeigt eine Dringlichkeitsliste und eine Ansicht je Tenant, der CSV-Export ersetzt den Wochenlauf. Der Test ist 14 Tage kostenlos, danach kostet der Dienst ab 10 € pro Tenant und Monat. Die Einrichtung ist in den Docs beschrieben.
Dauerhaftes Monitoring oder periodisches Skript
Ein wöchentliches Skript liefert ein Inventar mit bis zu sieben Tagen Verzögerung und hängt vom Konto, vom Zertifikat und vom Server ab, auf dem es läuft. Das Zertifikat der Inventar-App läuft selbst ab und gehört in dieselbe Liste. Dauerhaftes Monitoring synchronisiert ohne Zeitplan, den du pflegen musst, meldet Änderungen statt Zustände und braucht keinen eigenen Betrieb. Für Compliance-Fragen nach Berechtigungen und Besitzern bleibt das Skript das richtige Werkzeug, weil es alle Felder liest. Für den Ablauf von Secrets und Zertifikaten ist eine Benachrichtigung, die von selbst ankommt, zuverlässiger als eine Datei, die jemand öffnen muss.