BLOG 13 January 2022

Monitoring Service Principals with Watchlists in Azure Sentinel

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:

 

  • Azure DevOps
  • Logic Apps
  • Azure Automation
  • Your datacenter public IPs

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.

 

Ingesting Service Principal sign-ins into Azure Sentinel

 

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.

 

Creating the watchlist through Powershell

 

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:

 

  • It’s currently in public preview, so it’s bound to change.
  • There are two separate API calls to be made when creating a new watchlist: One to create it, another to populate it with data.
  • You have to be careful with the formatting of your watchlist data, as the watchlist accepts spaces, but querying a column with spaces is not that apparent.

 

Setting up the Analytics Rule

 

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

 

 

Provide a rule title and description

 

 

Add the KQL query and configure rule logic
 

 

Configure incident settings

Final solution

 

When you have completed the previous three steps, you will have a working analytics rule that creates an incident when an unauthorized sign-in happens.

 

 

Unfortunately, I have noticed that the document with Microsoft IP’s isn’t always up to date and false positives happen more than they should. So it’s advisable to either:

 

  • Add the IP’s that are missing to the watchlist yourself
  • Attach a Playbook to the rule which enriches the incident with the Network Owner of that IP address

Conclusion

 

This blog post walked you through creating a watchlist through the API and using that watchlist to create an ‘IP Allow’ list for Service Principals.

 

Far too often, Service Principals are not monitored and are given free rein. This leaves your organization vulnerable, as seen in the recent Solarwinds attacks. Administrators should be educated in the danger that Service Principals can pose and trained in treating the client secret and certificates in much the same ways as they should treat a password.

 

This solution is a first step into securing your Service Principals, which every organization should take.

Thijs Lecomte

Microsoft 365 Consultant

Focus

  • Cloud Security & Compliance
  • Identity Management
  • Security Operation Center Architect

 

Bio

  • MVP Security
  • Security enthusiast focusing on securing cloud environments. Microsoft Sentinel expert and Microsoft Defender engineer.
  • LinkedIn