ADSync Webservice

Wenn ich von ADSync aus der Ferne einen Status abfragen oder ein "SyncNow" starten möchte, geht das offiziell nur per PowerShell, WMI, RemotePowerShell mit Authentifizierung und einem privilegierten Konto. Bei Azure Functions  habe ich gelernt, die einfach Rest-APIs mit einem Secret funktionieren. Also habe ich mir per PowerShell sowas mal schnell selbst gebaut. Im Grund ist es aber ein generischer Service, mit dem Sie jegliches PowerShell-Skript oder Programm starten können.

Anforderungen

Fast alle mittlere oder größere Firmen nutzen ADSync / AADConnect, um lokale Identitäten (Benutzer, Gruppen, PCs) mit einem AzureAD abzugleichen. Es gibt zwar auch eigene Alternativen, z.B. per Graph oder AzureAD Cloud Sync aber ADSync ist deutlich in der Mehrheit. Allerdings sollte der Computer natürlich gut abgesichert sein, insbesondere, wenn er auch die Kennworte als Hashwert per Password Hash Sync (PHS) aus dem AD auslesen und in die Cloud übertragen darf. Auch die lokale SQL-Datenbank enthält sehr viele schützenswerte und personenbezogene Informationen. Dennoch sehe ich einen Bedarf für folgende Aktionen:

  • Trigger
    Wenn eine 3rd-PartyProvisioning-Lösung im lokalen Active Directory Änderungen umsetzt, dann sind die 15/30 Minuten Verzögerung sehr lange. Ein "SyncNow" kann ich als ADSync-Admin natürlich lokal als auch per Remote PowerShell ausführen. Aber dazu muss sich der Prozess mit privilegierten Anmeldedaten authentifizieren. Für nur einen "Sync" anschubsen ist mir das etwas viel
  • Abfragen
    Manchmal hätte ich schon gerne einen Detailbericht eines Benutzers aus dem Metaverse. Aber auf der Konsole mit Berechtigungen möchte ich ungern arbeiten, müsste ein Prozess ja nicht gleich volle Rechte haben.
  • Status
    Natürlich kann ich per Graph in AzureAD den Status des ADSync abfragen. Soweit ich gesehen habe, betrifft dies aber den Export in die Cloud aber nicht die lokalen Verbindungen.

Es gibt also durchaus einige Anknüpfungspunkte, über die ich gerne ADSync abfragen oder anstoßen möchte, ohne dass ich gleich mit einem "Domain Admin" oder "ADSync Admin" agieren oder mich auf dem Server direkt oder per Remote PowerShell anmelden möchte.

Planung

Nach meinen Erfahrungen mit Azure Functions in der Cloud ist der Gedanke gereift, einfach einen Webservice zu bauen, der per HTTPS ansprechbar ist und die Daten nach einer Authentifizierung liefert oder Aktionen auslöst. Dabei möchte ich explizit auf eine klassische Anmeldung per NTLM, Kerberos oder gar Basic verzichten, sondern mir schwebt da eher ein Applikationstoken vor, wie es auch Azure Storage mit dem AccessToken macht. Auch bei der Microsoft Graph API gibt es den Weg einer "Application" mit einer Application GUID und einem Secret oder Zertifikat.

Consultants und Administratoren arbeiten seltener mit C# und Visual Studio sondern eher mit PowerShell. Daher habe ich mir überlegt, dass ein PowerShell-Script per Windows Taskplaner beim Start des Computers und nach seiner Beendigung als System gestartet wird. Wenn ein Angreifer schon auf dem Server ist, dann hat er eh viel mehr Rechte. Dieses Skript startet einen HTTP-Listener, der GET/POST-Requests annimmt und anhand der URL damit verbundene PowerShell-Skripte startet und die Ergebnisse zurück gibt. Da es eher "klein" gedacht ist, macht er das erst mal nicht "Multithreaded" oder massiv parallel. Es gibt nämlich durchaus andere fertige Lösungen, auf die ich auf PowerShell als HTTPServer auch verweise. Mein Ziel ist aber "nur" ein minimaler Webservice für ADSync, den ich auch verstehe. Nutzen könnte ich das dann einfach per Invoke-Webrequest von jedem Client, der das "Geheimnis" kennt.

invoke-webrequest `
    -Method GET `
    -URI https://adsync01.msxfaq.de:44388/?action=syncnow&secrect=ganzgeheimesgeheimnis

Auch das Auslesen von Identitäten aus dem Metaverse könnte genau so gehen.

invoke-webrequest `
    -Method GET `
    -URI https://adsync01.msxfaq.de:44388/?action=query&name=frank&secrect=ganzgeheimesgeheimnis

Und wenn mich ein Status interessiert, wäre das so möglich.

invoke-webrequest `
   -Method GET `
   -URI https://adsync01.msxfaq.de:44388/?action=status&secrect=ganzgeheimesgeheimnis

Mein Rumpf-Skript müsste also nur die HTTP-Requests annehmen und z.B. basierend auf einer CSV-Datei zu jeder Action ein in einer Konfiguration hinterlegtes Skript ausführen und dem Skript per Pipeline die Daten als Objekt übergeben. Es muss ja nicht jedes Skript alles selbst parsen.

Die Rückgabe ist dann idealerweise einfach ein JSON-String oder ein Objekte, welches zu JSON konvertiert wird.

Coding

Ich brauche also eine PS1-Datei, die ich erst interaktiv und später "als Prozess" laufen lasse, einen HTTP-Listener instanziert und die eingehenden Anfragen entsprechend verarbeitet. Dazu braucht das Skript natürlich eine Konfigurationsdatei für den Service selbst. Die je nach URL-Aktion zu startenden Skripte aber als einfache Liste (CSV-Datei) angelegt.

# CSV-Datei mit der Information
action,script,secret,
status,get-status.ps1,123456789
status,get-status.ps1,123456789ABCD
syncnow,invoke-sync.ps1,

Ich habe keinen Zugriffsschutz pro Pfad und Source-IP-Adresse eingebaut. Sie können als einfachen Schutz die Windows Firewall nutzen und ansonsten vertraue ich auf einen langen Secret. So macht das Azure Functions auch.

Da ich keine XXL-Lösung plane, habe ich z.B. den Port und den Zertifikatsnamen und Logging als Parameter hinterlegt.

param (
     [string]$listenparam = "http://+:44388/",  #"https://+:443/",  #"http://+:80/"
     [string]$configfile = ".\config.csv" 
)

Mehr Parameter gibt es nicht. Die eigentliche Konfiguration erfolgt in der CSV-Datei und die wichtige "Business Logik" ist eh in den aufgerufenen Skripten.

ADSync Abfragen

Fast hätte ich vergessen, wozu ich den kleinen Service eigentlich entworfen habe. Ich wollte ja einen Status von ADSync haben oder einen "SyncNow" anschubsen. Die technischen Details habe ich auf ADSync Monitoring schon beschrieben, so dass ich hier nur die Skripte aufführe.

Denken Sie daran, dass die diversen ADSync-Commandlets mit einem Administrator oder einem Konto ausgeführt werden müssen, welches in der lokalen Gruppe "ADSyncAdmins" Mitglied sind.

# Datei get-adsyncstatus.ps1
Write-Host "Get ADSyncConnectorStatistics"
Get-ADSyncScheduler


Write-Host "Get ADSyncConnectorStatistics"
foreach ($connector in (Get-ADSyncConnector)) {
   "Name= $($connector.name)"
   Get-ADSyncConnectorStatistics $connector.name 
}

Write-Host "Get ADSyncRunStepResult -First"
Get-ADSyncRunStepResult -First

Write-Host "Get ADSyncConnectorRunStatus"
Get-ADSyncConnectorRunStatus
# Datei invoke-adsync
"Start Start-ADSyncSyncCycle -PolicyType delta"
Start-ADSyncSyncCycle -PolicyType delta
"End Start-ADSyncSyncCycle -PolicyType delta"

Die CSV-Datei ist ja oben schon dokumentiert, so dass Sie nur noch das Skript brauchen.

adsyncwebservice.ps1.txt

Die Funktion zur Abfrage eines Benutzers aus dem Metaverse habe ich noch nicht umgesetzt. dazu braucht man ja den DN um dann mit "Get-ADSyncCSObject" die Details zu bekommen. Leider habe ich noch keinen Weg zu einer "LDAP-ähnlichen Suche" umgesetzt. Per PowerShell geht es nicht so einfach

Das Skript protokolliert Ausgaben aktuell nur auf die Konsole. Für einen "Hintergrundbetrieb" wäre es schon wünschenswert, wenn Debug-Ausgaben und Zugriffe in einer Protokolldatei landen würden, die nach einigen Tagen weggeräumt wird, dass Fehler und kritische Events im Windows Eventlog landen und auch ein paar Performance Counter wären sicher hilfreich bei der Überwachung.

Run as Service

Früher habe ich BAT/CMD-Dateien mit "SERVANY.EXE" und anderen Tools als Dienst eingerichtet. Das ist heute mit dem Taskplaner von Windows sehr viel einfacher. Wenn ich das Skript automatisch beim Start mit einem bestimmten Konto laufen lassen will, dann geht das sehr einfach. Siehe dazu auch PowerShell und Taskplaner. Sie können hier das Skript auch z.B. nach jeder Stunde beenden und neu starten, so dass auch eine "Garbage Collection" möglich ist.

HTTPS

Es ist natürlich nicht besonders sicher, das "Secret" in der URL per HTTP unverschlüsselt über das Netzwerk zu schicken. Daher sollten Sie in der Produktion dann natürlich ein Zertifikat bereitstellen und HTTPS nutzen. Technisch ist das relativ einfach aber ich habe es nicht im Skript automatisiert. Sie müssen also schon selbst folgende Dinge machen.

  1. Zertifikat mit Private Key bereitstellen
    Ideal ist natürlich ein Zertifikat einer öffentlichen oder internen PKI mit dem Namen des Servers. In der Not tut es aber auch ein "Self signed"-Zertifikat, welches sie mit MAKECERT oder per PowerShell selbst erstellen können
  2. Bindung konfigurieren
    Mit NETSH konfigurieren Sie den HTTP-Listener für die Nutzung des Zertifikats. Sie benötigen dazu den Hashwert.
  3. HTTPS im Skript angeben
    Nun müssen Sie das Skript entweder mit dem Parameter "$listenparam" auf HTTPS umstellen oder sie ändern den Default im Code

Es ist nicht besonders schwer aber ich habe es absichtlich nicht im Code eingebaut, um den Code nicht länger als nötig zu machen. Es ist ja eher ein "Beispiel" und weniger eine produktive Software.

Weitere Links