Intro
You probably already came across the challenge to make sure that administrators using a highly privileged administrative role in Entra ID or an Azure RBAC role which allows control over sensitive resources should be only allowed if administrators use a dedicated administrative workstation. At Microsoft we call those devices Privileged Access Workstations (PAW). PAWs are highly restricted and protected devices with the single purpose to secure and protect the admin’s credentials following Zero Trust and Clean Source Principle. Now, the issue is that Admins could either employ that device or simply ignore it and use their office computers instead, which seems to be much more convenient. The same applies for the attackers, because admins not using a PAW makes their life much easier as they would have a direct attack path at hand. This is not what you want! (This article assumes you already have implemented a PAW for cloud services management.)
So, how do you make sure that highly privileged users must use their PAWs for working with highly privileged roles in Azure?
Let me show you some cool things to get there, as there are several technologies involved like Conditional Access, Microsoft Graph and some others like Microsoft Graph Explorer, PowerShell and a bit of Kusto for monitoring queries to give you a more complete picture. Let’s get started.
Solution Summary
What we do here is using Conditional Access with a block rule to deny all logons from non-PAW-devices targeting all members of a certain Entra ID security group. Since there is no way at this time to set the ExtensionAttribute1 via the Entra ID Portal we explore two options to set this attribute. One is using Graph Explorer, and the other one is using PowerShell.
Interested? Let’s get started.
Mission
First of all, we set the stage for our main actors.
- PAWDevice1 – Privileged Administrative Workstation (PAW) Entra ID device.
- Admin1 – Privileged administrative Entra ID account.
- PAW-Users – Entra ID security group having Admin1 as member.
Mission: We want to make sure that Admin1 can only login using their PAW.
Scenario 1: This is what we are going to do in this article. Starting with a very small scope (the user) to verify how this is working.
Scenario 2: Targeting roles would set the scope to all activated roles. In the picture below the user has no role enabled. By using PIM to enable a privileged role the user would be in scope for the Conditional Access policy where the activated role of the user would be in the targeted roles list. Then, when the user tries to access a resource which is in the liste of target resources Conditional Access would kick in.
Surely, you could combine both scenarios. Again: Before widening the scope of a very restrictive Conditional Access policy, do some monitoring first using the Report-Only mode and always make sure you have working Break-Glass Accounts.
Now, how do we enable Conditional Access to distinguish between a PAW and an Office device to enforce PAW usage and how do we target the right users or roles?
First goal – ‘Tagging’ the PAW device
First task is setting the ExtensionAttribute1 for the PAW device object in Entra ID. We are going to use Microsoft Graph Explorer and PowerShell for this task.
A brief explanation of the ExtensionAttribute1 attribute: Microsoft Entra ID offers a set of 15 extension attributes with predefined names on the user and device resources. These properties were initially custom attributes provided in on-premises Active Directory (AD) and Microsoft Exchange. However, they can now be used for more than syncing on-premises AD and Microsoft Exchange data to Microsoft Entra ID through Microsoft Graph.
Getting started with Microsoft Graph Explorer
Let’s start using Microsoft Graph Explorer (https://developer.microsoft.com/en-us/graph/graph-explorer). It is a great tool to dig deeper into all the information Microsoft Graph can provide you with. Besides, and this is very helpful, you can exactly determine the Microsoft Graph permissions needed for certain Microsoft Graph related operations via the tab “Modify permissions”.
Speaking of “permissions for Microsoft Graph Explorer”. You might have to set permissions for the tasks you want to perform within the app, because it performs tasks on your behalf. We deal with this just a little bit further down the line.
Now, let’s first find our device using Microsoft Graph Explorer.
The first thing we need to do when starting to use Microsoft Graph Explorer is to log on using credentials for the tenant in which the device is managed, because if not logged on you only would see the “Sample Tenant”! And yes, it is highly recommended that we use a PAW for the tasks below.
In Microsoft Graph Explorer make sure you are using the latest features by selecting “Beta”.
Then we change the address line to:
https://graph.microsoft.com/beta/devices
This should give you a list of all devices from Entra ID.
Important: If you have never used Microsoft Graph Explorer before, it might throw an error when running this query for devices, because that app is not automatically allowed to perform tasks on your behalf.
The error looks like this:
As you can see it says that this app does not have sufficient privileges.
In this case you would need to consent for the permissions you need.
Consent to delegated permissions for Microsoft Graph Explorer
To be allowed to consent to permissions needed by this application within Entra ID you have to elevate to either Global Administrator or Security Administrator Entra ID role.
Entra ID Account (your account) permission needed for consenting: microsoft.directory/servicePrincipals/managePermissionGrantsForAll.microsoft-company-admin
How to consent
Click on the tab “Modify permissions”.
It will show you exactly which permissions are needed to query for devices or even do more.
It is always highly recommended to use least privilege. For reading device information we must consent to “Device.Read.All”. The button “Consent” is in the same line at the right-hand side and you will be prompted to consent as shown below.
Tick the box “Consent on behalf of your organization” and then click on the button “Accept”.
Now “Modify permission” should look like this:
Now re-run the query. You should get a list of all devices in the “Response preview”
Find PAWDevice1
Now, we want to query for PAWDevice1.
For that we change the address to include a filter.
=================================================================================
Tip: as soon as you enter a ? at the end of the web address it will show you a list of available commands to use.
https://graph.microsoft.com/beta/devices?$filter=displayName eq 'PAWDevice1'
=================================================================================
After running the query filtering for the display name of the device we get a single device entry with all its attributes as a result. Here we can also verify that none of the extension attributes has a value set.
What we need now is the value for “id”, which is the ObjectID of this device. Don’t confuse the value “id” with the value “deviceId” which is also in the list of values.
We copy the Object ID into VS Code (or any other editor), because we need it for setting the ExtensionAttribute1 for this device.
Set ExtensionAttribute1 for PAWDevice1
Open Microsoft Graph Explorer in your web browser and log on with your Entra ID account of your tenant. Make sure that you have the Entra ID role “Intune Administrator” activated to perform the task of setting the device’s ExtensionAttribute1.
Entra ID Account (your account) permission needed for this task: microsoft.directory/devices/extensionAttributeSet1/update
(see reference for role permissions here: https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/permissions-reference?toc=%2Fgraph%2Ftoc.json#intune-administrator)
The next operation we perform is a PATCH operation. Remember we had to consent to certain permissions for the GET operation. Now we need to consent for permissions for the PATCH operation (if not already done).
The URL needed for the next step is built of two parts:
- https://graph.microsoft.com/beta/devices
- The unique device ID GUID. Remember: That ID should have been copied into the editor VS Code (or any other editor you like).
After we enter the URL including the device id like this (mind that each device ID is unique), switch to PATCH on the left-hand side and select the “Modify permissions” tab, you’ll likely see the below:
Here we need to consent to “Directory.AccessAsUser.All” permissions.
What does that mean in terms of permissions?
The description says “Allows the app to have the same access to information in your work or school directory as you do.” We ask the app to do something for us. And it does it for us with the same permissions we have at that moment.
After consenting, click on the tab “Request body” and enter the following JSON code to update the value for ExtensionAttribute1.
{
"extensionAttributes": {
"extensionAttribute1": "PAW"
}
}
After entering the JSON code click on the blue button (upper right-hand side) “Run query”.
It should show this if successful:
If we change from PATCH back to GET we can just click on Run query and it will show us the device entry again.
Now we scroll down to look for the value of ExtensionAttribute1.
Now it has the value “PAW.
Wasn’t that fun! This was an exercise to give you some hands-on experience on Microsoft Graph Explorer. This tool is especially helpful when trying to get the right information for automating such tasks via PowerShell.
Setting ExtensionAttribute1 with PowerShell
When using PowerShell to access Microsoft Graph it is a similar process when it comes to consenting to permissions needed for an application which will then act on behalf of the user. In this case it is another application than for Graph Explorer. Its name is Microsoft Graph Command Line Tools. The former name was Microsoft Graph PowerShell and it had been changed to the new name in May 2023.
With this script and the correct activated role (same as for Graph Explorer) we can easily set a device’s ExtensionAttribute1 value or instead we could even do it as bulk for an Entra ID device group.
To be able to run the script we want to make sure we have the following PowerShell modules installed on our device.
- Microsoft.Graph.Authentication
- Microsoft.Graph.Identity.DirectoryManagement
- Microsoft.Graph.Groups
More information on how to install the modules:
https://learn.microsoft.com/en-us/powershell/microsoftgraph/installation?view=graph-powershell-1.0
PowerShell Code for Set-DeviceExtensionAttribute.ps1
<#
.SYNOPSIS
Sets the ExtensionAttribute1 on devices.
.DESCRIPTION
.PARAMETER TargetGroup
Assign the ExtensionAttribute1 to all devices in a group
.PARAMETER DeviceName
Assign the ExtensionAttribute1 to a specific device
.PARAMETER ExtensionAttributeValue
The string value of the extension attribute. Default in this script is "PAW"
.EXAMPLE
Set-DeviceExtensionAttribute -DeviceName mydevice -ExtensionAttributeValue "PAW"
Set-DeviceExtensionAttribute -TargetGroup DeviceGroupName -ExtensionAttributeValue "PAW"
.NOTES
Disclaimer
The sample scripts provided here are not supported under any Microsoft
standard support program or service. All scripts are provided AS IS without
warranty of any kind. Microsoft further disclaims all implied warranties
including, without limitation, any implied warranties of merchantability or
of fitness for a particular purpose. The entire risk arising out of the use
or performance of the sample scripts and documentation remains with you. In
no event shall Microsoft, its authors, or anyone else involved in the
creation, production, or delivery of the scripts be liable for any damages
whatsoever (including, without limitation, damages for loss of business
profits, business interruption, loss of business information, or other
pecuniary loss) arising out of the use of or inability to use the sample
scripts or documentation, even if Microsoft has been advised of the
possibility of such damages.
#>
[CmdletBinding()]
param (
[Parameter(ParameterSetName = 'GroupAssign', Mandatory = $True)]
[String]
$TargetGroup,
[Parameter(ParameterSetName = 'DeviceAssign', Mandatory = $True)]
[String]
$DeviceName, #not case-sensitive
[Parameter()]
[String]
$ExtensionAttributeValue = "PAW"
)
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
#region Functions
# ////////////////////////////////////////////////////////////////////
function Connect-ToGraph {
$Parameter = @{
'Scopes' = "Device.ReadWrite.All"
}
try {
Connect-MgGraph @Parameter
}
catch {
Write-Error -Exception $_.Exception
break
}
}
Function Set-DeviceExtensionAttribute {
[cmdletbinding(DefaultParameterSetName = 'All')]
param (
[Parameter(Mandatory, ParameterSetName = 'DeviceId')]
[String]
$DeviceId,
[Parameter(Mandatory = $true)]
[String]
$ExtensionAttributeValue
)
$graphApiVersion = "Beta"
$Resource = "devices/$DeviceId"
$Uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
$JSON = @"
{
"extensionAttributes": {
"extensionAttribute1": "$ExtensionAttributeValue"
}
}
"@
# Important. The last curly bracket in the above JSON must be without any space before it!
try {
Invoke-MgGraphRequest -Uri $Uri -Method PATCH -Body $JSON -ContentType 'application/json'
Write-Host
Write-Host "Success - Wait a moment until changes have been synced to the tenant." -ForegroundColor Green
}
catch {
Write-Host "PATCH operation failed with error."
Write-Host "Error: " $Error
Write-Host "============================================="
Write-Host "JSON BODY: $JSON"
Write-Host "URI: $uri"
}
}
Function Get-Devices {
param(
[Parameter(Mandatory, ParameterSetName = 'GroupName')]
[string] $GroupName
)
try {
Write-Host "Getting Group '$GroupName'."
$Group = Get-MgGroup -Filter "displayName eq '$GroupName'" -ErrorAction SilentlyContinue
if ($Group) {
$GroupMembers = Get-MgGroupMember -GroupId $Group.Id
return $GroupMembers
}
else {
Write-Host "Group '$GroupName' NOT FOUND in tenant!" -ForegroundColor Red
return $false
}
}
catch {
return $false
}
}
#endregion Functions
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
#region MAIN
# ////////////////////////////////////////////////////////////////////
Connect-ToGraph
# Setting the value for a single device
if ($DeviceName) {
Write-Host "Getting Device '$DeviceName'."
$Device = Get-MgDevice -Filter "DisplayName eq '$DeviceName'" -ErrorAction SilentlyContinue
if ($Device.Id) {
Set-DeviceExtensionAttribute -DeviceId $Device.id -ExtensionAttributeValue $ExtensionAttributeValue
}
else {
Write-Host "Device '$DeviceName' does not exist." -ForegroundColor Red
}
}
# Setting the value for all member devices of an Entra ID security group
If ($TargetGroup) {
$Devices = Get-Devices -GroupName $TargetGroup
foreach ($Device in $Devices) {
Set-DeviceExtensionAttribute -DeviceId $Device.id -ExtensionAttributeValue $ExtensionAttributeValue
}
}
#endregion MAIN
When running the script for the first time and if we did not consent for any permission for Microsoft Graph Command Line Tools the following consent prompt will appear.
Again, here you tick the box “Consent on behalf of your organization” and click the button “Accept”.
The scope for the permissions is defined in the script function “Connect-ToGraph”.
$Parameter = @{
'Scopes' = "Device.ReadWrite.All"
}
Overview of Function Set-DeviceExtensionAttribute
The main function in this script is “Set-DeviceExtensionAttribute”.
It performs what we did using Graph Explorer.
It builds up the URI:
$graphApiVersion = "Beta"
$Resource = "devices/$DeviceId"
$Uri = "https://graph.microsoft.com/$graphApiVersion/$($Resource)"
It creates the JSON:
$JSON = @"
{
"extensionAttributes": {
"extensionAttribute1": "$ExtensionAttributeValue"
}
}
"@@
It runs the PATCH operation:
Invoke-MgGraphRequest -Uri $Uri -Method PATCH -Body $JSON -ContentType 'application/json'
Conditional Access
Brief overview
First a brief overview of the elements of Conditional Access policies we use for our POC.
We have four main sections to be considered for our policy:
- Assignment to Users, groups or directory roles
- Target resources we want to protect ( in our case this will be applications)
- The condition under which the policy is applied
- The grant control will be set to BLOCK
Important: You want to move slowly and carefully because you don’t want to lock yourself and everyone else out.
Prerequisites
First we must make sure that you have the appropriate permissions to create Conditional Access policies.
To Create a device-based Conditional Access policy our account must have one of the following permissions in Microsoft Entra:
- Global administrator
- Security administrator
- Conditional Access administrator
Create Policy
Let’s move on to create the device-based Conditional Access policy.
Open the Microsoft Entra Admin Center and browse to
Protection > Conditional Access
Link: https://entra.microsoft.com/#blade/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/fromNav/
Under ConditionalAccess click on “Policies” and then on “New policy”
Policy Name
First we give it a name and call it “PAW-Block-Device-Filter”
Assignments
Let’s first decide who will be excluded from this Conditional Access Policy.
- Break Glass Accounts (validated emergency accounts if everyone is locked out – those accounts must be excluded from all CA policies)
- Entra ID Connect Account(s) – (Accounts for AD user synchronization)
- Your account – During testing phase
Who to include in this Conditional Access Policy:
We only target the group “PAW-Users”.
Important note: We do not want to include any roles in this example/demo configuration, because this could have an instant impact on all members of a targeted role (that is tenant wide. Example: If you would target the role Global Administrators the Conditional Access policy would be effective for all user accounts who currently are having the role active -> except the ones in the list of excluded users/groups) and for a start we only want to target our Admin1 account which is member of PAW-Users. Remember this is for demonstrating how the approach works.
Target resources
We want to enforce usage of a PAW device for Microsoft admin portals in Entra targeting PAW-Users. There is a handy way to do this.
(see also the related Microsoft Learn article for more information:
https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps#microsoft-admin-portals
Under Target resources we select “Cloud apps” from the pull-down menu. Then we select the radio button “Select apps” and under “Select” we clock on the link with the name “None”. (That is because at this point in time no app had been selected.)
Should look like this now:
Conditions
The condition we define tells Conditional Access when to be applied. Keep in mind that we only target the members of the group PAW-Users. No Entra ID roles.
Condition: The condition defines the rules under which the Conditional Access policy engine applies what is configured under “Grant”.
To configure the condition click on “0 conditions selected” in the category “Conditions”.
The condition uses a device filter. To configure the device filter first click on “Yes” under “Configure”. Then select the radio button “Exclude filtered devices from policy”.
Now we configure the filter. Under “Property” select the pull-down menu and select ExtensionAttribute1 as value. Operator must be set to “Equals” and the value must be “PAW”.
To finish the configuration, click on the button “Done”.
Grant control
To configure the Grant control to block access we select the radio button “Block access” and then click on the button “Select”.
The whole policy would read:
When members of the group PAW-Users log on to one of the Microsoft admin portals and their logon is coming from a device that has not set ExtensionAttribute1 to “PAW” the logon will be denied. If they logon from a device with ExtenstionAttribute1 set to “PAW” then the logon will be allowed.
Important: For the start we only set this Condition Access policy to Report-only.
That mode doesn’t block anything but allows for monitoring before introducing a restrictive policy like this.
Monitoring Conditional Access policies in Report-only mode
To be able to use Kusto (Link: https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query) queries on Entra ID Sign-In logs you must configure Entra ID to store Sign-In logs in a Log Analytics workspace. When done you can easily query for Sign-In events where the new policy would have blocked a logon attempt for the targeted users.
Here is an example of a short Kusto query to find all cases where a Conditional Access policy which is set to Report-only would have blocked a logon attempt.
SigninLogs
| extend CAP = parse_json(ConditionalAccessPolicies)
| mv-expand CAP
| extend DeviceName = parse_json(DeviceDetail)
| where CAP.result contains "reportOnlyFailure"
| project TimeGenerated, UserDisplayName, ConditionalAccessPolicyName=CAP.displayName, DeviceDisplayName=DeviceName.displayName, ResultDescription, Location, IPAddress, NetworkLocationDetails, ResourceDisplayName
| sort by TimeGenerated desc
Now we could test the new created Conditional Access policy with the test user Admin1.
Testing it out
Let's try to logon with user Admin1 to a Microsoft admin portal of your choice. Say, we would try the Azure Portal. Let’s also assume that this user account has already gone through the process of registering for MFA.
In the browser type in “portal.azure.com”. Logon will be allowed as long as the Conditional Access policy is set to Report-only.
In Log Analytics you could see the following when using the Kusto query from above:
If we enable the Conditional Access policy (set it from “Report-Only” to “ON”), Admin1 would not be allowed to log on from a device that has not set ExtensionAttribute1. In that case the account Admin1 would see the following:
I hope this blog was helpful and it could give you some insights and ideas on how to make your environment more secure. Thanks for reading.