Monday, December 23, 2019

Custom Azure RBAC Roles (Step by Step)

Azure provides you the ability to create custom RBAC roles, and the process bears no resemblance to any comparable process in Active Directory. As is typical for me, I am going to explain by example.

The IT department that I work for has a dedicated process for decommissioning servers, and on premise this is a very mature process which looks something like this:
  • Switch off the VM for a week and see if anyone screams
  • Delete the VM and associated disk
  • Delete DNS records
  • Revoke certificates
  • etc.
You get the picture. So in order the harmonize that process for Azure VMs we attempted to replicate the process and quickly discovered that the personnel that have been assigned the right 'Virtual Machine Contributor' do not have the ability to delete the associated disks. This is reasonable because unlike VMWare, the disks within a VM in Azure are totally separate objects and totally separate object types. What is not reasonable, and in my opinion stupid, is that there is no inbuilt role with Azure to allow that. So here we go:
  • For education you can start by grabbing a role that you want to expand upon. In this case it makes sense to start with 'Virtual Machine Contributor'. You could start from scratch, but more of that later. So lets run some PowerShell (as always, excuse my line-wrap)
Get-AZRoleDefinition  -Name "Virtual Machine Contributor" | ConvertTo-Json | Out-File "C:\Temp\Virtual Machine Contributor.json"

This will create a file that looks like the following. For clarity I am highlighting the items we will be modifying.

{
    "Name": "Virtual Machine Contributor",
    "Id": "9980e02c-c2be-4d73-94e8-173b1dc7cf3c",
    "IsCustom": false,
    "Description": "Lets you manage virtual machines, but not access to them, and not the virtual network or storage account they're connected to.",
    "Actions": [
        "Microsoft.Authorization/*/read",
        "Microsoft.Compute/availabilitySets/*",
        "Microsoft.Compute/locations/*",
        "Microsoft.Compute/virtualMachines/*",
        "Microsoft.Compute/virtualMachineScaleSets/*",
        "Microsoft.DevTestLab/schedules/*",
        "Microsoft.Insights/alertRules/*",
        "Microsoft.Network/applicationGateways/backendAddressPools/join/action",
        "Microsoft.Network/loadBalancers/backendAddressPools/join/action",
        "Microsoft.Network/loadBalancers/inboundNatPools/join/action",
        "Microsoft.Network/loadBalancers/inboundNatRules/join/action",
        "Microsoft.Network/loadBalancers/probes/join/action",
        "Microsoft.Network/loadBalancers/read",
        "Microsoft.Network/locations/*",
        "Microsoft.Network/networkInterfaces/*",
        "Microsoft.Network/networkSecurityGroups/join/action",
        "Microsoft.Network/networkSecurityGroups/read",
        "Microsoft.Network/publicIPAddresses/join/action",
        "Microsoft.Network/publicIPAddresses/read",
        "Microsoft.Network/virtualNetworks/read",
        "Microsoft.Network/virtualNetworks/subnets/join/action",
        "Microsoft.RecoveryServices/locations/*",
       "Microsoft.RecoveryServices/Vaults/backupFabrics/protectionContainers/protectedItems/*/read",
        "Microsoft.RecoveryServices/Vaults/backupFabrics/protectionContainers/protectedItems/read",
        "Microsoft.RecoveryServices/Vaults/backupFabrics/protectionContainers/protectedItems/write",
        "Microsoft.RecoveryServices/Vaults/backupFabrics/backupProtectionIntent/write",
        "Microsoft.RecoveryServices/Vaults/backupPolicies/read",
        "Microsoft.RecoveryServices/Vaults/backupPolicies/write",
        "Microsoft.RecoveryServices/Vaults/read",
        "Microsoft.RecoveryServices/Vaults/usages/read",
        "Microsoft.RecoveryServices/Vaults/write",
        "Microsoft.ResourceHealth/availabilityStatuses/read",
        "Microsoft.Resources/deployments/*",
        "Microsoft.Resources/subscriptions/resourceGroups/read",
        "Microsoft.Storage/storageAccounts/listKeys/action",
        "Microsoft.Storage/storageAccounts/read",
        "Microsoft.Support/*"
    ],
    "NotActions": [],
    "AssignableScopes": [
        "/"
    ]
}
  • So now we need to start the modifications. The first step is to provide the name. I strongly suggest the following format: so in my case:
"Name": "SLHS-Virtual Machine Contributor v1.0",
  • Don't forget the JSON comma !
  • Next DELETE the whole ID field. When we create the custom role, Azure will assign a fresh ID for us. If you forget this step, the process will try to over-right the existing role which (a) would be bad but (b) would fail.
  • Next we change the 'IsCustom' field.
"IsCustom": true,
  • Next we change the description. In my case I chose:
"Description": "Lets you manage virtual machines, including the deletion of disks.",
  • OK now the fun part, we need to add a line to provide the access we want. For this we need to turn to the master recipe list which is provided here:
https://docs.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations
  • This is the list of  'Resource Provider Operations', a compendium of all available rights. You kind of need to know what to search for, but for this purpose we need to be looking at 'Microsoft.Compute/disks'. If you search the library page for that you will see entries like 'Microsoft.Compute/disks/read', 'Microsoft.Compute/disks/write' and of course 'Microsoft.Compute/disks/delete'. At this point we can talk a little about structure. You can wildcard each element after the slash, so for example 'Microsoft.Compute/disks/delete' will allow VM disk deletion, but 'Microsoft/disks/*' will allow all actions including delete.
  • So lets run with that and insert that line into our JSON code.
  • Now here's the stupid part. The 'AssignableScopes' line. I would argue that if you want to create a custom you would want the ability to assign that role to anyone on any object in any subscription. But for custom roles (at the time of me writing this (December 2019) you cannot wildcard the subscription or assign it the tenant root. You must specify a specific subscription. Will show how to create a workaround for this later, but for now I am going to specify a specific subscription. So the resultant edited JSON file end up looking like this (again I have highlighted the delta, and remember we removed the ID line):
{

    "Name": "SLHS-Virtual Machine Contributor v2.0",
    "IsCustom": true,
    "Description": "Lets you manage virtual machines, including the deletion of disks.",
    "Actions": [
        "Microsoft.Authorization/*/read",
        "Microsoft.Compute/availabilitySets/*",
        "Microsoft.Compute/locations/*",
        "Microsoft.Compute/virtualMachines/*",
        "Microsoft.Compute/virtualMachineScaleSets/*",
        "Microsoft.Compute/disks/delete",
        "Microsoft.DevTestLab/schedules/*",
        "Microsoft.Insights/alertRules/*",
        "Microsoft.Network/applicationGateways/backendAddressPools/join/action",
        "Microsoft.Network/loadBalancers/backendAddressPools/join/action",
        "Microsoft.Network/loadBalancers/inboundNatPools/join/action",
        "Microsoft.Network/loadBalancers/inboundNatRules/join/action",
        "Microsoft.Network/loadBalancers/probes/join/action",
        "Microsoft.Network/loadBalancers/read",
        "Microsoft.Network/locations/*",
        "Microsoft.Network/networkInterfaces/*",
        "Microsoft.Network/networkSecurityGroups/join/action",
        "Microsoft.Network/networkSecurityGroups/read",
        "Microsoft.Network/publicIPAddresses/join/action",
        "Microsoft.Network/publicIPAddresses/read",
        "Microsoft.Network/virtualNetworks/read",
        "Microsoft.Network/virtualNetworks/subnets/join/action",
        "Microsoft.RecoveryServices/locations/*",
       "Microsoft.RecoveryServices/Vaults/backupFabrics/protectionContainers/protectedItems/*/read",
        "Microsoft.RecoveryServices/Vaults/backupFabrics/protectionContainers/protectedItems/read",
        "Microsoft.RecoveryServices/Vaults/backupFabrics/protectionContainers/protectedItems/write",
        "Microsoft.RecoveryServices/Vaults/backupFabrics/backupProtectionIntent/write",
        "Microsoft.RecoveryServices/Vaults/backupPolicies/read",
        "Microsoft.RecoveryServices/Vaults/backupPolicies/write",
        "Microsoft.RecoveryServices/Vaults/read",
        "Microsoft.RecoveryServices/Vaults/usages/read",
        "Microsoft.RecoveryServices/Vaults/write",
        "Microsoft.ResourceHealth/availabilityStatuses/read",
        "Microsoft.Resources/deployments/*",
        "Microsoft.Resources/subscriptions/resourceGroups/read",
        "Microsoft.Storage/storageAccounts/listKeys/action",
        "Microsoft.Storage/storageAccounts/read",
        "Microsoft.Support/*"
    ],
    "NotActions": [],
    "AssignableScopes":  [
                             "/subscriptions/4a5ce960-87d4-431b-ac1c-67a70cb1516e"
                                       ]
}

  • So save your work as something like C:\Temp\SLHS-Virtual Machine Contributor v2.0.json".
  • Next we create the new role using PowerShell:
New-AZRoleDefinition  -InputFile  "C:\Temp\SLHS-Virtual Machine Contributor v2.0.json"
  • If you are successful then you will be presented with some output that describes your newly created role:

Name             : SLHS-VM Contributor v2.0
Id               : 4c428c4d-34f9-4e15-9776-2c04ef26f4a3
IsCustom         : True
Description      : Lets you manage virtual machines, including the deletion of disks.
Actions          : {Microsoft.Authorization/*/read, Microsoft.Compute/availabilitySets/*,
                   Microsoft.Compute/locations/*, Microsoft.Compute/virtualMachines/*...}
NotActions       : {}
DataActions      : {}
NotDataActions   : {}
AssignableScopes : {/subscriptions/4a5ce960-87d4-431b-ac1c-67a70cb1516e}
  • That's it for the basic process. You can now assign that role (in this screenshot its v4.0 not v2.0 but you get the idea.


Now we have to deal with the single subscription malarkey. For this example we are going to start from scratch and create a role specifically for the task at hand (deleting VM disks). Essentially what we need is a script that takes a base name for our role, in the following example "SLHS-VMDiskDestroyer-v1.0", a description "Allows holder to delete VM disks", the RBAC Role from the Microsoft dictionary "Micrsoft.Compute/disks/delete" and the name of a pre-created (wait for the sync) AD group "CustomRBAC-VMDiskDestroyers-U_GG_IA"

When the script runs it will cycle through every subscription, add the role using the base name + the subscription GUID (each role in the tenant must have a unique name) and assign the role to the specified AD group. As with any script that does something to every subscription - take care!


# Will add custom role to all subscriptions
# Complete the three top variables if you have cloned this script.
#######################################################
# Constants for easy cloning of script
$BaseRBACName = "SLHS-VMDiskDestroyer-v1.0"
$Desc.        = "Allows holder to delete VM disks"
$Role         = "Microsoft.Compute/disks/delete"
$ADGroup      = "CustomRBAC-VMDiskDestroyers-U_GG_IA"
#######################################################

$JSONName = $RBACName + ".json"
If($JSONName -Like "* *")
{
  Write-Host "Error: JSONName must contain no spaces"
  Exit
}
Get-AZSubscription | ForEach-Object `
{
  $SubID = $_.ID
  $SubName = $_.Name
  $RBACName = $BaseRBACName + "-" + $SubID
  If(Test-Path "c:\temp\$JSONName")
  {
    Remove-Item "c:\temp\$JSONName" -Force
  }
  # Make JSON file
  Add-Content "c:\temp\$JSONName" "{"
  Add-Content "c:\temp\$JSONName" " `"Name`": `"$RBACName`","
  Add-Content "c:\temp\$JSONName" " `"IsCustom`": true,"
  Add-Content "c:\temp\$JSONName" " `"Description`": `"$Desc`","
  Add-Content "c:\temp\$JSONName" " `"Actions`": ["
  Add-Content "c:\temp\$JSONName" " `"$Role`""
  Add-Content "c:\temp\$JSONName" "  ],"
  Add-Content "c:\temp\$JSONName" " `"NotActions`": [],"
  Add-Content "c:\temp\$JSONName" " `"AssignableScopes`": ["
  Add-Content "c:\temp\$JSONName" " `"/subscriptions/$SubID`""
  Add-Content "c:\temp\$JSONName" "   ]"
  Add-Content "c:\temp\$JSONName" "}"
  Write-Host "Adding role definition to $SubName"
  Try
  {
    $RoleObj = New-AZRoleDefinition -InputFile "c:\temp\$JSONName" -ErrorAction Stop
  }
  Catch
  {
    Write-Host "Did not add role definition, probably already exists" -ForegroundColor Yellow
  }
  # Add AD group
  $GroupID = (Get-AzADGroup -SearchString $ADGroup).ID
  $SubScope = "/subscriptions/$SubID"
  Write-Host "Adding $ADGroup to $RBACName"
  Try
  {
    New-AZRoleAssignment -ObjectID $GroupID -RoleDefinitionName $RBACName -Scope $SubScope -ErrorAction Stop
  }
  Catch
  {
    Write-Host "Did not assign role to group, group already has that role" -ForegroundColor Yellow
  }
  Write-Host "`n`n"
}
Cheers!






No comments:

Post a Comment