Skip to content

humanascode/azure-rbac-controller

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Azure RBAC Controller

Manage Azure RBAC role assignments as code using Terraform, with automated drift detection via GitHub Actions.

Overview

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

How It Works

  1. Run the provided map.ps1 PowerShell 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.
  2. Terraform import blocks are generated to import existing role assignments into Terraform state.
  3. 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.
  4. 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.
  5. GitHub Actions workflows are set up to:
    • terraform plan is triggered on pull requests to show proposed changes.
    • terraform apply is triggered on merges to the main branch to enforce changes (for approved and completed PRs).
    • detect-rbac-drift runs 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.

Import workflow

Prerequisites

  • 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

Setup Guide

Step 0: Fork this Repository (Or download as ZIP)

Option A: Fork (Recommended)

  1. Go to this repository on GitHub.
  2. Click the "Fork" button in the top-right corner to create a copy in your own GitHub account.
  3. Clone your forked repository locally: git clone https://github.com/<your-org>/<your-repo>.git

Option B: Download as ZIP

  1. Click the green "Code" button and select "Download ZIP".
  2. Extract the ZIP file to your local machine.
  3. Create a new repository in your GitHub organization.
  4. 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

Step 1: Create Azure Storage Account for Terraform State

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.Context

Step 2: Create Service Principal with OIDC for GitHub Actions

GitHub Actions will authenticate to Azure using OIDC (OpenID Connect) - no secrets to rotate!

2.1 Create App Registration

# Create the app registration
$app = New-AzADApplication -DisplayName "sp-terraform-rbac-manager"

# Note the AppId (Client ID)
$app.AppId

2.2 Create Service Principal

# Create service principal (using the $app variable from previous step)
$sp = New-AzADServicePrincipal -ApplicationId $app.AppId

2.3 Configure Federated Credentials for GitHub OIDC

Replace <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:

Federated credentials

2.4 Assign Required RBAC Permissions

The service principal needs permissions to:

  1. Manage role assignments on target subscriptions
  2. 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>"

Step 3: Configure GitHub Secrets

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.Id

Go to your GitHub repository → SettingsSecrets and variablesActionsNew repository secret 3 times to add the above secrets.


Step 4: Running the Mapper Script

Ensure you are logged into Azure with an account that has read access to the subscriptions you want to scan:

Connect-AzAccount

The 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"

Parameters

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)

Examples

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"

Generated Files

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).


Step 5: GitHub Actions Workflows and Pull Requests

Branch, commit and push the generated files

  1. Create a new branch in your GitHub repository using git checkout -b rbac-mapping
  2. Add the generated files to git: git add .
  3. Commit the changes: git commit -m "Import Azure RBAC role assignments"
  4. Push the branch: git push origin rbac-mapping

Terraform Plan on Pull Requests

  1. Open a pull request against the main branch
  2. The GitHub Actions workflow terraform-plan.yml triggers automatically
  3. Review the Terraform plan output in the PR checks
  4. You should only see resources to import and 0 for create, destroy, or change

Recommendation: Configure branch rulesets to protect the main branch. This ensures that all changes go through pull requests with proper review and that the Terraform plan workflow runs before merging.

Terraform Apply on Merge

  1. If you are satisfied with the plan, merge the PR into main
  2. The GitHub Actions workflow terraform-apply.yml triggers automatically
  3. The role assignments are now managed by Terraform!

Managing Role Assignments

Now that the role assignments are imported into Terraform, you can manage them via code. To do so:

  1. Open the relevant directory under subscriptions/<subscription-name>/

  2. edit the terraform.tfvars.json file 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)

  3. 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"
        }
      }
    }
  4. add commit and push to a new branch

  5. open a pull request against main

  6. review the plan output in the PR checks

  7. merge the PR to apply the changes

Drift Detection

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.

What 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.

How It Works

  1. 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
  2. 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

Drift detection


Troubleshooting

Authentication Errors

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

Permission Errors

If you encounter permission errors:

  • Ensure the service principal has User Access Administrator role on the target subscriptions
  • Ensure the service principal has Reader role on the target subscriptions
  • Ensure the service principal has Storage Blob Data Contributor on the storage account
  • If running the mapper script locally, ensure your account has read access to the subscriptions

Terraform State Errors

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 Contributor role on the storage account
  • Check that the AZURE_SUBSCRIPTION_ID secret is set to the subscription containing the storage account

Contributing

This project welcomes contributions and suggestions! Please feel free to raise an issue or create a pull request.


License

MIT License - see LICENSE for details.


Disclaimer

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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published