Hybrid Agent Statusabfrage

Immer mehr Firmen nutzen den "Modern Hybrid"-Mode über lokal installierte Agenten. Sie habe damit fast alle Möglichkeiten einer Koexistenz, inklusive Migration, Free/Busy-Anzeige etc. Nur der Zugriff in Microsoft Teams auf den Kalender von OnPremises liegenden Postfächer ist nicht möglich. Dennoch müssen Sie natürlich überwachen, ob der lokale Agent funktioniert..

Diese Seite beschreibt auch die Herleitung, d.h. Angangspunkt, Analyse, Test, Konfigurationsanpassungen und ein Skript zur Integration in eigene Dienste.

Status abfragen

Leider liefert ihnen Microsoft 365 keine Stelle, an der Sie den aktuellen Status des Hybrid Agenten ablesen können. Das geht zum Teil über eine Überwachung mit dem lokalen Eventlog (Siehe HCW Logging)  aber wichtiger ist die Information aus Microsoft 365. Für die Anfrage des Status zum Modern Hybrid Agenten bietet Microsoft zwei Optionen an:

  • HCW
    Sie können den HCW - Hybrid Configuration Wizard starten und während der Konfiguration des Modern HybridMode zeigt er ihnen direkt an, welche Agenten es gibt und welchen Status diese haben.

    Das ist natürlich nicht
  • HybridManagement-PowerShell
    Die zweite Option ist eine Powershell, die auf dem Agenten mit installiert wird, aber (Stand Nov 2024) nicht die gewünschte Code-Qualität hat.

    Zum PowerShell-Modul habe ich eine eigene Seite auf HybridManagment.psm1, die explizit auf die Probleme etc. eingeht.

Die GUI eignet sich natürlich nicht für entsprechende Automatisierungen und auch die HybridManagment.psm1-PowerShell erlaubt keine automatische Anmeldung und MFA, hat eine Abhängigkeit zur AzureAD PowerShell und erst eine aktuelle Version von "https://aka.ms/hybridconnectivity" unterstützt z.B.: MFA.

Commandlets im Modul

Aber das Schöne an einer Powershell ist, dass die Quellen "offen" sind und Code von Microsoft kann oft als Quelle für eine eigene Optimierung genutzt werden. Diese PS1M-Datei ist allerdings "gemischt". Wenn ich das Modul importiert habe, stellt es mehrere Commandlets bereit:

PS C:\Downloads> Import-Module .\HybridManagement.psm1
PS C:\Downloads> get-command -Module hybridmanagement

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        GetAuthHeader                                      0.0        HybridManagement
Function        GetAuthToken                                       0.0        HybridManagement
Function        Get-HybridAgent                                    0.0        HybridManagement
Function        Get-HybridApplication                              0.0        HybridManagement
Function        New-HybridApplication                              0.0        HybridManagement
Function        Remove-HybridApplication                           0.0        HybridManagement
Function        TestEndpoints                                      0.0        HybridManagement
Function        Test-HybridConnectivity                            0.0        HybridManagement
Function        TestoneEndpoint                                    0.0        HybridManagement
Function        TestProxySettings                                  0.0        HybridManagement
Function        TestTLSSettings                                    0.0        HybridManagement
Function        Update-HybridApplication                           0.0        HybridManagement

Für die Anwendung sind nur eine Teilmenge an Befehlen relevant aber die internen Commandlets sind nicht als "privat" deklariert und damit versteckt. Auch die Namensgebung ohne den "Bindestring" zwischen Verb und Subject entspricht eher nicht nach Microsoftvorgaben. Zumindest folgen aber die öffentlichen Commandlets einer "*-hybrid*" Namenskonvention.

Im Code wird dann manuell ein AuthToken abgerufen, ein AuthHeader erstellt und mit Invoke-RestMethod ein Graph-Aufruf auf eine "/EDU/"-URL gestartet.

Das macht es natürlich interessant, wie der Wert gefüllt wird. Im Code sehen Sie daher einmal den Aufruf von "GetAuthToken" gefolgt von GetAuthHeader, bei dem das erhaltene Token so umformatiert wird, dass der Invoke-RestMethod damit arbeiten kann. Auch der Code ist enthalten:

 

Damit habe ich aber genug gesehen um mir meine Statusüberwachung selbst zu bauen.

Status mit MGGraph

Zuerst habe ich es einfach gemacht und als "Delegate" den Status abgerufen. Zuerst habe ich MgGraph installiert und verbunden.

Install-Module Microsoft.Graph

Connect-MgGraph `
   -TenantId msxfaqlab.onmicrosoft.com `
   -ClientId "1950a258-227b-4e31-a9cf-717495945fc2" `
   -Scopes AuditLog.Read.All,Directory.AccessAsUser.All,email,openid,profile

Die ClientID "1950a258-227b-4e31-a9cf-717495945fc2" steht übrigens für "Microsoft Azure PowerShell".

Die ist aber schon abgekündigt. Siehe EOL MSOnline und AzureAD PowerShell. Es geht aber auch mit einer anderen ClientID. Da müssen sie aber wieder "Consent" erteilen. Das habe ich mir aber erst mal gespart, denn später soll die Abfrage ja eh automatisiert mit App-Permissions laufen. Aus den Fiddler-Logs des HCW habe ich gesehen, wie HCW die Connectoren ausliest.

Invoke-MGGraphRequest -Uri "https://graph.microsoft.com/edu/connectorGroups?`$expand=members"

 

 

Damit habe ich sowohl die öffentliche IP-Adresse, die Exchange von mir sieht als auch einen "Status". Wobei der MgGrPH-Aufruf hier etwas tückisch ist, denn so liefert er ein Array zurück:

PS C:\temp> (Invoke-MGGraphRequest -Uri "https://graph.microsoft.com/edu/connectorGroups?`$expand=members").value.gettype()

IsPublic IsSerial Name        BaseType
-------- -------- ----        --------
True     True     Object[]    System.Array

PS C:\temp> (Invoke-MGGraphRequest -Uri "https://graph.microsoft.com/edu/connectorGroups?`$expand=members").value

Name                           Value
----                           -----
isDefault                      False
id                             e71c6f90-34d7-4b0e-be95-1f9e952a7bed
connectorGroupType             syncFabric
name                           Default group for AD Sync Fabric
members                        {}
isDefault                      False
id                             f371a12e-3cd8-4cf2-9629-2fc2d4e8d63c
connectorGroupType             syncFabric
name                           Group-UCLABOR.DE-96e5bfea-ccfd-406c-bc6e-6673d01a4722
members                        {}
isDefault                      False
id                             61914166-6324-4790-8515-49e30ac19114
connectorGroupType             exchangeOnline
name                           Default group for Exchange Online
members                        {f92ae649-f9e5-48e4-ab32-99e84ba8b119}

Und da gibt es bei mir schon drei Gruppen mit eigenen "Member"-Einträgen. Erst wenn ich die Rückgabe mit "-OutputType PSObject" anfordere, sehe ich die Struktur:

 

Die URL "/edu" ist natürlich etwas suspekt und daher habe ich ein paar andere URLs ausprobiert und eine offizielle URL gefunden, die sogar noch die Version liefert.

URL  Ergebnis
https://graph.microsoft.com
   /edu
     /connectorGroups

200 OK

{
   "@odata.context":"https://graph.microsoft.com/edu/$metadata#connectorGroups(members())",
   "value":
   [
      {
         "id":"e71c6f90-34d7-4b0e-be95-1f9e952a7bed",
         "name":"Default group for AD Sync Fabric",
         "connectorGroupType":"syncFabric",
         "isDefault":false,
         "members":[]
      },
      {
         "id":"f371a12e-3cd8-4cf2-9629-2fc2d4e8d63c",
         "name":"Group-UCLABOR.DE-96e5bfea-ccfd-406c-bc6e-6673d01a4722",
         "connectorGroupType":"syncFabric",
         "isDefault":false,
         "members":[]
      },
      {
         "id":"61914166-6324-4790-8515-49e30ac19114",
         "name":"Default group for Exchange Online",
         "connectorGroupType":"exchangeOnline",
         "isDefault":false,
         "members":
         [
            {
               "id":"f92ae649-f9e5-48e4-ab32-99e84ba8b119",
               "machineName":"EX01.UCLABOR.DE",
               "externalIp":"80.66.20.27",
               "status":"inactive"}
         ]
      }
   ]    
}
 
 

Diese API liefert aber auch offiziell den Agent

https://graph.microsoft.com
   /beta
      /onPremisesPublishingProfiles
         /applicationProxy
            /connectorGroups/$expand=members

200 OK

{
   "@odata.context":"https://graph.microsoft.com/beta/$metadata#onPremisesPublishingProfiles('applicationProxy')/connectorGroups(members())",
   "value":
   [
      {
         "id":"e71c6f90-34d7-4b0e-be95-1f9e952a7bed",
         "name":"Default group for AD Sync Fabric",
         "region":"eur",
         "connectorGroupType":"syncFabric",
         "isDefault":false,
         "members":[]
      },
      {
         "id":"f371a12e-3cd8-4cf2-9629-2fc2d4e8d63c",
         "name":"Group-UCLABOR.DE-96e5bfea-ccfd-406c-bc6e-6673d01a4722",
         "region":"eur",
         "connectorGroupType":"syncFabric",
         "isDefault":false,
         "members":[]
       },
       {
         "id":"61914166-6324-4790-8515-49e30ac19114",
         "name":"Default group for Exchange Online",
         "region":"eur",
         "connectorGroupType":"exchangeOnline",
         "isDefault":false,
         "members":
         [
            {
               "id":"f92ae649-f9e5-48e4-ab32-99e84ba8b119",
               "machineName":"EX01.UCLABOR.DE",
               "externalIp":"80.66.20.27",
               "status":"inactive",
               "version":"1.5.732.0"}
         ]
      }
   ]
}
https://graph.microsoft.com
   /beta
      /connectorGroups?$expand=members

400 Bad Request

{
   "error":{
     "code":"BadRequest",
      "message":"Resource not found for the segment 'connectorGroups'.",
      "innerError":{"date":"2024-11-12T14:32:27",....}
   }
}
 
https://graph.microsoft.com
   /1.0
      /connectorGroups?$expand=members

404 Not Found

{
   "error":{
      "code":"ResourceNotFound",
      "message":"Invalid version: onpremisespublishingprofiles",
      "innerError":{"date":"2024-11-12T14:35:54", ......
      }
   }
}
https://graph.microsoft.com
   /connectorGroups?$expand=members

400 Bad Request

{
   "error":{
      "code":"BadRequest",
      "message":"Resource not found for the segment 'connectorGroups'.",
      "innerError":{"date":"2024-11-12T14:36:13", ......
      }
   }
}
https://graph.microsoft.com
   /onPremisesPublishingProfiles
      /applicationProxy
      /connectorGroups?$expand=members

404 Not Found

{
   "error":{
      "code":"ResourceNotFound",
      "message":"Invalid version: onpremisespublishingprofiles",
      "innerError":{"date":"2024-11-12T14:36:55", ......
      }
   }
}

Anscheinend ist dies eine recht versteckte URL und API, die zwar hinter graph.microsoft.com agiert und sicher etwas mit AzureADAppProxy zu tun hat, aber eigenständig ist. Mit den klassischen URLs kommen wir da nicht weiter.

Und damit komme ich natürlich auch nicht mit den PowerShell-Gegenstücken wie z.B. "Get-MgBetaOnPremisePublishingProfileConnectorGroupMember" der MgGraphPowerShell heran.

Mit dem Connect am Anfang und der passenden URL mit etwas Parsing habe ich aber des Status

Install-Module Microsoft.Graph

Connect-MgGraph `
   -TenantId msxfaqlab.onmicrosoft.com `
   -Scopes AuditLog.Read.All,Directory.AccessAsUser.All,email,openid,profile

$Result = Invoke-MGGraphRequest `
             -Uri 'https://graph.microsoft.com/beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/?$expand=members' `
             -OutputType PSObject 
$status = ($Result.value | where {$_.connectorGroupType -eq "exchangeOnline"}).members.status
$status

Die Ausgabe ist dann einfach "Inactive" oder Active". Die meisten Firmen werden immer nur genau einen Modern Hybrid Agent installiert haben.

Berechtigungen

Es gilt auch hier das Prinzip der "geringsten Rechte" (Least Privilege). Leider ist das beim Abrufen des Status nicht sehr hilfreich, wenn mittels Graph Explorer (https://aka.ms/ge) kann ich ermitteln, welche Berechtigungen ich zum Aufruf eines Service benötige. Mit der URL erhalten wir:

Ein "Directory.ReadWrite.All" sollten Sie eigentlich nur sehr vertrauenswürdige Applikationen gewähren und diese auch schützen. Ein "weniger umfangreiches Recht" wie z.B. Application.ReadAll oder zumindest "Directory.Read.All" ist wohl nicht vorgesehen.

Status as App: App Anlegen

Damit ist es nun natürlich nicht mehr weit, solche Abfragen zu automatisieren und von einem Benutzer abzukoppeln. Nur über eine App kommen wir dann an MFA vorbei. Ich habe in meinem Tenant daher eine passende App angelegt. Das geht sehr einfach über das Azure Portal mit Anlage eines Client Secret und der Zuweisung der Berechtigungen. Da wir aber schon MGGraph gestartet haben, schreibe ich die MgGraph Powershell-Befehle auf:

param(
   $AppName = "MSXFAQ-HybridAgentStatus"
)


# Sofern noch nicht erfolgt, bitte installieren oder aktualisieren.
Install-Module Microsoft.Graph
#Update -Module Microsoft.Graph

# Gesondert Importieren müssen wir die relevanten mi PowerShell 5/7 nicht mehr
# Import-Module Microsoft.Graph.Authentication
# 

# Bitte interaktiv mit einem Admin-User 
# Sie benötigen die Entra ID Rolle "Application Adminstrator" oder "Global Administrator"
# Evemtuell müssen Sie noch "Consent" erteilenn.
Connect-MgGraph -Scopes Application.Read.All,Application.ReadWrite.All,User.Read.All


# Nun legen wir eine neue App mit dem Namen an
$App = New-MgApplication -DisplayName $AppName 

# Als Muster nutze ich hier ein App-Kennwort
# Es sind maximal 2 Jahre Gültigkeitsdauer möglich
$AppCred = @{
    "displayName" = "$($Appname)-Credentials"
    "endDateTime" = (Get-Date).AddMonths(24)
}

$AppSecret = Add-MgApplicationPassword `
                   -ApplicationId $App.id `
                   -PasswordCredential $AppCred

# Ausgabe der beiden Daten für die spätere Nutzung
Write-Host "AppID    : $($App.Id)"
Write-Host "AppSecret: $($AppSecret.SecretText)"
$App.PasswordCredentials


# Reply Addressen setzen
$RedirectURI = @("https://login.microsoftonline.com/common/oauth2/nativeclient")
Update-MgApplication `
   -ApplicationId $App.ID `
   -IsFallbackPublicClient `
   -PublicClient @{RedirectUris = @($RedirectURI)}


#Add Application Permission
#Directory.ReadWrite.All    Application    19dbc75e-c2e2-444c-a770-ec69d8559fc7
$params = @{
    RequiredResourceAccess = @(
        @{
            ResourceAppId = "00000003-0000-0000-c000-000000000000"
            ResourceAccess = @(
                @{
                    Id = "19dbc75e-c2e2-444c-a770-ec69d8559fc7"
                    Type = "Role"
                }
            )
        }
    )
}
Update-MgApplication -ApplicationId $App.ID -BodyParameter $params


# Admin Consent einholeb
$URL = "https://login.microsoftonline.com/$($App.PublisherDomain)/adminconsent?client_id=$($App.AppID)"
# Browser starten, um als Admin einmalig den Comsent für die App Permission einzuholen
Start-Process $URL

Wir können sehr viel in den Apps per PowerShell konfigurieren, aber der Admin Consent ist nicht einfach automatisierbar.

Wer natürlich im Hintergrund dem Azure Portal bzw. Browser auf die Finger schaut, kann den Aufruf ans Backend auch synthetisch nachbauen.

Kontrolle Azure Portal

Im Azure Portal finden wir dann die App samt (nicht mehr einsehbarem) AppSecret und Berechtigungen. Eventuell müssen Sie von "Owned Applications" auf "All Applications" klicken, da ich per PowerShell keinen Owner geflegt habe

Im Bereich "Certificates & Secrets" sehen Sie den Eintrag für das App-Kennwort.

Und unter API-Permissions die Berechtigungen

 

Nun können wir den Code auf "Anmeldung als App" umstellen.

$AppID="8d5f94a2-499b-460d-9062-deb1cd7f4e37"
$AppSecret="<hier muss das lange App Secret rein"

$AppPass = ConvertTo-SecureString -String $AppSecret -AsPlainText -Force 
$AppCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AppId, $AppPass

Connect-MgGraph `
   -TenantId msxfaqlab.onmicrosoft.com `
   -ClientSecretCredential $AppCred	

$Result = Invoke-MGGraphRequest `
             -Uri 'https://graph.microsoft.com/beta/onPremisesPublishingProfiles/applicationProxy/connectorGroups/?$expand=members' `
             -OutputType PSObject 
$status = ($Result.value | where {$_.connectorGroupType -eq "exchangeOnline"}).members.status
$status

Nun ist es natürlich an ihnen, denn Code in geeigneter Weise zu erweitern, damit er in ihr Systemmonitoring integriert werden kann.

Weitere Links