Eine Möglichkeit, ein Cloud-System maximal zu sichern, ist der Aufbau eines virtuellen Netzwerks, um die maximale Kontrolle über jeglichen Traffic zu erhalten. Alle ausführenden Komponenten müssen dann auf die Versionen upgegradet werden (Integration Service Environment statt normalen Logic App Hosting, Premium App Service Plan, Premium Service Bus Namespace, etc.) die sich in so ein virtuelles Netzwerk integrieren lassen. Kann man manche Komponenten nicht in das Netzwerk direkt hängen, wie z.B. CosmosDB’s, Storage Accounts, etc. gibt es nun die Möglichkeit, für alle diese Komponenten einen privaten Endpunkt anzulegen. Dabei wird ein virtuelles Netzwerkinterface angelegt, das sich in das Netz hängen lässt und somit die Kommunikation innerhalb des privaten Netzes erlaubt bzw. bei manchen Komponenten sogar jeglichen anderen Verkehr unterbindet. Dieses Netzwerkinterface hat dann eine verschlüsselte Kommunikation mit der Zielkomponente und man kann über Firewalls jeglichen anderen Verkehr deaktivieren. Der Clou hinter diesen Endpunkten ist, dass sie nur Traffic durchlassen, der direkt aus dem privaten Netz kommt, auch wenn das Networkinterface ggf. eine Public IP besitzt. Damit beschränkt sich der Zugriff auf unsere Zielkomponenten auf Absender innerhalb des Netzwerkes. Man kann allen Traffic, der in diese eingehen sollen, über Zugriffskomponenten wie Application Gateway, Express Route etc. schützen und kontrollieren.
Hat man in seinem System auch Azure Functions oder App Services im Einsatz gibt es letztendlich zwei Möglichkeiten, die öffentlichen Endpunkte der Komponenten gegen unerwünschte Aufrufer zu schützen. Zum einen wären da die IP Access Restrictions in denen man einfach die gewünschten Aufrufer white-listed. Die Punkte sind letztendlich für diese IP’s immer noch komplett offen und man muss die White-Lists ständig bearbeiten. Möchte man stattdessen einen öffentlichen Endpunkt ganz abschalten/schützen, kapselt man seine Function App hinter einem Private Endpoint und unterdrückt somit jeglichen öffentlichen Traffic. Alle Aufrufe von draußen müssen nun über das App Gateway, WAF, API Management an die Function weitergeleitet werden und wir haben so alle Möglichkeiten, unseren eingehenden Traffic zu validieren bzw. zu kontrollieren.
Problem:
Nachteil an dieser Methode für Functions und App Services, mal abgesehen von den höheren Kosten für die großen Versionen der beteiligten Komponenten, ist, das man die Komponenten wirklich nur noch durch die privaten Endpunkte erreicht. Jeglicher Traffic, der nicht über eine Absender IP von innerhalb des Netzwerkes kommt, kann auch nicht mehr durchgelassen werden. Im Falle von Aufrufen unserer Methoden wollen wir dies zwar, jedoch werden auch die Möglichkeiten unterbunden, Code von draußen (Visual Studio, DevOps, etc.) zu den Funktionen zu publishen. Das ist ein sehr ärgerliches Thema, da alle möglichen Workarounds hierfür relativ aufwendig sind. So könnte man sich zum Beispiel einen Build-Agent bzw. eine virtuelle Maschine in das Netz hängen, um von dort aus Code zu publishen. Jedoch hat man dann die Kosten und den Maintenance-Aufwand hierfür am Hals. Auch die Privaten Endpunkte vor dem Publish zu löschen und danach wieder zu erstellen, wirkt nicht wirklich effizient.
Lösung:
In meinem letzten Projekt hatte ich dieses Szenario vorliegen und musste eine effiziente Lösung finden, mit der dieses Problem aus der Welt geschafft werden konnte. Entschieden habe ich mir für eine Lösung, für die ich mir das ZIP-Deploy von Azure Functions zunutze mache. Dies ist ein Mechanismus, bei dem man ein fertig gepacktes Zip direkt auf die Funktion hochlädt und alle MS Deployment Mechanismen umgeht. Aber auch hier stellt sich für uns das Problem, dass wir das nicht von außen erledigen können. Da wir aber keine direkten MS Deplyoment Verfahren mehr verwenden, sondern nur noch über ein Paket reden, dass von A nach B geschoben werden muss, kann man hierfür einen Zwischenschritt in Form einer weiteren Function App einbauen. Diese Funktion macht nichts anderes, als den originalen Aufruf entgegenzunehmen, sich um die Authentifizierung/Autorisierung des Aufrufes zu kümmern und schlussendlich das Paket, jetzt aber innerhalb des privaten Netzes, auf die Function zu publishen. Da sich diese Funktion vom Code her in der Regel nicht ändern sollte, können wir auch diese Function mit einem priavten Endpunkt schützen und alle unsere Functions sind nach außen hin geschützt. Angefangen hatte ich mit der Idee, den Publish über einen codelosen Redirect im API Management zu machen, konnte aber diese Möglichkeit technisch nicht umsetzen (Vielleicht gelingt es ja jemand anderem dieses Problem zu lösen). Auf der anderen Seite verlagerte sich der Fokus aber auch schnell auf die Codevariante, um ein Autorisierungskonzept verwirklichen zu können. Dieses Konzept rührt letztendlich daher, dass das Zip-Deploy bei seiner Authentifizierung relativ rudimentär geblieben ist: Basic ist sowohl Pflicht als auch die einzige Möglichkeit, die uns gegeben wird. Die Daten hierfür liegen im Publish Profile der Funktion:
Sample Publish Profile:
<publishData>
<publishProfile profileName="FA-SampleTargetFunctionForZipPublish - Web Deploy"
publishMethod="MSDeploy"
publishUrl="fa-sampletargetfunctionforzippublish.scm.azurewebsites.net:443"
msdeploySite="FA-SampleTargetFunctionForZipPublish"
userName="$FA-SampleTargetFunctionForZipPublish"
userPWD="WC3f2qjABo7dDoQ2iqlpLlL5RGPrCuvctaTbRf4JnbqDNx39f3ohe7hlnZij"
destinationAppUrl="http://fa-sampletargetfunctionforzippublish.azurewebsites.net"
SQLServerDBConnectionString=""
mySQLDBConnectionString=""
hostingProviderForumLink=""
controlPanelLink="http://windows.azure.com"
webSystem="WebSites">
<databases/>
</publishProfile>
<publishProfile profileName="FA-SampleTargetFunctionForZipPublish - FTP"
publishMethod="FTP"
publishUrl="ftp://waws-prod-am2-379.ftp.azurewebsites.windows.net/site/wwwroot"
ftpPassiveMode="True"
userName="FA-SampleTargetFunctionForZipPublish\$FA-SampleTargetFunctionForZipPublish"
userPWD="WC3f2qjABo7dDoQ2iqlpLlL5RGPrCuvctaTbRf4JnbqDNx39f3ohe7hlnZij"
destinationAppUrl="http://fa-sampletargetfunctionforzippublish.azurewebsites.net"
SQLServerDBConnectionString=""
mySQLDBConnectionString=""
hostingProviderForumLink=""
controlPanelLink="http://windows.azure.com"
webSystem="WebSites">
<databases/>
</publishProfile>
</publishData>
In diesem befinden sich neben den Informationen zu dem Endpunkt für den Publish auch die Basic Credentials zur Authentifizierung. Nutzen kann man dies nun, in dem man sich diese Informationen nicht speichert, sondern sich bei jedem Aufruf der Publish Funktion aufs Neue zieht. Wenn man sich diese Informationen ziehen will, muss man sich ganz normal gegen Azure authentifizieren, also zum Beispiel über Oauth Token. Wir nehmen also den Access-Token, den wir im API Management erhalten haben und leiten diesen weiter an unsere Publish Funktion. Dort nehmen wir diesen Token für die Authentifizierung gegen den Azure Ressource Manger beim Holen des Publish Profiles und haben so sofort schon überprüft, ob der übergebene Access Token auch die Berechtigung hat, Code zur Azure Funktion zu publishen. Also zusammengefasst: Authentifizierung (Aufruf -> APIM), Autorisierung (APIM -> Ressource Manager)
Der Rest der Funktion schneidet sich ansonsten nur die Credentials aus dem Publish Profile und führt den eigentlichen Publish über einen HTTP Post gegen die Zielfunktion aus, inklusive Basic Header.
Prerequisites für diese Lösung:
- API Management
- Virtuelles Netzwerk
- Premium App Service Plan der Functions
- Publishing Function braucht VNET-Integration
Optional wären hier noch Application Gateway und Web Firewall, um das Maximum an Kontrolle über den eingehenden Verkehr zu erlangen.
Technische Umsetzung:
Zu erst schreiben wir den Code für unsere Publish Funktion. Diese sollte letztendlich drei Punkte implementieren: Publish Profile ziehen, Request mit extrahierten Credentials zusammenbauen, Code publishen. Als Parameter soll die Funktion das fertige Zip-Paket bekommen und einen OAuth Token im Header. Als erstes besorgen wir uns das Publish Profil, um den aktuellen Endpunkt und die Credentials dazu zu bekommen. Endpunkt hierfür ist immer: https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Web/sites/{functionName}/publishxml?api-version=2015-08-01
(Die geklammerten Werte müssen hier natürlich ersetzt werden)
Dieser Aufruf muss natürlich genauso authentifiziert werden. Hier zu fügen wir an unseren Request den Authorization Header aus unserem originalen Aufruf hinzu. Hierbei können wir dann auch gleich das Thema Autorisierung abhaken, da wir nur das Publish Profile zurückbekommen, wenn der Service Principal hinter dem OAuth Token auch die richtige Berechtigung auf unsere Zielfunktion hat (RBAC).
Das zurückkehrende XML ist in die diversen Möglichkeiten aufgebaut. Mit ihm können wir den Code an die Funktion publishen (http/ftp/etc.). In meinem Beispiel habe ich den MSDeploy Block genommen. Aus diesem schneiden wir uns nun die Ziel URL sowie die den Usernamen und das Passwort heraus. An die URL fügen wir noch den </api/zipdeploy> an. Also zum Beispiel: https://FunctionName.scm.azurewebsites.net/api/zipdeploy. Aus den Credentials bauen wir ein Basic Header: Basic Username:Passwort wobei UserName:Passwort Base64 decoded sein muss. Diesen Wert packen wir in unseren Authorization Header für unseren Publish Aufruf. Danach können wir unseren Request als Post und application/zip auf unsere Zielfunktion publishen (z.B. WebRequest.Create(functionUrl)) (Natürlich Response-Verarbeitung nicht vergessen). An dieser Stelle ist unser Code schon fertig und wir können uns unserem Azure Setup zuwenden.
Als ersten erstellen wir uns hierzu eine Funktion App, auf die wir unseren Code publishen. Da wir mit privaten Endpunkten kommunizieren wollen, muss diese auf einem Premium Service Plan gehostet sein. Für einfache VNet-Integration würde zwar der Standard Plan reichen. Für Private Endpoints muss man hier aber auf Premium gehen. Danach integrieren wir unsere Funktion in unser VNet. Dies brauchen wir um ausgehend in unser VNet senden zu können. Damit wir dabei aber auch eine private IP als Absender tragen, braucht es noch zwei App Konfigurationen:
{
"name": "WEBSITE_DNS_SERVER",
"value": "168.63.129.16"
},
{
"name": "WEBSITE_VNET_ROUTE_ALL",
"value": "1"
}
Die IP des DNS-Servers ist hierbei immer gleich. Sobald unser Code gepublished ist, können wir auch vor diese Funktion einen privaten Endpunkt spannen. Der Code sollte sich ja nicht mehr ändern und jeglicher eingehender Traffic kommt über das API Management. Hier sind wir auch gleich beim nächsten ToDo: Wir erstellen uns eine API, um unsere Funktion zu kapseln. Wichtig sind hierbei nur zwei Dinge: Zum einen sollte sich diese API bereits um die Authentifizierung des JWT Tokens kümmern und zum anderen müssen wir natürlich auch sicherstellen, dass dieser Header nicht abgeschnitten, sondern an unsere Publish Funktion weitergeleitet wird (Thema Autorisierung).
Et Voila. Von nun an haben wir einen öffentlich erreichbaren Endpunkt, über den wir Code an unsere Funktion publishen können, ohne diese aus unserem geschützten Netzwerk herausnehmen zu müssen.
Ich hoffe dieser Artikel war für euch interessant und hilfreich. Falls ihr Fragen haben solltet, wendet euch gerne an mich.
Beste Grüße aus Stuttgart
Ben
PS: mein nächster Artikel wird sich darum drehen, wie man diesen Mechanismus in seine CI/CD Pipelines einbaut, um den Prozess vollständig automatisieren zu können.