PowerShell Keyvault

Ich stelle hier einen Weg vor, wie ich Konfigurationen meiner PowerShell-Skripte in eine verschlüsselte Datei auslagere und optional sogar von einem Server bereitstelle.

Beispiel Graph

In der letzten Zeit habe ich immer mal wieder Code-Samples in Microsoft Graph geschrieben. Für die Anmeldung einer Applikation an Graph muss ich sich mein Code aber mit einer AppID und AppSecret authentifizieren. Diese Daten erhalte ich bei der Registrierung der App im AzureAD und sollte sie besonders gut schützen.

Es gibt zwar Apps, die zusätzlich noch eine Anmeldung durch den Anwender oder Administrator erfordern, damit sie mit der "Delegate"-Berechtigung arbeiten aber es gibt da auch noch die "App-Permission", mit der ich dann allein mit AppID und AppKey auf Daten zugreifen kann. Ein besserer Schutz kann ich durch die Nutzung eines Zertifikats als Authenticator nutzen, wenn ich das Zertifikat dann auch "nicht exportierbar" auf dem System hinterlege. Für erste Schritte ist das aber aufwändiger und so finden sich sehr viele Code-Samples, in denen die App-Daten einfach als Konstanten im Code hinterlegt sind, z.B.:

$LoginUrl="https://login.microsoftonline.com"  # Graph API URLs
$ResourceUrl="https://graph.microsoft.com"     # Ressource API URLs
$AppID="12345678-1234-1234-1234-123456789abc"  # App ID aus dem Azure Portal
$Appkey="xxxxxxxxx"                            # App Key 
$TenantName="msxfaq.onmicrosoft.com"           # Tenant name

Da bin ich auch keine Ausnahme. Aber nur weil es alle machen, ist es ja nicht gut. Dies gilt umso mehr, wenn Skripte und Samples z.B. auf GitHub oder anderen Plattformen geteilt werden. Allzu leicht landen Zugangsdaten auf öffentlichen Servern. da ist eine Verbindung zu einem lokalen System mehr als nur eine Kür.

Skript oder Config-Datei"

Auf der Seite PS Passwort / Kennwort habe ich das Thema mit Kennworten in PowerShell-Skripten ebenfalls schon thematisiert. Auch Kennworte sind leichte Opfer, wenn Sie in einem Skript hinterlegt sind und Visual Studio Code weist auch klar darauf hin, wenn Sie z.B. Password als Variablennamen verwenden:

Meine Überlegung ist daher, die Konfiguration eines PowerShell-Skripts in eine XML oder JSON-Datei auszulagern. Gerade Dateien wie "app.config" oder "scriptname.config" sind aus der .NET-Entwicklung schon lange geläufig.

PowerShell selbst ist aber eine Shell und kein Programm. Ich habe noch keinen Weg gefunden, wie PowerShell .z.B. neben einer "skriptname.ps" auch eine "Skriptname.config" automatisch mit einliest. Das überlässt PowerShell dem Entwickler.

Aber mich hindert ja niemand daran, eine XML oder JSON-Datei neben mein Skript zu legen und dieses beim Start einzulesen und die Parameter daraus zu extrahieren. Das geht per JSON sehr einfach. Ich könnte z.B. die ganze erforderlichen Informationen einer Graph-App in der folgenden JSON-Datei ablegen:

{
   // Graph API URLs
   "LoginUrl"   : "https://login.microsoftonline.com",
   // Ressource API URLs
   "ResourceUrl": "https://graph.microsoft.com", 
   // App ID aus dem Azure Portal
   "AppID"      : "12345678-1234-1234-1234-123456789abc",
   // App Key
   "Appkey"     : "superecretstring",
   // Tenant name
   "TenantName" :"msxfaq.onmicrosoft.com"
}

Diese Datei kann ich mit wenigen Zeilen einlesen:

$config = get-content "c:\temp\graph.config" | ConvertFrom-Json
$config

LoginUrl : https://login.microsoftonline.com
ResourceUrl : https://graph.microsoft.com
AppID : 12345678-1234-1234-1234-123456789abc
Appkey : superecretstring
TenantName : msxfaq.onmicrosoft.com

Das kann aber eine Malware genauso und wenn das Verzeichnis des Sourcecode gleich mit extrahiert wird, dann sind die Daten im schlimmsten Fall der gesamten Welt bekannt.

Es wäre nicht das erste mal, dass in  Code auf Github auch Anmeldedaten veröffentlicht werden.

So ist das also alles andere als "sicher"

Web Password Vault

Ich könnte die Zugangsdaten schon alleine damit sichern, dass ich diese nicht auf dem Computer vorhalte, sondern z.B. einer zentralen Credential Datenbank. Mein Skript könnten sich die Daten dann von dort holen, z.B. mit:

$config = Invoke-Restmethod https://credential.msxfaq.de/<credentialguid>

Natürlich muss sich das Skript auch hier irgendwie legitimieren aber da sind schon mehrere Stufen möglich, z.B.

  • Source-IP oder Computer
    Wenn das Skript auf einem bekannten Computer ausgeführt wird, dann kann der Webserver z.B.: die IP-Adresse als erste Kriterium nutzen. Optional wäre natürlich noch eine integrierte Authentifizierung durch das Computerkonto oder ein Dienstkonto bzw. GMSA - Group Managed Service Account möglich.
  • Interner Server
    Wenn es nur um interne Systeme geht, dann könnte die URL z.B.: nur intern erreichbar sein. Das Wissen um die URL würde erst einmal keinen Zugriff erlauben. Allerdings ist es für Angreifer recht einfach per Phishing oder eine Webseite mit JavaScript solche "bekannten" URLs auszulesen und die Informationen auszuleiten.
  • GUID statt Namen
    Ich bin zwar kein Freund von "Security by Obscurity" aber GUIDs zu erraten ist nicht einfach und wenn der Webserver einfach immer etwas zurück gibt und die Anfragen "drosselt", ist es es schon schwer, gültige Konfigurationen zu erraten und der Webserver könnte solche Zugriffe auch erkennen.
  • Logging
    Über die Protokollierung des Service könnte auch ermittelt werden, welche Kennworte von vom abgefragt werden

Das hilft natürlich nur etwas, wenn ein Angreifer "nur" den Code selbst hat aber keinen legitimen Zugriff auf den Vault. Wenn er aber schon "auf dem Computer" selbst ist, dann kommt er natürlich an die Daten heran. Wenn die Information selbst nicht verschlüsselt ist, muss ich noch HTTPS verwenden und der Schutz basiert eigentlich nur darauf, dass die URL nicht allgemein erreichbar ist.

Das reicht mir noch nicht.

Managed Service Account

Windows kennt schon einige Jahre das Prinzip des GMSA - Group Managed Service Account, damit Sie Dienste aber auch geplante Tasks nicht mehr mit einem "Benutzerkonto" starten sondern mit einem speziellen Computerkonto, über das Windows wacht und z.B. auch die Kennworte regelmäßig ändert. Das Verfahren ist relativ einfach auch für Skripte zu nutzen. Allerdings wird dann das Skript mit dem Benutzer gestartet. Wenn ihre Software dann aber auf einen anderen Server zugreifen will, dann muss das GMSA-Konto sich dort über die "integrierte Authentifizierung" anmelden können, z.B.: NTLM-Anmeldung oder Kerberos.

Das reicht also nicht, wenn Sie sich mit davon unabhängigen Credentials z.B. an einem Cloud-Service anmelden müssen. Aber ein GMSA könnte sich ja an einem entsprechenden Kennwortserver anmelden, um die Zugangsdaten temporär zu erhalten.

Azure Key Vault

Microsoft hat sich in der Cloud dem Thema etwas anders gewidmet. Auch da kann ich Zugangsdaten in einem zentralen Tresor hinterlegen und die Anwendungen holen sich die Daten bei Bedarf. Da hier aber der "Skript Host" in Azure läuft und jeder Code damit identifizierbar ist, ist eine viel einfacher die Daten nur an legitimen Code zu geben.

Wenn Sie z.B. PowerShell als Teil einer Azure Funktion verwenden, sollten Sie keine Credentials dort hinterlegen sondern "Special Placeholder", die dann vom Azure Host beim Start ersetzt werden.

Diese Funktion steht aber "On Premises" nicht zur Verfügung.

SecureString und Dateien

Aber wir können noch einen weiteren Weg gehen, um unsere Konfiguration auf dem System besser zu schützen, so dass sie weder einfach einsehbar noch anderweitig verwendet werden kann. PowerShell kennt die Commandlets "convertfrom-securestring" und "Convertto-SecureString", welche nicht nur Credentials sondern auch beliebige Strings ein ein verschlüsseltes Format konvertieren und wieder zurückholen können. Es bedient sich dabei der "Windows Data Protection API", welche, vereinfacht ausgedrückt, das Kennwort des angemeldeten Benutzers als Schlüssel verwendet. Die so gesicherten Informationen lassen sich nur auf dem gleichen Computer mit dem gleichen Dienstkonto wieder lesbar machen.

Wenn Sie die Anleitung der beiden Commandlets genau lesen, dann ist der "SecureString" der Mittelpunkt und nicht der "Encryped String" oder "plain text".

Meine Idee ist es die zu schützende Konfiguration in einem String als JSON/XML-Datei anzulegen. Den kann ich mit "ConvertFrom-JSON" sehr einfach in dein PSCustomObject überführen und im Skript nutzen. Ich kann den String aber auch über die Zwischenstation "System.Security.SecureString" in einen "Encrypted String" überführen und als Datei schreiben und wieder lesen. Als Parameter brauche ich dann nur die dynamische veränderlichen Schalter und den Pfad zur Konfigurationsdatei. Sie können die Schritte recht einfach nachvollziehen:

  • Anlage der Klartextkonfiguration
    zuerst erstelle ich den String mit der Klartextkonfiguration:
[string]$appconfig = '
{
   // Graph API URLs
   "LoginUrl"   : "https://login.microsoftonline.com",
   // Ressource API URLs
   "ResourceUrl": "https://graph.microsoft.com", 
   // App ID aus dem Azure Portal
   "AppID"      : "12345678-1234-1234-1234-123456789abc",
   // App Key
   "Appkey"     : "superecretstring",
   // Tenant name
   "TenantName" :"msxfaq.onmicrosoft.com"
}'
  • Konvertierung PSCustomObject
    Um zu sehen, dass ich mich irgendwo vertippt habe, probiere ich gleich die JSON-Umwandlung aus
$appconfig |ConvertFrom-Json

LoginUrl    : https://login.microsoftonline.com
ResourceUrl : https://graph.microsoft.com
AppID       : 12345678-1234-1234-1234-123456789abc
Appkey      : superecretstring
TenantName  : msxfaq.onmicrosoft.com
  • Speichern als verschlüsselte Datei
    Den JSON-String werde ich nun erst in einen SecureString wandeln, den ich dann wieder einen encryptedString wandle und ine ine Datei schreibe:
$secureappdata = ConvertTo-SecureString -String $appconfig -AsPlainText
$cryptedappdata = ConvertFrom-SecureString -SecureString $secureappdata
$cryptedappdata | out-file appconfig.secured

Die resultierende Datei enthält nur noch einen Bitstrom, der aber nun nicht von jedem System gelesen werden kann.

Vielleicht glauben Sie mir ja nicht, dass diese Datei nur decodiert werden kann, wenn ich es mit meinem Windows Anmeldekonto mache. Daher stelle ich ihnen hier die Datei einfach zur Verfügung:

ps_keyvault.appconfig.secured.txt

Viel Spaß beim Versuch diese Datei wieder "lesbar" zu machen. Es "sollte" nicht möglich sein. wenn sie nicht den passenden Schlüssel haben, den es nur auf meinem Test-PC mit Test-Benutzer gibt. Die Schritte dazu sind:

  • Einlesen und Decodieren der Datei
    Auf dem gleichen PC mit dem gleichen Benutzer kann ich die Information einfach wieder zurückholen.
# Einlesen der Datei in einen SecureString
$secureappdata2 = (ConvertTo-SecureString -String (gc appconfig.secured))

# Umwandeln in Klartext JSON-Object und Ausgabe als PSCustomObject
ConvertFrom-SecureString -SecureString $secureappdata2 -AsPlainText | ConvertFrom-Json

LoginUrl    : https://login.microsoftonline.com
ResourceUrl : https://graph.microsoft.com
AppID       : 12345678-1234-1234-1234-123456789abc
Appkey      : superecretstring
TenantName  : msxfaq.onmicrosoft.com

Wenn wie wirklich eine Konfigurationsdatei auf mehreren Systemen nutzen wollen, dann können Sie bei ConvertTo-SecureString und ConvertFrom-SecureString natürlich einen eigenen Schlüssel angeben. Den müssen Sie dann aber auch wieder irgendwie absichern. Das wäre dann aber wieder über den Maschinenschlüssel möglich.

Die "verschlüsselte" Information macht es natürlich einfach, den kompletten Code samt der Datei auch "irrtümlich" z.B. auf GitHub bereit zu stellen. Es kann ja niemand etwas damit anfangen. Allerdings muss ich nun mehrere Dinge addieren:

  • Code muss die Information einlesen
    Ich muss die Datei lesen, decodieren und auch mit dem Fehler umgehen, wenn Sie nicht da ist oder nicht entschlüsselt werden kann.
  • Code muss das Konfigurations-Objekt nutzen
    Natürlich muss ich im Code nun nicht mehr die Variable aus der "Param"-Sektion nutzen, sondern das Konfigurationsobjekt.
  • Config-Datei anlegen
    Auch die Anlage der Datei muss berücksichtigt werden. Aktuell ist das natürlich ein manueller Prozess.

Aktuell habe ich mir das so vorgestellt, dass die Konfiguration im Code im Klartext stehen kann und dann verschlüsselt exportiert wird. Danach kann sie im Code "geleert" werden.

Praktische Umsetzung

Zuerst dachte ich an ein Modul oder eine Klasse, die ich in meinem PowerShell-Skripten verwende und vielleicht per WinGet, NuGet o.ä. verfügbar mache. Letztlich ist die Schöpfungshöhe aber überschaubar und letztlich lege ich die Konfiguration nur einmal an und muss sie im Skript dann immer nur noch einlesen und verwenden.

  1. Konfiguration als JSON-Datei anlegen
  2. JSON-Datei in encrypted File exportieren
  3. Im Code den Import einbinden und den Zugriff auf die Einstellungen umsetzen

Der Code um aus einer Klartext-JSON-Datei den Encrypted String zu machen, passt in wenige Zeilen:

$secureappdata = ConvertTo-SecureString -String (get-content scriptname.config) -AsPlainText
ConvertFrom-SecureString -SecureString $secureappdata | out-file scriptname.secureconfig
remove-item scriptname.config

Im eigentlichen Skript muss ich am Anfang dann nur noch folgendes einfügen und die Werte im Code verwenden:

$secureappdata2 = (ConvertTo-SecureString -String (get-content appconfig.secured))
$config= ConvertFrom-SecureString -SecureString $secureappdata2 -AsPlainText | ConvertFrom-Json

Im Code kann ich dann mit $config die einzelnen Einstellungen holen.

Den "Get-Content"-Teil können Sie natürlich auch mit einem "Invoke-WebRequest" ersetzen, um die Daten von einem Webserver abzurufen. Dann müssen Sie lokal im Code-Verzeichnis überhaupt keine "Credential-Datei" speichern.

Ein kleines Problem gibt es natürlich noch. Wenn die verschlüsselte Konfiguration dennoch z.B. auf GitHub landet und jemand anderes seine eigene Konfiguration dann auch im Rahmen eines Pull/Merge als "neue Version" einspielt und die anderen Nutzer dies nicht bemerken, dann ist deren Konfiguration natürlich auch überschrieben. Daher würde ich den Dateinamen der Konfiguration selbst als Parameter übergeben oder die Daten direkt aus einem Keyvault abholen. Die verschlüsselte Text-Information kann ja quasi auch per HTTP übertragen werden.

Vielleicht baut ja jemand mal ein "PowerShell Keyvault" als kleine Webapplication, über die ich einen encrypted-String einlagern und per Invoke-Webrequest einfach wieder abrufen kann.

Für den Anfang reicht übrigens ein beliebiger Webserver, auf dem sie eine "encrypted Textdatei" bereitstellen, auf den Sie die gerechnete Konfig-Datei einfach hochladen.

Eigentlich ist es egal, wo sie die Datei ablegen. Sie kann auf dem Server selbst liegen aber auch auf einem anderen Server, von dem sich das PowerShell-Skript die Datei holt und dann lokal im Speicher decodiert. Ich sehe zwei Vorteile einer Ablage auf einem anderen Server:

  • Nicht Teil des lokalen Dateisystems
    Wenn ich die Information direkt von einer Quelle (HTTP oder SMB) in eine Variable im Speicher lese und dirt decodiere und verwende, dann entfallen Risiken wie z.B. Credentials im Backup oder auf dem lokalen Dateisystem.
  • Nicht im Repository
    Wenn Sie den Code entwickeln und die Dateien z.B. auf GitHub verwalten, landen so die Daten nicht aus Versehen bei anderen Personen. Aber selbst wenn, wären Sie zumindest nicht im Klartext lesbar
  • ACLs
    Wenn die zentrale Ablage dies erlaubt, können Sie den Abruf der verschlüsselten Informationen über ACLs, z.B. auf die Source-IP-Adresse oder das Computerkonto/Dienstkonto beschränken. Das erschwert den Zugriff auf die sowieso verschlüsselte Datei.
  • Tracking
    Zuletzt können Sie auf dem Server natürlich protokollieren, wer welche Zugangsdaten abgerufen hat.

Die Sicherheit des "Keyvault" steht und fällt aber mit der Sicherheit des lokalen Betriebssystems und des Dienstkontos mit dem damit verbundenen Schlüsselmaterial. Wenn ein Angreifer auf dem Server Zugriff als Dienstkonto hat, dann kann er auch die Credentials abrufen und decodieren.

Weitere Links