BLOG 13 January 2022
In every Microsoft 365/Azure environment there are multiple Service Principals. Service Principals can be used for your own custom-built apps
In every Microsoft 365/Azure environment there are multiple Service Principals. Service Principals can be used for your own custom-built apps, to deploy Azure resources through Azure DevOps, or to integrate with third applications.
Authentication with a Service Principal happens through the OAuth 2.0 protocol. In order to retrieve an access token, the tenant ID and application ID must be included into the secret in addition to an authentication method. The latter, for a Service Principal with application permissions, is a client secret or certificate.
While Service Principals offer some advantages over service accounts, their security should not be neglected. The client secrets or certificates are often stored in a non-secure place (like on the personal computer of a developer or unencrypted inside source code), which makes them vulnerable in case of an attack (hack, etc.). While user accounts can be protected with Conditional Access, this is not the case for Service Principals. This means that we have no way of restricting from where a service principal can authenticate.
Mostly, Service Principals are used within predefined locations and workloads. For example:
Currently, there is no way to restrict access to only these Azure services. Too many times, I see that some people are using production Service Principals (with access rights to the entire environment) locally from their computer.
A current preview in Azure AD allows you to see these service principal logs and also stream these to Log Analytics (which can be used by Azure Sentinel). In this blog post, we will walk you through a solution that will create an incident in Azure Sentinel when a Service Principal is used from an IP address other than the ones used for the Azure services mentioned above.
We will retrieve the current Azure Cloud Public IP addresses, save them into an Azure Sentinel watchlist through the API and create an Analytics Rule that will create an incident when that happens.
In order to monitor Service Principal sign-ins, we need to ingest that data into Azure Sentinel. To do so, navigate to the Azure Active Directory blade inside of the Azure portal and select ‘Diagnostic settings‘. If you are already ingesting sign-ins logs into Sentinel, you will see an entry here. Otherwise click ‘create’.
Be sure to select the ‘ServicePrincipalSigninLogs’ in order to ingest the right data into Azure Sentinel.
Note that it can take a while before the first data shows up into Azure Sentinel. Also note that ingestion isn’t instant, there is a delay between the sign-in, it being recorded in the Azure AD audit logs, and the entry being synchronised to the Log Analytics workspace. Typically this can be anywhere from a couple of minutes to 15 minutes.
To know which IP’s the Service Principal is allowed to sign in from, we will be using a watchlist in Azure Sentinel. Watchlists are an amazing tool to setup an allow or block lists to use across different analytics rules.
Depending on your needs, you might want to create multiple watchlists. If your Service Principals are logging in from on-premises datacenters, you could add your own public IP ranges here too.
This blog post focuses on monitoring Service Principals which are running inside Microsoft’s online services. This is why I want to create a watchlist that contains all public IP addresses used by Microsoft’s cloud services.
The IP’s can be downloaded from the Microsoft website or you can use the Get-AzNetworkServiceTag command to retrieve them from Powershell.
Inside our Managed Azure Sentinel offering, where we manage and handle the Sentinel environment of our customers, we use an Azure Function that runs weekly to retrieve the current IP addresses and add them to a Sentinel Watchlist through the API.
The complete script can be found on our public GitHub repository.
This script retrieves the latest JSON file from the Microsoft website and adds those IPs to an Azure Sentinel watchlist.
The Powershell code to add and populate a watchlist is as follows:
#Create watchlist
$JSON = @"
{
"properties": {
"contentType": "text/csv",
"description": "csv1",
"displayName": "$WatchListName",
"numberOfLinesToSkip": "0",
"provider": "Microsoft",
"rawContent": "",
"source": "Local file"
}
}
"@
Invoke-RestMethod -Uri "https://management.azure.com/subscriptions/$subscriptionID/resourceGroups/$resourceGroup/providers/Microsoft.OperationalInsights/workspaces/$workspaceName/providers/Microsoft.SecurityInsights/watchlists/$WatchListName`?api-version=2019-01-01-preview" -Body $JSON -Headers $authHeader -Method PUT -ContentType 'application/json; charset=utf-8'
#Populate watchlist
$JSON = @"
{
"properties": {
"contentType": "text/csv",
"description": "csv1",
"displayName": "$WatchListName",
"numberOfLinesToSkip": "0",
"provider": "Microsoft",
"rawContent": "$CSVContent",
"source": "Local file"
}
}
"@
Invoke-RestMethod -Uri "https://management.azure.com/subscriptions/$subscriptionID/resourceGroups/$resourceGroup/providers/Microsoft.OperationalInsights/workspaces/$workspaceName/providers/Microsoft.SecurityInsights/watchlists/$WatchListName`?api-version=2019-01-01-preview" -Body $JSON -Headers $authHeader -Method PUT -ContentType 'application/json; charset=utf-8'
There are a few things to note about the watchlist API:
Now that we have the sign-ins and IP’s in Azure Sentinel, it is just a matter of creating an Analytics Rule that will fire when a sign-in is found that isn’t from an allowed IP address.
In our example, I have created a watchlist called ‘LogicAppServicePrincipals’ which contains a list of all the Service Principals that should only be used from Logic Apps.
The first step is to retrieve all the Logic Apps public IPs from our watchlist. This is done with the following query:
let LogicAppIP = toscalar(_GetWatchlist('MSCloudIPs')
| where Name startswith "LogicApps" or Name == "AzureConnectors"
| summarize l=make_list(Ranges));
Next up, we have to retrieve all the sign-ins from a correct IP address (a Logic App IP address in this case). This is done through mv-apply and ipv4_is_match.
The mv-apply function will duplicate records and add every possible Logic App to every Managed Principal Sign-in. Then we use the ipv4_is_match parameter as the IP’s in our watchlist are in a CIDR format and the sign-in logs are not. This function allows us to easily check if an IP belongs to a CIDR range.
Because the mv-apply will duplicate content, we use to summarize function to only keep one row per sign-in.
This query will provide us the ‘CorrectSignins’ variable which contains all the sign-ins from service principals that occur from a valid (Logic App) IP address.
let CorrectSignins = (
AADServicePrincipalSignInLogs
| join kind=rightouter (_GetWatchlist('LogicAppServicePrincipals')) on $left.ServicePrincipalName == $right.SPName
| mv-apply l=LogicAppIP to typeof(string) on
(
where (ipv4_is_match(IPAddress, l))
)
| summarize arg_max(TimeGenerated, *) by CorrelationId);
AADServicePrincipalSignInLogs
If you want to learn more about lookups inside of KQL, check out this blog post by Ofer Shezaf where I got the idea to use the mv-apply command.
The final step is pretty simple, here we want to retrieve all sign-ins, but exclude the ones in the ‘CorrectSignins’ variable (which we know come from a valid IP-address). This is done through a ‘rightouter join’. With this join, we will retrieve all the sign-ins that don’t have a match with a ‘correct’ sign-in. The result will contain all the sign-ins that happen from an ‘unauthorized’ IP address.
The full query is as follows:
let LogicAppIP = toscalar(_GetWatchlist('MSCloudIPs')
| where Name startswith "LogicApps" or Name == "AzureConnectors"
| summarize l=make_list(Ranges));
let CorrectSignins = (
AADServicePrincipalSignInLogs
| join kind=rightouter (_GetWatchlist('LogicAppServicePrincipals')) on $left.ServicePrincipalName == $right.SPName
| mv-apply l=LogicAppIP to typeof(string) on
(
where (ipv4_is_match(IPAddress, l))
)
| summarize arg_max(TimeGenerated, *) by CorrelationId);
AADServicePrincipalSignInLogs
| join kind=rightouter (_GetWatchlist('CloudControlServicePrincipals')) on $left.ServicePrincipalName == $right.SPName
| join kind=leftanti CorrectSignins on CorrelationId
| extend IPCustomEntity = IPAddress
| extend AccountCustomEntity = ServicePrincipalName
Now it’s time to pack up the query and create an Analytics rule from it
Microsoft 365 Consultant
Focus
Bio
The Collective is a highly-skilled Microsoft partner with expertise in security, compliance, endpoint management, messaging, and Microsoft Teams voice and meetings.
© The Collective - BE 0726.449.826 - Privacy Policy