Using Azure AD Privileged Identity Management with Active Directory roles (such as domain admin)

I just put my Azure AD Group Writeback Script on Github, and figured it was time to do something I know many have requested from Microsoft to deliver, but that is still missing; Using Azure AD Privileged Identity Management to control access to Active Directory built-in groups such as Domain Admin, Schema Admin and Enterprise Admin.

To keep this blog post as short as possible, I will not be covering things such as setting up Active Directory, configuring Azure AD Connect and so on. Also, this section on github covers what you need to know for authenticating the Azure AD Group Writeback Script to Azure AD for none-Azure environments, so I will only cover that briefly.

What we will do in this blog post is:

Let us start by creating a few privileged groups in the Azure Portal – “AD – Domain Admins” and “AD – Enterprise Admins”.

Notice that no roles are assigned

Next, for both groups, we open the group properties, find “Privileged access” and click “Enable privileged access”:

Now, I add two active assignments, just to have a few members to actually sync when setting up the AAD Group Writeback Script. These will be converted to eligible later.

Now that our two groups have been created, let us set up the AAD Group Writeback Script. Start by either downloading the Writeback Script repo or installing git and cloning the repo like this:

git clone "https://github.com/goodworkaround/AAD-Group-Writeback-Script.git" "c:\git\AAD-Group-Writeback-Script"
cd c:\git\AAD-Group-Writeback-Script

Next, make sure that the Active Directory PowerShell Module and the Azure AD PowerShell Module is installed using PowerShell:

Install-WindowsFeature RSAT-AD-PowerShell
Install-Module AzureAD

Start PowerShell and launch the script provided here (which is the same as the one below this line):

function New-WriteBackScriptInstallation {
    [CmdletBinding()]

    Param
    (
        [Parameter(Mandatory=$false,
                   Position=0)]
        $ConfigFile = ".\Run.config"
    )

    $appName = "AzureAD to AD group writeback script"

    Install-Module AzureAD | Out-Null
    Connect-AzureAD | Out-Null

    $requiredGrants = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]::new(
        "00000003-0000-0000-c000-000000000000", # Microsoft Graph
        @(
            [Microsoft.Open.AzureAD.Model.ResourceAccess]::new("5b567255-7703-4780-807c-7be8301ae99b","Role") 
            [Microsoft.Open.AzureAD.Model.ResourceAccess]::new("df021288-bdef-4463-88db-98f22de89214","Role") 
        )
    )

    Write-Verbose "[Change] Creating the app registration '$appName'" -Verbose
    $app = New-AzureADApplication -DisplayName $appName -RequiredResourceAccess $requiredGrants
    $sp = New-AzureADServicePrincipal -AppId $app.appid
    $key = New-AzureADApplicationPasswordCredential -ObjectId $app.ObjectId -EndDate (get-date).AddYears(100)

    $url = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/$($app.AppId)/isMSAApp/"
    Write-Verbose "Go to the url that already is put on the clipboard and click 'Grant admin consent':`n`n$url" -Verbose
    $url | Set-Clipboard
    Read-host -Prompt "Click enter when done"

    if(Test-Path $ConfigFile) {
        $ConfigFile2 = ".\{0}.config" -f [guid]::NewGuid()
        Write-Verbose "File $ConfigFile already exists, writing to $ConfigFile2"
        $ConfigFile = $ConfigFile2
    }

    $_config = [ordered] @{
        AuthenticationMethod = "ClientCredentials"
        ClientID = $app.appid
        EncryptedSecret = "$($key.Value | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString)"
        TenantID = "$((Get-AzureADCurrentSessionInfo).TenantId.ToString())"
        DestinationOU = "OU=AAD Group writeback,DC=contoso,DC=com"
        ADGroupObjectIDAttribute = "info"
        AADGroupScopingMethod = "PrivilegedGroups"
        GroupDeprovisioningMethod = "PrintWarning"
    } | ConvertTo-Json
    set-content $ConfigFile -Value $_config

    Write-Host "Created file $ConfigFile"
}
New-WriteBackScriptInstallation

Authenticate to Azure AD with an account with Global Admin, Cloud Application Admin or Applicaiton Admin role (need to be able to create an app registration, and consent to it):

Grant the app registration the required access, which is to read all users and groups: Click enter on the script window, and it should return that it has created a file for you. This is a config file that should work – almost, just by changing a few settings πŸ™‚ The authentication stuff is now correct, and you only need to update the DestinationOU in your script. Here is my final configuration, where only line 6 and 9 has changed (DestinationOU and GroupDeprovisioningMethod):

{
    "AuthenticationMethod":  "ClientCredentials",
    "ClientID":  "e0736d2a-d385-42b0-80f6-60a11d817996",
    "EncryptedSecret":  "01000000d08c9ddf0115d1118c7a00c04fc297eb01000000be3e7b5924a98f4cbf5ebc0e1d25f1130000000002000000000003660000c00000001000000091f23fe77a40a5c4ac2ebe88e97e9ccc0000000004800000a0000000100000009d6fb3fd22c6ae7a74768b9d8d440767600000003580e05d35304ee9fed6b2e3ec5473e09109664fe14420959eb0ad7f37581c9466ab443509764ab3f6c160e7140a6ccdbb5b1ce5b5d80c6b6ac290bb8ead530085d91ef6b7f54e91a936d08a37dbf0e37526f69ccff49842e7c106d382e4ae5f14000000aa145ebe1fcffd99e6eca8d669b67a9858b6bbd8",
    "TenantID":  "937a3b05-1264-406f-ade1-3f4a42d4e26f",
    "DestinationOU":  "OU=AAD Privileged Groups,DC=contoso,DC=com",
    "ADGroupObjectIDAttribute":  "info",
    "AADGroupScopingMethod":  "PrivilegedGroups",
    "GroupDeprovisioningMethod":  "Delete"
}

Running Run.ps1 with -WhatIf and -Verbose now shows what is going to happen when:

.\Run.ps1 -WhatIf -Verbose 
VERBOSE: Successfully received access token
VERBOSE:  - oid:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - aud:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - iss:             https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE:  - appid:           e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE:  - app_displayname: AzureAD to AD group writeback script
VERBOSE:  - roles:           Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE:  - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:   - Creating group 'AD - Domain Admins' in AD
What if: Performing the operation "New" on target "CN=AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03),OU=AAD Privileged Groups,DC=contoso,DC=com".
VERBOSE:  - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE:   - Creating group 'AD - Enterprise Admins' in AD
What if: Performing the operation "New" on target "CN=AD - Enterprise Admins (8b264bee-f405-46f7-b1be-b7f1bf4ef72d),OU=AAD Privileged Groups,DC=contoso,DC=com".
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE:  - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
WARNING: Unable to find AD group for AAD group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:  - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
WARNING: Unable to find AD group for AAD group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned

This looks good! Let’s just run it without WhatIf, before starting to invoke it as a scheduled task every 5 minutes (or you know, if you want to be a bit more sure that the script is working, run it manually and see what it does, like I will do in this blog post)

.\Run.ps1 -Verbose 
VERBOSE: Successfully received access token
VERBOSE:  - oid:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - aud:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - iss:             https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE:  - appid:           e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE:  - app_displayname: AzureAD to AD group writeback script
VERBOSE:  - roles:           Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE:  - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:   - Creating group 'AD - Domain Admins' in AD
VERBOSE:  - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE:   - Creating group 'AD - Enterprise Admins' in AD
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE:  - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:   - Adding member to AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Donald Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE:   - Adding member to AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Dolly Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE:  - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned

That’s it. Our AD now looks like this! We can not add the “AD – Domain Admins” group to our Domain Admins group to achieve global admin permissions.

Note 1: The adminSDHolder process will mess with the permissions of the group when you add it to Domain Admins, so make sure that you are running the Run.ps1 script as a user that has access to those kinds of groups

Note 2: Large ADs with many sites will experience that replication is an issue. This is where you could consider using this script to populate groups in a bastion forest. I am looking at adding support for msDs-ShadowPrincipal objects, just like MIM PAM does πŸ™‚

We are now ready to convert Dolly and Donald to Eligible Assignments. Go back to the Azure portal, on the AD – Domain Admins group and click “Update”, and change the assignments to eligible:

When running Run.ps1 now, we should see the users being removed from the group in AD:

VERBOSE: Successfully received access token
VERBOSE:  - oid:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - aud:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - iss:             https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE:  - appid:           e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE:  - app_displayname: AzureAD to AD group writeback script
VERBOSE:  - roles:           Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE:  - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:  - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE:  - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:   - Removing member from AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Dolly Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE:   - Removing member from AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Donald Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE:  - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned

Sweet, now let’s look at Donald’s experience in all of this:

First he signs into the Azure Portal, and searches for “PIM” (which can be a favorite of course, directly linking to PIM)

He then goes to “Active just in time”

He goes to Privileged Access Groups, and acitivates his “AD – Domain Admins” group

The role is now active

Now, when Run.ps1 is again triggered, we see that Donald is added to the “AD – Domain Admins” AD group:

VERBOSE: Successfully received access token
VERBOSE:  - oid:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - aud:             e67bebd1-7f97-4837-9379-bd3869c7d814
VERBOSE:  - iss:             https://sts.windows.net/937a3b05-1264-406f-ade1-3f4a42d4e26f/
VERBOSE:  - appid:           e0736d2a-d385-42b0-80f6-60a11d817996
VERBOSE:  - app_displayname: AzureAD to AD group writeback script
VERBOSE:  - roles:           Group.Read.All User.Read.All
VERBOSE: Getting all scoped groups
VERBOSE: Found 2 groups in scope
VERBOSE: Starting Save-ADGroup
VERBOSE:  - Processing AADGroup 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:  - Processing AADGroup 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Save-ADGroup finished
VERBOSE: Processing all memberships
VERBOSE:  - Processing group 'AD - Domain Admins' (22fe80d8-674f-48ef-83b9-6539a7df9f03)
VERBOSE:   - Adding member to AD group 'AD - Domain Admins (22fe80d8-674f-48ef-83b9-6539a7df9f03)': CN=Donald Duck,OU=User accounts,DC=contoso,DC=com
VERBOSE:  - Processing group 'AD - Enterprise Admins' (8b264bee-f405-46f7-b1be-b7f1bf4ef72d)
VERBOSE: Determining whether there are AD groups to deprovision
VERBOSE: No groups that should be deprovisioned

There are a few configuration options available for the script that I have not mentioned here. Please visit github for checking those out.

Good luck!

13 thoughts on “Using Azure AD Privileged Identity Management with Active Directory roles (such as domain admin)

  1. Really nice script! Got i working with filter. Is it possible to choose if the guid should be in the AD groupname or not? Seems that it will remove it if the groupname is long.

    1. Hi,

      Yes, have a look at “ADGroupNamePattern” in the documentation. You’ll find an example in Example1.config, and here: https://github.com/goodworkaround/AAD-Group-Writeback-Script#configuration

      So essentially, if you know that your displaynames will always be unique, you can configure ADGroupNamePattern to “{0}”, which will cause the group to be called the same thing in AD as in Azure AD (but it will also cause collisions if there are groups with same display name, due to having same DistinguishedName)

  2. Stepping through this after errors I’m finding lines 25-33 in the Run.ps1 don’t seem to be taking the AccessToken.

    When the AuthenticationMethod is set to “ClientCredentials” the line…

    $AccessToken = Get-ClientCredentialsMSGraphAccessToken -ClientID $Config.ClientID -EncryptedSecret $Config.EncryptedSecret -TenantID $Config.TenantID

    Doesn’t seem to assign the AccessToken to the variable.

    Any help would be much appreciated.

    thanks

  3. Hi Marius,

    When I set the AADGroupScopingMethod to “Filter” and AADGroupScopingConfig to “id eq “, the Save-ADGroup function wants to re-create all groups previously synced from AD to Azure AD. I tried changing AADGroupScopingMethod to “PrivilegedGroups” as well, which produces the same result. Any idea why that’s happening? Thanks

    1. There appears to be a limitation in the graph requests I’m able to run. I can run Get-GraphRequestRecursive -Url “https://graph.microsoft.com/v1.0/groups//members?`$select=id,displayName,userPrincipalName,onPremisesDistinguishedName,onPremisesImmutableId” -AccessToken $AccessToken, and it will return the members of the group, but I can’t filter on /groups directly or even run a get of the group itself: https://graph.microsoft.com/v1.0/groups/. I read an article about ‘group search limitations for guest users in organizations’, but I wouldn’t think it’d apply here. Any thoughts?

      1. Hi Marius, thanks for the response. I haven’t modified the anchor attribute. I ended up having to set the $ScopeGroups variable to the specific Graph URL for the group. However, then I was getting a ‘Cannot bind argument to parameter ‘ScopedGroups’ because it is null.’ error. I added a $Result reference on the line after $Result = Invoke-RestMethod $Url -Headers @{Authorization = “Bearer $AccessToken” } -Verbose:$false in the HelperFunctions script, and then it worked. I’m not quite sure why. One other question. Should I be filtering these groups from being synced from AD back to Azure AD via the Connect utility? Otherwise, it looks like the PIM group gets created in on-premise AD, but then when synced to Azure, it creates a duplicate group. Thanks again!

  4. Hi, since ADconnect now allows to bring cloud groups present only on cloud on prem, I wanted to use this to create cloud-only groups where users could request to become domain admin of local domain; everything works, however I would like to bypass the sync imposed by ad connect (30 minutes) otherwise the requestor is activated, but until the sync is performed he will not be seen as domain admin on onprem servers.
    Would you have any advice?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s