Azure Security

Delegated RBAC administrator with Terraform

Delegated RBAC administrator with Terraform

TL;DR

Stop providing your Terraform identities with owner permissions on your subscriptions! Use delegated rbac admin instead so they can assign a subset of roles to other identities.

Jump to recipe

Background

During this post I will use the term “Terraform identity”. This is a simplification which means any entity you are authenticating with to Azure from CI/CD, local, or anywhere. Could be a service principal, managed identity, or even a user logged in with Azure CLI.

If you wanted to assign role assignments from Terraform back in the days, you had to assign the correct permission to the correct identity on the applicable scope. Owner permissions or at least User Access Administrator (which implicitly grants owner permissions anyway) on the entire subscription is necessary to allow for assigning roles to other entities. This is not a good practice when it comes to principle of least privilege, and opens you up for possible misuse of the identity.

What you actually want is for Terraform identity to have contributor access, which allows for almost all resource management, and potentially the ability to grant some role assignments.

The solution

Reader identity for unprotected branches (with some reader roles assigned) and contributor identity (with UAA excluding privileged roles) that has the permission to assign a subset of roles. More on the specific way of doing this below.

This solution potentially improves your security because you are running the plan stage (presumably in a feature branch with no review or approval process) as a reader with some added data plane access. And you are running the apply stage with a slightly broader contributor role assignment. Contributor role does not grant permission to manage role assignments.

Manual method

conditional assignment

You can use the portal to manually add the role assignment. I won’t go into this in detail here since Microsoft already has a guide. The only thing I will mention is that by default there are some roles missing from the privileged list.

  1. Choose add role assignment on your subscription.

  2. Use the “User Access Administrator” role as this grants only role assignment permissions.

  3. Add your identity (user, service principal, managed identity)

  4. Choose what user can do:

    • Allow only selected roles (recommended by me)
    • Allow all roles except privileged (recommended by Microsoft)
    • Allow all roles
  5. Choose your logic:

    • Allow specific
    • Allow specific for specific principal types
    • Allow specific for specific principals
    • Allow all except specific (recommended by me)

For the best middle way between security and useability I recommend the “Allow all except specific”, and exclude all privileged roles.

For the optimal security I recommend Allow specific for specific principals. This requires you to explicitly allow roles for only the provided principals.

Microsoft recommends you allow assigning all roles except privileged, but they are missing some privileged roles on their list. This leads me to recommend using the allow all roles except specific, because then you can add the missing privileged roles to the list.

privileged roles

Terraform assignment

Lets imagine you have a separate “master identity” that already has owner permissions on all your resources. This identity can be used in some form of CI/CD to assign permissions for other less privileged Terraform identities on a sub scope. Your master identity could have owner permissions on some higher management group, or on root level.

If you use Terraform for this, your code would look something like this:

locals {
  # Add whichever roles you want to this list and it will be excluded.
  # Roles excluded: Owner, User Access Administrator, Role Based Access Control, Contributor, Access Review Operator
  role_definition_ids = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168, b24988ac-6180-42a0-ab88-20f7382dd24c, 76cc9ee4-d5d3-4a45-a930-26add3d73475"
  # Set this to either null, ServicePrincipal, User, or Group
  principal_type = null
}

data "azurerm_subscription" "this" {
}

data "azurerm_client_config" "current" {
}

resource "azurerm_role_assignment" "sp_delegated_rbac_admin" {
  role_definition_name = "Role Based Access Control Administrator"
  scope                = data.azurerm_subscription.this.id # subscription id of the currently authenticated subscription
  principal_id         = data.azurerm_client_config.current.object_id # object id of the currently running terraform identity
  principal_type       = local.principal_type # leave at null to not restrict types
  description          = "Role Based Access Control Administrator role assignment with ABAC Condition."
  condition_version    = "2.0"
  condition            = <<-EOT
(
 (
  !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
 )
 OR
 (
  @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {${local.role_definition_ids}}
 )
)
AND
(
 (
  !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
 )
 OR
 (
  @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {${local.role_definition_ids}}
 )
)
EOT
}

This identity is presumably associated with your protected branch somehow (CI/CD variables or OIDC), and has permissions to handle your infrastructure and grant itself relevant reader roles like Key Vault Secrets User or Storage Blob Data Reader.

The other identity used for unprotected branches should still only have reader access on your subscription, but also “Reader and data access” on state storage account. If using entra id auth you should assign Storage Blob Data Owner or similar to allow for state refresh on plan (locking the blob might require data owner role).

The unprotected branch identity would also need some data plane permissions for storage account, ai services, or key vaults.

In summary

  • Use separate identities for your plan and apply stages.
  • Assign reader + specific data plane to reader identity and contributor with UAA delegated admin to the contributor identity.
  • Hopefully you are using managed identities by now.

Using Azure with the correct level of privilege can be challenging, but with this post you might save some time on your implementation. I hope you found the information provided useful 😊

In any case this post will serve as a form of noteToSelf. Please leave comments and criticism if you see something incorrect/insecure/inaccurate!