Manage Azure RBAC role assignments as code using Terraform, with automated drift detection via GitHub Actions.
This solution helps you:
- Export existing RBAC role assignments from Azure subscriptions into Terraform configuration
- Manage role assignments with Infrastructure as Code with version control and pull request reviews
- Support for ABAC conditions - role assignments with conditions are fully supported
- Exclude time-based (PIM) assignments - JIT access assignments are automatically excluded
- Detect configuration drift automatically via scheduled GitHub Actions workflows
- Apply changes safely with plan-on-PR and apply-on-merge workflows
- Run the provided
map.ps1PowerShell script to scan specified Azure subscriptions. The script maps out all of the existing role assignments in the subscription (at all scopes) and generates Terraform configuration and variables files. - Terraform import blocks are generated to import existing role assignments into Terraform state.
- Corresponding variables files are created to define the desired state of role assignments. These files can be used later to add, remove, or modify role assignments.
- After the files are generated by the script, commit and push the generated files to a new branch in your GitHub repository and create a pull request.
- GitHub Actions workflows are set up to:
terraform planis triggered on pull requests to show proposed changes.terraform applyis triggered on merges to the main branch to enforce changes (for approved and completed PRs).detect-rbac-driftruns on a schedule to check for any drift between the actual state in Azure and the desired state defined in Terraform. If any drift is detected, an issue is created in the repository for review.
- Azure PowerShell module linked here
- Terraform >= 1.6
- GitHub Runners with PowerShell and Terraform installed (PowerShell is pre-configured on GitHub-hosted managed runners)
- GitHub organization to host the repository
- GitHub repository for storing the configuration
- Azure Storage Account for Terraform state backend
Option A: Fork (Recommended)
- Go to this repository on GitHub.
- Click the "Fork" button in the top-right corner to create a copy in your own GitHub account.
- Clone your forked repository locally:
git clone https://github.com/<your-org>/<your-repo>.git
Option B: Download as ZIP
- Click the green "Code" button and select "Download ZIP".
- Extract the ZIP file to your local machine.
- Create a new repository in your GitHub organization.
- Initialize and push the code to your new repository:
cd <extracted-folder> git init git add . git commit -m "Initial commit" git remote add origin https://github.com/<your-org>/<your-repo>.git git branch -M main git push -u origin main
Create a storage account to store Terraform state files:
# Login to Azure
Connect-AzAccount
# Create resource group
New-AzResourceGroup -Name "rg-terraform-state" -Location "eastus"
# Create storage account (name must be globally unique)
$storageAccount = New-AzStorageAccount `
-ResourceGroupName "rg-terraform-state" `
-Name "tfstate<unique-suffix>" `
-Location "eastus" `
-SkuName "Standard_LRS" `
-AllowBlobPublicAccess $false `
-MinimumTlsVersion "TLS1_2"
# Create container for state files
New-AzStorageContainer -Name "tfstate" -Context $storageAccount.ContextGitHub Actions will authenticate to Azure using OIDC (OpenID Connect) - no secrets to rotate!
# Create the app registration
$app = New-AzADApplication -DisplayName "sp-terraform-rbac-manager"
# Note the AppId (Client ID)
$app.AppId# Create service principal (using the $app variable from previous step)
$sp = New-AzADServicePrincipal -ApplicationId $app.AppIdReplace <your-org> and <your-repo> with your GitHub repository details:
# Credential for pull requests (terraform plan)
$prCredential = @{
Name = "github-pr"
Issuer = "https://token.actions.githubusercontent.com"
Subject = "repo:<your-org>/<your-repo>:pull_request"
Audience = @("api://AzureADTokenExchange")
}
New-AzADAppFederatedCredential -ApplicationObjectId $app.Id @prCredential
# Credential for main branch (terraform apply)
$mainCredential = @{
Name = "github-main"
Issuer = "https://token.actions.githubusercontent.com"
Subject = "repo:<your-org>/<your-repo>:ref:refs/heads/main"
Audience = @("api://AzureADTokenExchange")
}
New-AzADAppFederatedCredential -ApplicationObjectId $app.Id @mainCredential
if you have configured the SP correctly, you should see the federated credentials listed:
The service principal needs permissions to:
- Manage role assignments on target subscriptions
- Read/write Terraform state in the storage account
# User Access Administrator on each subscription (to manage role assignments)
New-AzRoleAssignment `
-ObjectId $sp.Id `
-RoleDefinitionName "User Access Administrator" `
-Scope "/subscriptions/<subscription-id>"
# Reader on each subscription (to read resources)
New-AzRoleAssignment `
-ObjectId $sp.Id `
-RoleDefinitionName "Reader" `
-Scope "/subscriptions/<subscription-id>"
# Storage Blob Data Contributor on state storage account
New-AzRoleAssignment `
-ObjectId $sp.Id `
-RoleDefinitionName "Storage Blob Data Contributor" `
-Scope "/subscriptions/<subscription-id>/resourceGroups/rg-terraform-state/providers/Microsoft.Storage/storageAccounts/tfstate<unique-suffix>"Add the following secrets to your GitHub repository:
| Secret Name | Value |
|---|---|
AZURE_CLIENT_ID |
The App Registration Client ID (App ID) |
AZURE_TENANT_ID |
Your Azure AD Tenant ID |
AZURE_SUBSCRIPTION_ID |
Subscription ID where the Terraform state storage account resides (not the subscriptions being managed) |
To retrieve these values:
# Client ID (from the app registration created in Step 2)
$app.AppId
# Tenant ID
(Get-AzContext).Tenant.Id
# Subscription ID (where the storage account resides)
(Get-AzContext).Subscription.IdGo to your GitHub repository → Settings → Secrets and variables → Actions → New repository secret 3 times to add the above secrets.
Ensure you are logged into Azure with an account that has read access to the subscriptions you want to scan:
Connect-AzAccountThe map.ps1 script scans Azure subscriptions and generates Terraform configuration:
./map.ps1 -SubscriptionIds @("<subscription-id-1>", "<subscription-id-2>") `
-StorageAccountName "tfstate<unique-suffix>" `
-StorageAccountResourceGroup "rg-terraform-state"| Parameter | Required | Description |
|---|---|---|
-SubscriptionIds |
Yes | One or more Azure subscription IDs to scan |
-StorageAccountName |
Yes | Storage account name for Terraform state |
-StorageAccountResourceGroup |
Yes | Resource group containing the storage account |
-StorageAccountContainer |
No | Container name (default: tfstate) |
Single subscription:
./map.ps1 -SubscriptionIds "12345678-1234-1234-1234-123456789012" `
-StorageAccountName "tfstatemycompany" `
-StorageAccountResourceGroup "rg-terraform-state"Multiple subscriptions:
./map.ps1 -SubscriptionIds "sub-id-1", "sub-id-2", "sub-id-3" `
-StorageAccountName "tfstatemycompany" `
-StorageAccountResourceGroup "rg-terraform-state"The script creates the following structure:
subscriptions/
├── <subscription-name>/
│ ├── main.tf # Terraform configuration with RBAC module
│ ├── backend.tf # Azure backend configuration
│ ├── import.tf # Import blocks for existing role assignments
│ └── terraform.tfvars.json # Role assignment data (including conditions if present)
Note: The mapper script automatically excludes time-based (PIM/JIT) role assignments that have an expiration date. Only permanent role assignments are imported into Terraform.
Now everything is ready to run plan and apply to import and manage role assignments (steps continued in the next section).
- Create a new branch in your GitHub repository using
git checkout -b rbac-mapping - Add the generated files to git:
git add . - Commit the changes:
git commit -m "Import Azure RBAC role assignments" - Push the branch:
git push origin rbac-mapping
- Open a pull request against the
mainbranch - The GitHub Actions workflow
terraform-plan.ymltriggers automatically - Review the Terraform plan output in the PR checks
- You should only see
resources to importand 0 for create, destroy, or change
Recommendation: Configure branch rulesets to protect the
mainbranch. This ensures that all changes go through pull requests with proper review and that the Terraform plan workflow runs before merging.
- If you are satisfied with the plan, merge the PR into
main - The GitHub Actions workflow
terraform-apply.ymltriggers automatically - The role assignments are now managed by Terraform!
Now that the role assignments are imported into Terraform, you can manage them via code. To do so:
-
Open the relevant directory under
subscriptions/<subscription-name>/ -
edit the
terraform.tfvars.jsonfile to add, remove, or modify role assignments (note that each record in the json file has a numeric key, Keys are meaningless, they should just be unique within the file) -
You can add roles, modify roles or remove roles by editing the json file:
{ "permissions": { "0": { ... }, "1": { "roleDefinitionName": "Reader", "scope": "/subscriptions/<sub-id>/resourceGroups/<rg-name>", "principalId": "<user-or-group-object-id>" }, "2": { "roleDefinitionName": "Storage Blob Data Reader", "scope": "/subscriptions/<sub-id>", "principalId": "<user-or-group-object-id>", "condition": "@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringEquals 'my-container'", "conditionVersion": "2.0" } } } -
add commit and push to a new branch
-
open a pull request against
main -
review the plan output in the PR checks
-
merge the PR to apply the changes
A scheduled workflow runs daily to detect configuration drift. Configuration drift occurs when role assignments are changed outside of Terraform (e.g., manually in the Azure portal). To keep control over your role assignments, the drift detection workflow runs daily and creates an issue if any drift is detected.
The drift detection identifies two types of drift:
| Drift Type | Description |
|---|---|
| Missing | Role assignments that exist in Azure but are not managed by Terraform |
| ConditionMismatch | Role assignments where the ABAC condition or condition version differs between Azure and Terraform |
Note: Time-based (PIM/JIT) role assignments with an expiration date are automatically excluded from drift detection.
- Runs a PowerShell script that:
- Scans the specified subscriptions for current role assignments (using Az PowerShell module)
- Excludes time-based PIM assignments that have an expiration date
- Compares the current state with the desired state defined in the Terraform configuration
- Compares both role assignment existence AND ABAC conditions
- If differences are found, generates a detailed report
- Creates an issue if drift is detected with:
- A summary table showing all drifted role assignments
- Drift type (Missing or ConditionMismatch)
- Condition details if applicable
- Remediation instructions for missing role assignments including:
- Ready-to-use import blocks for
import.tf - JSON entries for
terraform.tfvars.json - Git commit and PR instructions
- Ready-to-use import blocks for
If GitHub Actions fails with authentication errors:
- Verify the federated credentials are configured correctly
- Check that the subject claim matches your repository and branch/PR
- Ensure the secrets are set correctly in GitHub
If you encounter permission errors:
- Ensure the service principal has
User Access Administratorrole on the target subscriptions - Ensure the service principal has
Readerrole on the target subscriptions - Ensure the service principal has
Storage Blob Data Contributoron the storage account - If running the mapper script locally, ensure your account has read access to the subscriptions
If Terraform cannot access the state file:
- Verify the storage account name and container name are correct
- Ensure the service principal has
Storage Blob Data Contributorrole on the storage account - Check that the
AZURE_SUBSCRIPTION_IDsecret is set to the subscription containing the storage account
This project welcomes contributions and suggestions! Please feel free to raise an issue or create a pull request.
MIT License - see LICENSE for details.
This project is provided as a helpful starting point for managing Azure RBAC with Terraform. While we've done our best to make it reliable, please review the code and Terraform plans before applying changes and always test in a non-production environment first. This project is provided as-is and without any warranty.


