What are Azure Roles and Custom Definitions?
When you start working more and more with Azure permissions you will undoubtedly have used Azure RBAC (also known as IAM) and have most likely used some of the great built-in roles that have been created and provided by Microsoft, but sometimes you may come across a requirement or a need to have a very specific role tailored with a set of permissions that are more granular than what comes out of the box in a standard Azure (RBAC) built-in role.
Luckily Azure offers a great deal of flexibility when it comes to defining your own custom roles vs built-in roles. This is where Custom Role Definitions comes into play.
Today we will look at how we can utilize GitHub actions in creating and also maintaining our Azure (RBAC) custom role definitions from a GitHub repository through source control and automatically publishing those changes in Azure through a GitHub actions workflow without much effort. If you are still a bit unclear on what Azure RBAC is, or wanted more information have a look at the Microsoft Docs.
Protecting secrets in github
Before we start using GitHub actions in this tutorial we will need the ability to authenticate to Azure. We will first create an 'Azure AD App & Service Principal'
giving the identity we create the relevant permissions to maintain custom RBAC roles and then store this identity credential as an encrypted GitHub Secret called 'AZURE_CREDENTIALS'
to use in our actions workflow to authenticate to Azure.
Create an Azure AD App & Service Principal
For this step I will be using Azure CLI using a powershell console. First we will log into Azure by running:
az login
Next we will create our Azure AD App & Service Principal
by running the following in a powershell console window:
# variables
$subscriptionId=$(az account show --query id -o tsv)
$appName="GitHub-RBAC-Admin"
# Create AAD App and Service Principal and assign Management Group Reader Role on Subscription
az ad sp create-for-rbac --name $appName `
--role "Management Group Reader" `
--scopes /subscriptions/$subscriptionId `
--sdk-auth
# Assign additional RBAC role to Service Principal - User Access Administrator on Subscription
az ad sp list --display-name $appName --query [].appId -o tsv | ForEach-Object {
az role assignment create --assignee "$_" `
--role "User Access Administrator" `
--subscription $subscriptionId
}
The above command will output a JSON object with the role assignment credentials. Copy this JSON object for later when we configure our github repository. You will only need the sections with the clientId
, clientSecret
, subscriptionId
, and tenantId
values:
{
"clientId": "<GUID>",
"clientSecret": "<PrincipalSecret>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>"
}
NOTE: The service principal we created has the RBAC/IAM roles: 'Management Group Reader'
and 'User Access Administrator'
, because we want our actions workflow script to be able to look at management groups and be able to change context as well as be able to create or amend role definitions at the scope/Subscription we want to maintain. In my case I only want to maintain RBAC for a single subscription. You can change the --scopes
parameter to change the permission scope of the service principal to a management group
instead if you want to use the actions workflow to maintain RBAC over multiple subscriptions.
Configure our GitHub repository
Firstly we will need to have a GitHub repository where we can store our custom role definition JSON files. If you need more information on how to set up a new GitHub repository, have a look here.
I called my repository [Azure-Role-Definitions]
. In my repository I have created 3 main folder paths:
.github/workflows: Here we will define and create our GitHub actions yaml file.
roleDefinitions: Here we will keep all our Azure custom role definition JSON files. This is also where we will maintain our custom role definitions when we need to make changes or even create new definitions we want to publish to Azure.
scripts: Here we will keep a simple PowerShell script that will be used in our actions yaml file.
Clone this newly created or existing repository and let's get started to create our first role definition JSON file now. You can also use my repo as a template by going HERE.
Remember in an earlier step we created an azure AD app & service principal and got a JSON object as output. We will now create a secret on our repository using the JSON object output, which our actions workflow will use to authenticate to Azure when it's triggered.
In GitHub, browse your repository.
Select Settings > Secrets > New repository secret.
Paste the JSON object output from the Azure CLI command we ran earlier into the secret's value field. Give the secret the name
AZURE_CREDENTIALS
.
Configure our first Custom Role Definition
We will create a simple role definition JSON that will only allow resource health read permissions, because we want to give someone the ability to look at resource health within a subscription in our tenant.
We will use the following JSON template structure to build our definition:
{
"Name": "",
"Id": "",
"IsCustom": true,
"Description": "",
"Actions": [],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": []
}
You can find more information on what each property in the JSON structure means HERE.
Our completed definition we will use in this tutorial will look something like below.
Note: Change the "AssignableScopes"
value with the subscription ID you want to publish and make this role available for use on.
{
"Name": "GH-CUSTOM-RESOURCEHEALTH-Reader",
"Id": "",
"IsCustom": true,
"Description": "Users with rights to only view Azure resource/service/subscription health.",
"Actions": [
"Microsoft.ResourceHealth/*/read"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/<subscriptioId1>"
]
}
Note: We can add more subscriptions to our assignable scopes or even use management groups if required. But for the purpose of this tutorial we only want to make the role available to a single Azure subscription. You will also notice that "Id": ""
is blank as our actions workflow script will take care of this value later on. Here are a few more valuable links for reference when creating custom role definitions:
Configure our GitHub actions workflow
The next thing we will do is create our GitHub actions workflow and script. Lets create the following yaml
file in our repository.
- Under
[.github/workflows]
create the following YAML file[Rbac-Apply.yml]
:
This is going to be our main actions workflow called: [Rbac-Apply]
.
Note: The workflow will only trigger on changes made to the repository path [roleDefinitions/*]
.
# '.github/workflows/Rbac-Apply.yml'
name: RBAC-Apply
on:
push:
paths:
- 'roleDefinitions/*'
jobs:
publish:
runs-on: windows-latest
steps:
- name: Check out repository
uses: actions/checkout@v3.6.0
with:
fetch-depth: 0
- name: Get changed files in .\roleDefinitions
shell: powershell
run: echo "CHANGED_FILES=$(git diff --name-only ${{ github.event.before }}..${{ github.event.after }})" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
- name: Log into Azure using github secret AZURE_CREDENTIALS
uses: Azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true
- name: 'Get changed role definitions'
shell: powershell
run: |
$changedFiles = "${{ env.CHANGED_FILES }}"
$changedFiles = $changedFiles.Split(' ')
$buildSourcesDirectory = $env:GITHUB_WORKSPACE
$resultArray = @()
Foreach ($file in $changedFiles) {
if ($file -like "roleDefinitions/*") {
$filePath = "$buildSourcesDirectory\$file"
$resultArray += $filePath
}
}
Write-Output "The following role definitions have been created / changed:"
Write-Output "$resultArray"
#Create a useable github environment variable array to string that will be used in powershell script
$psStringResult = @()
$resultArray | ForEach-Object {
$psStringResult += ('"' + $_.Split(',') + '"')
}
$psStringResult = "@(" + ($psStringResult -join ',') + ")"
#Set github env variable to use in powershell script as input
echo "ROLE_DEFINITIONS=$psStringResult" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
Write-Output "Convert array to psString:"
Write-Output $psStringResult
- name: Create - Update role definitions
uses: azure/powershell@v1
with:
inlineScript: |
.\scripts\set-rbac.ps1 -RoleDefinitions ${{ env.ROLE_DEFINITIONS }}
azPSVersion: 'latest'
Now under our repository folder path [scripts]
we will create a PowerShell script called [Set-Rbac.ps1]
.
Note: This powershell script calls cmdlets from the AZ module, so if a self-hosted GitHub actions runner is used instead of a GitHub-hosted runner
, please ensure that the AZ module is installed and configured on your runner. The below script may be amended to suit your environment better if you use deeply nested management groups. What the script below does is read in each JSON role definition from our repo under the path ./roleDefinitions/*.json
and then sets the context to one of the subscriptions defined in the JSON file 'AssignableScopes'
. Once in the context of a subscription, the script will evaluate whether a Custom Role Definition already exists in the context of the subscription, if it does the script will update the role definition with any changes or if the role does not exist it will be created.
# 'scripts/set-rbac.ps1'
#Parameters from github actions
Param (
[Parameter(Mandatory)]
[array]$RoleDefinitions
)
#Directory in use.
Write-host "Current Scripting directory: [$PSScriptRoot]"
#checked out build sources path
$BuildSourcesDirectory = "$(Resolve-Path -Path $PSScriptRoot\..)"
Write-host "Current checked out build sources directory: [$BuildSourcesDirectory]"
Foreach ($file in $RoleDefinitions) {
$Obj = Get-Content -Path $file| ConvertFrom-Json
$scope = $Obj.AssignableScopes[0]
If ($scope -like "*managementGroups*") {
$managementGroupSubs = ((Get-AzManagementGroup -GroupId ($scope | Split-Path -leaf) -Expand -Recurse).Children)
If ($managementGroupSubs.Type -like "*managementGroups") {
Set-AzContext -SubscriptionId $managementGroupSubs.children[0].Name
}
If ($managementGroupSubs.Type -like "*subscriptions") {
Set-AzContext -SubscriptionId $managementGroupSubs.Name[0]
}
#Test if roledef exists
$roleDef = Get-AzRoleDefinition $Obj.Name
If ($roleDef) {
Write-Output "Role Definition [$($Obj.name)] already exists:"
Write-Output "----------------------------------------------"
$roleDef
Write-Output "----------------------------------------------"
Write-Output "Updating Azure Role definition"
$Obj.Id = $roleDef.Id
Set-AzRoleDefinition -Role $Obj
}
Else {
Write-Output "Role Definition does not exist:"
Write-Output "Creating new Azure Role definition"
New-AzRoleDefinition -InputFile $file
}
}
If ($scope -like "*subscriptions*") {
Set-AzContext -SubscriptionId ($scope | Split-Path -leaf)
#Test if roledef exists
$roleDef = Get-AzRoleDefinition $Obj.Name
If ($roleDef) {
Write-Output "Role Definition [$($Obj.name)] already exists:"
Write-Output "----------------------------------------------"
$roleDef
Write-Output "----------------------------------------------"
Write-Output "Updating Azure Role definition"
$Obj.Id = $roleDef.Id
Set-AzRoleDefinition -Role $Obj
}
Else {
Write-Output "Role Definition does not exist:"
Write-Output "Creating new Azure Role definition"
New-AzRoleDefinition -InputFile $file
}
}
}
That's it, now each time a new JSON definition is added or an existing definition is amended on our repository under the path ./roleDefinitions/
the changes when pushed to our repo will trigger our github actions workflow and will auto-magically create or update any existing RBAC roles in Azure and we can now use proper version control and automation around governing our Azure RBAC custom role definitions using GitHub Actions.
We can also confirm that our role is now published and usable in Azure. 😄
I hope you have enjoyed this post and have learned something new. You can also find the code samples used in this blog post on my GitHub page or you can even use my repo as a template HERE ❤️
If you wanted to see how to do this using DevOps yaml pipelines instead have a look at one of my other posts below:
Automate Azure Role Based Access Control (RBAC) using DevOps
Marcel.L ・ May 5 '21
Author
Like, share, follow me on: 🐙 GitHub | 🐧 X/Twitter | 👾 LinkedIn
Top comments (4)
Hi Marcel. In order to give the app reg permissions to Management Group Reader would that need to come from a super admin on the tenant?. Fortunately you know where I work and what my limitations are so please advise.
Hi Clint,
To assign “Management Group Reader” permission to the AD App created, you would need to be at a minimum a “User Access Administrator” at the management group scope.
I can suggest that if you do not have the permission of “User access administrator” you could check with the tenant admin or team responsible for AAD and identities to assign the permission on your created app for you :)
So someone with the correct access could run the command in the tutorial on your behalf if you do not have the access to that.
one more question, if I have already created some custom roles manually in the azure portal am I able to retrofit these custom roles to your way of working in this blog?
Yes you can :)