Empowering developer teams to manage their own Azure RBAC permissions in highly regulated industries

Many highly regulated enterprises are looking to leverage the public cloud for the many benefits that it provides. Some examples are to increase agility of their developer teams, improve compliance and security & reduce costs.

Empowering developer teams to manage their own Azure RBAC permissions in highly regulated industries

Many highly regulated enterprises are looking to leverage the public cloud for the many benefits that it provides. Some examples are to increase agility of their developer teams, improve compliance and security & reduce costs.

Unfortunately, what usually happens is that their developer teams end up with partial autonomy, for example the teams can deploy their Azure resources but are not able to authorize their own Managed Identities or Service Principals to access these Azure resources.

In this post I'm going to propose one potential solution to increase the autonomy for these developer teams.

Azure Onboarding Process

Typically, enterprises build their own solution for automatically onboarding developer teams to the Azure platform, this solution leverages an onboarding identity to provision all the required resources in Azure. The solution may look something like the following:

Once a project has been through the onboarding process the following things are typically setup:

  • Azure DevOps - An Azure DevOps Project is created and seeded with a starter deployment pipelines; Self-Hosted Azure DevOps agents are provisioned in each of the Azure Subscriptions provided.
  • Deployment Identity - The Managed Identity or Service Principal used by the Project for deployment automation is created with the relevant Azure RBAC Roles. When deploying from Azure a Managed Identity is the recommended principal type as we do not need to store any secrets or manage lifetime of access tokens.
  • Azure Subscriptions - These live within the Enterprises Management Group hierarchy where Azure Policy enforces the Guardrails.

Azure Management Group Hierarchy

As I mentioned above the projects Azure Subscription lives within the enterprise Management Group hierarchy with which we can enforce governance controls for example assigning Azure RBAC permissions and Azure Policies.

An overly simplistic hierarchy may look something like the following, look at the Enterprise Landing zone - Management group and subscription organization documentation for details on building out more complex scenarios:

With the hierarchy above amongst other things we can:

  • Assign project teams Azure RBAC roles at a higher level than their individual subscriptions i.e. Project#1 and/or Project#3
  • Enforce Policy at an environment scope i.e. Dev and/or Prod, some policies may not make sense for certain environments or may require different parameters in each environment.

Azure Policies

Azure Policies allow us to enforce the guardrails on the Azure platform but still allow our developer teams some autonomy while adhering to these rules.

For our scenario I recommend the following Azure Policies to allow projects to perform their own role assignments during deployment but mitigate the risk of individuals performing an escalation of privilege attack.

  • Prevent Creation of Custom Roles - This prevents privilege escalation as we would be able to create a custom role with arbitrary Azure RBAC Permissions and potentially assign this to a principal we control. Custom RBAC roles are typically created & managed centrally therefore we want to restrict this activity for developer teams. We can use this policy in combination with the built-in Azure RBAC roles and/or a custom role which in addition restricts role creation via Azure RBAC permissions.
{
    "displayName": "Custom roles are not allowed",
    "description": "This policy will audit or deny the creation of RBAC custom roles.",
    "mode": "All",
    "parameters": {
        "effect": {
            "type": "String",
            "metadata": {
                "displayName": "Effect",
                "description": "Enable or disable the execution of the policy"
            },
            "allowedValues": [
                "Audit",
                "Deny",
                "Disabled"
            ],
            "defaultValue": "Deny"
        }
    },
    "policyRule": {
        "if": {
            "allOf": [
                {
                    "field": "type",
                    "equals": "Microsoft.Authorization/roleDefinitions"
                },
                {
                    "field": "Microsoft.Authorization/roleDefinitions/type",
                    "notEquals": "BuiltInRole"
                }
            ]
        },
        "then": {
            "effect": "[parameters('effect')]"
        }
    }
}
{
  
    "name": "allowed-principal-type-role-assignments", 
    "properties": {
        "displayName": "Allowed Principal Types",
        "description": "This policy defines a allow list of principal types that can be used in IAM(correct evaluation mandates use of Microsoft.Authorization/roleAssignments.apiVersion >= 2018-09-01-preview)",
        "mode": "All",
        "parameters": {
            "principalTypes": {
                "type": "Array",
                "metadata": {
                    "description": "The list of allowed principal types for role assignments.",
                    "displayName": "Allowed Principal Types"
                },
                "allowedValues": [
                    "User",
                    "Group",
                    "ServicePrincipal",
                    "Unknown",
                    "DirectoryRoleTemplate",
                    "ForeignGroup",
                    "Application",
                    "MSI",
                    "DirectoryObjectOrGroup",
                    "Everyone"
                ]
            },
            "effect": {
                "type": "String",
                "metadata": {
                    "displayName": "Effect",
                    "description": "Enable or disable the execution of the policy."
                },
                "allowedValues": [
                    "Audit",
                    "Deny",
                    "Disabled"
                ],
                "defaultValue": "Deny"
            }
        },
        "policyRule": {
            "if": {
                "allOf": [
                    {
                        "field": "type",
                        "equals": "Microsoft.Authorization/roleAssignments"
                    },
                    {
                        "field": "Microsoft.Authorization/roleAssignments/principalType",
                        "notIn": "[parameters('principalTypes')]"
                    }
                ]
            },
            "then": {
                "effect": "[parameters('effect')]"
            }
        }
    }   
}

For this policy to be correctly evaluated developer teams need to use apiVersion >= 2018-09-01-preview when executing their Microsoft.Authorization/roleAssignments. The way that the policy is implemented i.e. Allow-List rather than Deny-List means that it will fail closed, therefore there should be no risk of circumvention.
{
  
    "name": "allowed-role-definitions", 
    "properties": {
        "displayName": "Allowed Role Definitions",
        "description": "This policy defines a allow list of role definitions that can be used in IAM",
        "mode": "All",
        "parameters": {
            "roleDefinitionIds": {
                "type": "array",
                "metadata": {
                    "description": "The list of allowed role definition IDs. Example: If you were to put in b24988ac-6180-42a0-ab88-20f7382dd24c as a value then only the Contrbutor role definition can be assigned.",
                    "displayName": "Approved Role Definitions"
                }
            },
            "effect": {
                "type": "String",
                "metadata": {
                    "displayName": "Effect",
                    "description": "Enable or disable the execution of the policy."
                },
                "allowedValues": [
                    "Audit",
                    "Deny",
                    "Disabled"
                ],
                "defaultValue": "Deny"
            }
        },
        "policyRule": {
            "if": {
                "allOf": [
                    {
                        "field": "type",
                        "equals": "Microsoft.Authorization/roleAssignments"
                    },
                    {
                        "value": "[last(split(field('Microsoft.Authorization/roleAssignments/roleDefinitionId'),'/'))]",
                        "notIn": "[parameters('roleDefinitionIds')]"
                    }
                ]
            },
            "then": {
                "effect": "[parameters('effect')]"
            }
        }
    }   
}

Keep in mind that the Onboarding Identity may need to be exempted from some of these policies to perform Azure RBAC Role Assignments for the projects Deployment Identity on the Azure Subscription i.e. a built-in role like Owner , or custom role.

One place where we could assign these Azure Policies is at the environment Management Group scope, typically you would also have an Onboarding Identity per environment to ensure separation of concerns.

In any event where you assign the policies will be up to you and highly dependent on your management group hierarchy.

Azure RBAC Roles

Azure RBAC provides us a set of built-in roles which enterprises can leverage out of the box; before creating a custom role I would recommend reviewing the possibility of leveraging one of the built-in roles.  When leveraging a custom role, we need to ensure that the role grants the minimal Azure RBAC Permissions required.

In the case of our deployment identities which are created during onboarding, we need to ensure we include permissions to create role assignments and Managed Identities & block custom role creation, see the following example.

{
    "properties": {
        "roleName": "deployment",
        "description": "Custom Role Assigned to a Projects Deployment Identity",
        "assignableScopes": [],
        "permissions": [
            {
                "actions": [
                    "Microsoft.Authorization/roleAssignments/delete",
                    "Microsoft.Authorization/roleAssignments/write",
                    "Microsoft.Authorization/roleAssignments/read",
                    "Microsoft.ManagedIdentity/userAssignedIdentities/assign/action",
                    "Microsoft.ManagedIdentity/userAssignedIdentities/delete",
                    "Microsoft.ManagedIdentity/userAssignedIdentities/write",
                    "Microsoft.ManagedIdentity/userAssignedIdentities/read",
                    "Microsoft.ManagedIdentity/identities/read"
                ],
                "notActions": [
                    "Microsoft.Authorization/roleDefinitions/write",
                    "Microsoft.Authorization/roleDefinitions/delete"
                ],
                "dataActions": [],
                "notDataActions": []
            }
        ]
    }
}

The example above only outlines the permissions required to perform the tasks described in this blog post, you would need to include additional permissions based on what you allow in your environment. It's usually a good practice to start with one of the built-in roles as a starting point for your custom role, for example contributor.

How will developer teams assign Azure RBAC Roles at deploy time?

As the projects have dedicated Azure DevOps Agents deployed within their subscriptions, we can assign their Deployment Identity to the Virtual Machine or Kubernetes Pod which is running their agent.

The Azure CLI has built-in support for requesting access tokens for a Managed Identity, this will enable us to assign the relevant role at deploy time while the Azure Pipeline Job is executing on their Azure DevOps Agent.

For a system assigned identity.

> az login --identity

For a user-assigned identity, we need to provide the id of the identity to be used as multiple can be assigned to the same resource.

> az login --identity -u /subscriptions/<subscriptionId>/resourcegroups/myRG/providers/Microso
        ft.ManagedIdentity/userAssignedIdentities/myID

We can then perform the role assignment directly via the Azure CLI or via a standard ARM Template.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "roleDefinitionID": {
        "type": "string",
        "metadata": {
          "description": "Specifies the role definition ID used in the role assignment."
        }
      },
      "principalId": {
        "type": "string",
        "metadata": {
          "description": "Specifies the principal ID assigned to the role."
        }
      },
      "scope": {
        "type": "string",
        "metadata": {
          "description": "Specifies the scope of the role assignment."
        }
      }
    },
    "variables": {
      "roleAssignmentName": "[guid(parameters('principalId'), parameters('roleDefinitionID'), parameters('scope'))]"
    },
    "resources": [
      {
        "type": "Microsoft.Authorization/roleAssignments",
        "apiVersion": "2020-04-01-preview",
        "name": "[variables('roleAssignmentName')]",
        "properties": {
          "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roleDefinitionId'))]",
          "principalId": "[parameters('principalId')]",
          "scope": "[parameters('scope')]"
        }
      }
    ]
  }

We can execute the following command to deploy the sample ARM Template above.

az deployment group create \
  --name TestRoleAssignment \
  --resource-group TestRG \
  --template-file role-assignment.json \
  --parameters roleDefinitionID=<Role Definition ID> \
  --parameters principalId=<SP/MSI Principal ID> \
  --parameters scope=<Resource Group/Sub ID>

I hope you enjoyed this post and that these concepts help you to empower your developer teams to achieve more!