Fighting AzureAD App registration client secrets – step1: reviewing client secret usage

Workload identity (including service principals) security keeps bugging me, especially the password credentials (aka client secret). I’m sure there are scenarios where this is the only option, but I see environments where these are used just because it is easier to implement. And one day I woke up and realized how dangerous it can be – so now I’m fighting client secrets as hard as I can.

TL;DR
– Why: a leaked client secret can be easly used without being noticed (or hardly noticed… you may keep an eye on risky workload identities or have other solutions in place)
– How:
– review client secret usage and try to migrate to certificate based auth,
– at least don’t store these secrets hard coded in scripts or programs,
– use conditional access for workload identities (Microsoft Entra Workload Identities licence is required),
– limit password lifetime (Microsoft Entra Workload Identities licence is required)

This is a huge topic, so I will split it into some form of “series”.

So let’s start with the Why?
As I mentioned, a leaked credential can be hard to notice (if there is no monitoring in place, or IT is not aware of the available option to review risky workload identities). In AzureAD – Security – Identity Protection (Entra: Protect&secure – Identity Protection) you can find “Risky workload identities” (will be discussed in other post).

Let’s imagine a targeted brute force scenario: to access resources using a service principal, you need 3 things: the tenant ID, the application ID and the password. Tenant ID for a domain can be easily acquired, the easiest way is navigate to AzureAD – External Identities – Cross-tenant access settings – Add organization, then enter the domain name:

Gathering tenant ID for a domain

Guessing an application ID is nearly impossible, however, with enough compute power, this is only a matter of time: when someone tries to access your tenant using a non-existent app id, the response will be an HTTP 400 (Bad request) error with the following message:

“error”:”unauthorized_client”,”error_description”:”AADSTS700016: Application with identifier ‘<appID>’ was not found in the directory ‘<tenant>’

On the other side, when using an existing app id with a wrong password, the response will be an HTTP 401 (Unauthorized) error with the following message:

“error”:”invalid_client”,”error_description”:”AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app ‘<appID>’

The last step is to brute force every password combination 😁 Okay, okay it is already hard to get the app ID and it is even more difficult to pick an app that has password credentials then guess these credentials, but not impossible. And I’m sure there are more sophisticated ways to skip to the last step (eg.: by default a user can read app registrations in the AzureAD portal and even read the hint for the Client secret value).

A non-privileged user has access to the client secret hint by default

Sure, you can review Service principal sign-ins from the portal to detect anomalous activities, but this sounds a very tedious task – unless you have some monitoring solution in place.

How to spot client credentials usage?

My first step towards achieving a password-free environment is to make an inventory of apps with client secrets. In my previous post, I wrote some words about AzureADToolkit and shared a custom script to get a report on these apps with the API permissions assinged.

This time, I’m focusing on sign-in activity to find active password credential usage. When we switch to “Service principal singn-ins” in Sign-in logs menu on the AzureAD portal, we can filter the results by client credential type used:

Filtering logs by client credential type

While this may be enough for a one time review, you may want to monitor password usage later. I prefer to have a PowerShell script for this purpose, but there are certainly other solutions available. I didn’t find ready-to-use cmdlets to query service principal sign-ins so I chose the hard way to write my own. Using developer tools in the browser (by hitting F12 😉) we can analyze Network traffic when opening a service principal sign-in event:

Request URL for a service principal sign-in event

What we need to see here is that the portal is using the Graph API beta endpoint and at the end of the request the source is specified source=sp where sp probably stands for “service principal”. To filter by client secret usage, we will use the ‘clientCredentialType eq clientSecret’ clause. To access sign-in information, the identity used requires ‘AuditLog.Read.All’ permission on Microsoft Graph. If you want to access these informations in an unattended manner (eg.: a scheduled task), you need to create a new app registration, grant the permission (application type) with admin consent and provide a credential (hopefully a certificate 😅)

Quick guide:

1. Create an app registration
2. Remove default permission, add AuditLog.Read.All Application permission and grant admin consent

3. Create a self-signed certificate (use admin PS if you want it to be created in Local Machine container; in a highly secure scenario, you can disable private key export by appending ‘-KeyExportPolicy NonExportable’):

New-SelfSignedCertificate -FriendlyName "F12 - SP client secret usage monitor" -NotAfter (Get-date).AddYears(2) -Subject "F12 - SP client secret usage monitor" -Container Cert:\LocalMachine\My\ -Provider “Microsoft Enhanced RSA and AES Cryptographic Provider” -KeyExportPolicy NonExportable

4. Export the certificate in cer format (only the cert, not the private key)

5. Upload the certificate on the app registration page

Now we have the right app for our needs, let’s query the information needed. To use certificate authentication, we will install MSAL.PS module for simplicity:

Install-Module MSAL.PS

The following script will write out client secret usage for the last 24 hours:

Import-Module msal.ps
$tenantID = '<tenantID>'
$appID = '<app ID>'
$certThumbprint = '<certificate thumbprint created for the app>'
$token = Get-MsalToken -TenantId $tenantID -ClientId $appID -ClientCertificate (get-item Cert:\LocalMachine\my\$certThumbprint) -AzureCloudInstance 1

#query sign-ins for the last 24 hours
$dateStart = (([datetime]::UtcNow).AddHours(-24)).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
$url = "https://graph.microsoft.com/beta/auditLogs/signIns?api-version=beta" + '&$filter=createdDateTime%20ge%20' + $($datestart) + "%20and%20createdDateTime%20lt%20" + (([datetime]::UtcNow).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")) + ' and clientCredentialType%20eq%20' + "'clientSecret'&source=sp"

$servicePrincipalSignIns = $null
while ($url -ne $null){
        #Write-Host "Fetching $url" -ForegroundColor Yellow
        $response =  Invoke-RestMethod -Method Get -Uri $url -Headers @{ Authorization = $Token.CreateAuthorizationHeader()}
        $servicePrincipalSignIns += $response.value
        $url = $response.'@odata.nextLink'
    }

$servicePrincipalSignIns | select createdDateTime,ipAddress,serviceprincipalname,clientCredentialType
Sample output

To be continued…

Leave a Reply