Managing Azure RBAC with terraform

Infrastructure as code is not nice to have, but the only solid and reliable way of deploying and managing your cloud platform and services in an enterprise. The first step into Azure is usually building out your secure landing zone, and an integral part of this is role-based access control. This article provides a possible approach on how to manage Azure RBAC with Terraform and gives some advice on best practices.

Why Terraform?

Let's first address the obvious question: should you use Terraform, or should you rather stick with Azure native ARM templates? This is not an easy question, as there is not a right or wrong answer to it. The main advantages I personally see with Terraform are the following:

  1. Terraform code is much shorter and more accessible.
  2. Terraform retains knowledge of the state of your deployments in its state file, and thus enables you to detect and remediate changes which have not been done via code.
  3. Terraform can also destroy resources or entire deployments.
  4. Terraform automatically takes into account dependencies between resources.

With regards to point 2, there is a small caveat here: in the case of Azure RBAC, Terraform does only evaluate the RBAC assignments and roles that it has created, it does not evaluate any other roles or assignments that were created via the Portal, CLI or PowerShell. So it is possible to create RBAC assignments outside of Terraform, which then won't be discovered and thus not removed during the next Terraform run.

There are surely more pros and also some cons to using Terraform over ARM templates. I encourage you to further read about the main differences and to come up with your own conclusion.

Azure RBAC best practices

Let's start by outlining some general best practices on Azure role-based access control:

  1. Use the built-in Azure roles wherever possible and avoid custom roles.
  2. If custom roles are needed, make them reusable by creating them at a higher scope.
  3. Assign roles only to groups, not directly to users.
  4. If you have an on-premises Active Directory (AD DS), consider managing your Azure RBAC groups in AD DS rather than on Azure AD. This choice depends heavily on your particular use case (e.g. where the users are created).
  5. Apply a naming convention for your RBAC groups.
  6. Always strive to follow a least-privilege approach, also with custom roles.

Most of these best practices apply not only to Azure, but to role-based access management in general. The question on AD DS over Azure AD for RBAC group management however mainly depends on your identity management strategy.

Let's get started with the preparations. We'll first prepare the AD DS (Azure AD) groups for RBAC assignments.

Azure AD (or AD DS) groups

There's not much complexity in creating security groups, neither in AD DS nor in Azure AD. In this particular case, I'm using the naming convention as follows:

sg_az_<resource_name>_<role_name>

Note that I'm using the underscore character for separation in the naming convention, as my Azure resources already include the hyphen character. This might of course differ in your environment.

So for our example, let's assume the following RBAC groups:

  • sg_az_sub-dev_reader (reader on subscription "sub-dev")
  • sg_az_rg-sap-p-euwe-01_cont (contributor on resource group "rg-sap-p-euwe-01")

I always apply a meaningful description to the group. When working with AD DS, you might also want to consider putting some information in the "notes" field (e.g. role and assignment scope). At this point in time, we do not necessarily need to have any members in one of the groups, as we need only the group itself for RBAC assignment.

Next, we prepare our Terraform files.

Terraform files

File structure

There are different approaches on how to separate your code into different Terraform files, depending also on the way you code (one-time code vs. reusable modules, etc.). In this case, I'll have my code distributed over 6 files:

  • az-rbac-main.tf (backend and providers)
  • az-rbac-mgmtgroups.tf (data sources for management groups)
  • az-rbac-subscriptions.tf (data sources for subscriptions)
  • az-rbac-identities.tf (data sources for identities)
  • az-rbac-roles.tf (custom roles definition)
  • az-rbac-assignments.tf (the actual RBAC assignments)

Backend

The Terraform backend determines how the state is loaded and how any operation (such as apply, plan, etc.) is executed. In this example I'm storing the state file in an Azure blob storage container (referred to as remote state).

terraform {
  backend "azurerm" {
    subscription_id      = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    resource_group_name  = "rg-iac"
    storage_account_name = "iactfstate"
    container_name       = "az-rbac"
    key                  = "az-rbac.tfstate"
  }
}

As we can easily read from the above snippet, the Terraform state file is stored into my storage account named "iactfstate" in the blob container "az-rbac". The state file will be named "az-rbac.tfstate".

Providers

We need two providers for our use case: "azurerm" (Azure Resource Manager) and "azuread" (Azure AD). The Azure provider will be used to assign roles, create custom roles and query subscriptions and management groups. The Azure AD provider is used only for the data sources of the identities.

# Azure Resource Manager
provider "azurerm" {
  version = ">=2.0.0"
  features {}
  subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  tenant_id       = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
}

# Azure AD
provider "azuread" {
  version = ">=0.7.0"
}

Both providers are declared without an alias, so they'll be referenced further on with "azurerm" or "azuread". We don't necessarily need an alias in this case, because we'll not be working with multiple subscriptions. For resource deployment we could imagine having multiple subscriptions targeted, thus needing multiple providers. For our needs, we can use only one subscription, as RBAC assignment via Terraform does not require to use the actual subscription we're assigning rights to, but any subscription in the tenant.

Management groups

The need for this file depends on your use of management groups on Azure. If you don't leverage these at all, you probably don't need this file.

# root mgmt group
data "azurerm_management_group" "mgt-root" {
  group_id = "mgt-root"
}

# SAP subscriptions
data "azurerm_management_group" "mgt-dev" {
  group_id = "mgt-dev"
}

I'm using the "azurerm_management_group" data source of Terraform to dynamically retrieve the management group attributes from ARM during the terraform execution.

Subscriptions

The Terraform data source "azurerm_subscription" retrieves a subscription and its attributes from ARM. The subscriptions are looked up using their subscription id.

# dev subscription
data "azurerm_subscription" "sub-dev" {
    subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

# prod subscription
data "azurerm_subscription" "sub-prod" {
    subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

I could be using variables for defining the subscription id for each subscription, but I chose to use the data sources provided by the terraform Azure provider. This will ensure that I always get the latest attribute values from ARM, and that any "plan" or "apply" already fails when getting the data into the data sources.

Identities

We talked about using mostly security groups to assign roles on Azure. However, you might also need to assign roles to service principals (e.g. for pipeline access) or even individual users.

# user1
data "azuread_user" "user1" {
  user_principal_name = "user1@contoso.com"
}

# service principal for pipeline
data "azuread_service_principal" "sp-cicd" {
  display_name = "sp-cicd"
}

# security group sg_az_sub-dev_reader_01
data "azuread_group" "sg_az_sub-dev_reader_01" {
  name = "sg_az_sub-dev_reader_01"
}

# security group sg_az_rg-sap-p-euwe-01_cont_01
data "azuread_group" "sg_az_rg-sap-p-euwe-01_cont_01" {
  name = "sg_az_rg-sap-p-euwe-01_cont_01"
}

You can see that I always name the Terraform object the same as the identity that I'm referencing. There are of course different approaches, and certainly better ones when it comes to Azure resource deployment. But for my use case with RBAC assignment, I've found it the easiest to reference the actual identity name in the object name.

I use data sources from the Terraform Azure AD provider for all identities, as they allow me to dynamically fetch the object ids needed for role assignment from Azure AD. This provides also an additional layer of validation, as Terraform will throw an error if it cannot find the appropriate identity in Azure AD.

Custom roles definition

Custom roles can be created with Terraform using the "azurerm_role_definition" resource. As of today, there is only one caveat to this: the Terraform Azure provider currently doesn't support creating custom roles at the tenant root group level. So you'll have to create the custom role on the level of one of your management groups, or directly on the level of a subscription, a resource group or a resource. I wouldn't recommend the latter if you don't have a really good use case for it, but it also depends on how you structure your resource groups.

A custom role definition with Terraform is pretty straight forward, and you can actually use the same parameters as you would use with an ARM template.

# create custom role for resource lock management
resource "azurerm_role_definition" "sub-dev_resource-lock-management" {
  name               = "custom_sub-dev_resource-lock-management"
  scope              = data.azurerm_subscription.sub-dev.id

  permissions {
    actions     = ["Microsoft.Authorization/locks/*"]
    not_actions = []
  }

  assignable_scopes = [
    data.azurerm_subscription.sub-dev.id
  ]
}

In this example I'm creating a custom role for resource lock management. This is a pretty common custom role to create, as managing resource locks is only allowed to the built-in "Owner" role. Resource locks provide a great way to protect your resources from accidental deletion or unwanted changes. However, you surely don't want to assign the Owner role to everybody for managing resource locks, as this implies that they could also entirely manage the RBAC assignment on the role assignment scope.

RBAC assignments

The last file contains all RBAC assignments. After having experimented with different approaches in the beginning (e.g. one Terraform file per subscription or per assignment scope), I have found this approach to be the most accessible. I can easily search for the name of an identity, a subscription, a management group or any other resource. I have also structured it using comments, to actually segregate the different subscriptions inside the file, but this is optional and depends on your preference.

So this is how an actual RBAC assignment to a subscription looks like in Terraform:

# assign reader role for team working on subscription sub-dev
resource "azurerm_role_assignment" "sub-dev-sg_az_sub-dev_reader_01" {
  scope                = data.azurerm_subscription.sub-dev.id
  role_definition_name = "Reader"
  principal_id       = data.azuread_group.sg_az_sub-dev_reader_01.id
}

The Terraform resource used is "azurerm_role_assignment". The subscription id is retrieved from the corresponding data source defined earlier, and will in this case deliver the resource id of the subscription (/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

If I want to assign the custom role created earlier, I need to slightly modify the code to use its role definition id instead of the role name:

# assign custom resource lock management role on subscription sub-dev
resource "azurerm_role_assignment" "sub-dev-sg_az_sub-dev_reader_01" {
  scope              = data.azurerm_subscription.sub-dev.id
  role_definition_id = azurerm_role_definition.sub-dev_resource-lock-management.id

  principal_id       = data.azuread_group.sg_az_sub-dev_reader_01.id
}

Note that in this case I'm not using a data source (thus I'm not referring to "data.") but the actual object of the role definition I've created in my custom role definition Terraform file.

Applying the configuration

There are multiple ways of actually deploying the Terraform code to your Azure environment, the most common and recommended one being pipelines, for example using Azure DevOps. You could also use the Cloud Shell, which already provides you with the Terraform executable, or Azure CLI and the Terraform executable installed on your workstation.

Terraform init

First you must initialize the Terraform backend by running terraform init.

No alt text provided for this image

Terraform plan

Terraform will first fetch the data for all your data sources from the respective provider.

No alt text provided for this image

After all data sources have been refreshed, Terraform will calculate the execution plan and will output the changes to be applied (only excerpt provided in screenshot):

No alt text provided for this image

Terraform apply

Once you're satisfied with the output of terraform plan, you can run terraform apply to actually deploy the changes to Azure, which you will have to confirm by typing "yes" when prompted.

Conclusion

In this article I've introduced a possible approach to managing your role-based access control for Azure with Terraform. There are certainly different approaches and you may find that others better respond to your needs. You might consider leveraging Azure Privileged Identity Management if you have the appropriate license, or you might also use ARM templates, Azure PowerShell, Azure CLI or any other infrastructure as code provider.

Thanks for reading this far. I encourage you to comment this article or to drop me a message here on LinkedIn if you want to further discuss the topic.

Odilichukwu Nwankwo. MSc, MBA

Enterprise Cloud Architecture. Multi Cloud Certified. IT Strategy. IT Management. IT Consulting. Program Management. Digital Transformation.

1y

Thanks for this. I guess this addresses the concern of "Azure RBAC is enforced on any action that's initiated against an Azure resource that passes through Azure Resource Manager."?

Hung Nguyen Van

Cloud Solutions, Cloud DevOps, Security & Operations | Azure | Network & System Administrator

1y

Hi, that's great article. That would be best if you can have an article with for_each ones. For example, we have multiple RBAC and grant different role_definition role names. Any guess?

khalil ennili

Cloud and DevOps Engineer

2y

thank you sebastian for this helpful article

James Eduard Andaya

Security Operations Lead @ Qi Group | DevSecOps Microsoft Security Certified | Microsoft Azure Certified | MikroTik Certified

2y

awesome thanks for sharing, do you have a repo for this?

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics