In this post we'll walk through the steps you can take to give a Service Principal a role with "Least Privilege" in Azure. After reading this article you will have a very practical method that you can use over and over again. You will be able to create roles for your Service Principals that will only allow them to deploy specific types of resources and only in the specified scopes.
Background
If you build an ARM Template or you get one from a 3rd party software company or services company, you will need permissions to deploy it. If you want to automate the deployment of that ARM Template, you will want to create a Service Principal that will do the deployment for you. Ideally the Service Principal will only have enough permission to deploy that ARM Template and do nothing else. If the Service Principal has broad permissions, like contributor or owner of an entire subscription or resource group, the Service Principal can be exploited.
For instance, the ARM Template can be altered and more services can be added to it. Or the Service Principal's credentials can be compromised or re-used in other automation pipelines.
So, how do you build a "Least Privilege" Service Principal with only the permissions that it needs? Let's find out.
Concepts
Here are the concepts I will discuss in this article.
- Service Principal - Essentially a Service Account that you can use to automate Azure.
- ARM Template - A declarative json file used for deploying Infrastructure As Code in Azure.
- Azure Subscription and Resource Groups are the scopes for where you can deploy Azure services. All Azure services live inside a Resource Group which lives inside a Subscription. You can scope permissions at the individual Resource level, the Resource Group level or for the whole subscription.
- Azure Built-In Roles and Azure Custom Roles. Roles are what determine what an identity can do in Azure. A user or a service principal doesn't have permission to do anything in Azure until it is assigned a role. Identities can have more than one role. Roles determine what actions you can perform in Azure and within what scope. Roles are at the heart of what this article is about!
- Permissions - there are thousands of permissions that determine what an identity can do in Azure. Building a role composed of only the minimum required permissions and only within the minimum required scope is how we get to least privilege.
Set Up
Here's what you'll need to follow along.
- An Azure Subscription with a Resource Group that YOU are the owner of. We are going to create a Service Principal that will be scoped to this Resource Group and this requires that you are an owner of it because you are delegating access to the Resource Group. Note that to create a Resource Group you need to be a Contributor or an Owner of a Subscription. Otherwise an Owner or Contributor will need to create a Resource Group for you and make you an owner.
- The Azure CLI. We will need two instances of the CLI running. In one instance you will sign in with YOUR credentials to create Service Principals and Roles. In the other instance you will sign in as the Least Privilege Service Principal.
- A simple text editor. I'm using VS Code.
- An ARM Template to deploy. You can find 100s of examples on the Azure Quickstart Templates site. I am going to use the Simple Umbraco CMS Web App Template. It uses various services like Azure App Service, Azure Storage, Azure SQL DB and Application Insights. Our Service Principal should ONLY be able to deploy those services in our chosen Resource Group. If we tried to use the same Service Principal to deploy Virtual Machines or Container Instances, it should fail.
Let's Start
Create a Service Principal
Sign in to the Azure CLI with your credentials and create a service principal. I will refer to this as your User CLI instance.
az ad sp create-for-rbac -n "leastsp" --skip-assignment
This will return something like this:
{
"appId": "55555555-5555-5555-5555-555555555555",
"displayName": "leastsp",
"name": "http://leastsp",
"password": "SuPerSecretP@ssw0rd",
"tenant": "00000000-0000-0000-0000-000000000000"
}
Make sure to copy and paste these details somewhere; you won't be able to see the password again!
Right now we have a Service Principal that has no permissions to do anything. It's just an identity in your Azure AD. Let's try to login to the Azure CLI.
Start another CLI instance. I will refer to this as the SP CLI. Sign in with this command (of course update the values with YOUR values):
az login --service-principal -u http://leastsp --tenant 00000000-0000-0000-0000-000000000000 -p SuPerSecretP@ssw0rd
You should get a message back that says No subscriptions found for http://leastsp.
That makes sense, this principal has no permissions to do anything yet!
We add permissions to a principal by assigning it a role. A role is essentially a container of permissions.
- Principals have roles.
- Roles have permissions.
Let's look at what roles are assigned to our Service Principal.
In the User CLI run this command (remember to use YOUR values):
az role assignment list --assignee 55555555-5555-5555-5555-555555555555
This returns an empty array []
. No roles assigned. 😦
To let the Service Principal login to your Azure subscription, let's give it a Reader role of the Resource Group that you own. If the Resource Group is called Least
, The ID for that Resource Group will be /subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least
.
Go back to your 1st CLI (the User CLI), the one where YOU are logged in. Type this in (last reminder to use YOUR values):
az role assignment create --role Reader --assignee 55555555-5555-5555-5555-555555555555 --scope "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least"
In the SP CLI try logging in again:
az login --service-principal -u http://leastsp --tenant 00000000-0000-0000-0000-000000000000 -p SuPerSecretP@ssw0rd
This time you should be successful and see something like this get returned:
[
{
"cloudName": "AzureCloud",
"homeTenantId": "00000000-0000-0000-0000-000000000000",
"id": "11111111-1111-1111-1111-111111111111",
"isDefault": true,
"managedByTenants": [],
"name": "Subscription Name",
"state": "Enabled",
"tenantId": "00000000-0000-0000-0000-000000000000",
"user": {
"name": "http://leastsp",
"type": "servicePrincipal"
}
}
]
And we should see the one Resource Group that our SP is now a Reader of:
az group list -o table
Name Location Status
------ ---------- ---------
least eastus Succeeded
The SP should ONLY be able to see the Resource Group that it is scoped to and nothing else in the subscription or any other subscriptions.
We can say that leastsp now has the Reader role, scoped to the Least Resource Group.
Now, let's try to deploy the Simple Umbraco template. We'll use the CLI command on that page to do the deployment. Remember to do this in your SP CLI:
az group deployment create --resource-group least --template-uri https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/umbraco-webapp-simple/azuredeploy.json --p '@parameters.json'
As you might expect, we get an error:
{"error":{"code":"AuthorizationFailed","message":"The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have authorization to perform action 'Microsoft.Resources/deployments/validate/action' over scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourcegroups/least/providers/Microsoft.Resources/deployments/azuredeploy' or the scope is invalid. If access was recently granted, please refresh your credentials."}}
This makes complete sense. Your SP only has a Reader role. We shouldn't expect a Reader to be able to deploy services! That would require the "write" kind of permissions. 😄
To deploy this template with least privilege, we will need to create a Role with only the permissions that are required.
Let's begin.
Building the Role
This is the main part of the article. Stay with me! 😅
We will create a role and add all the permissions we need to it. When we try to deploy the ARM Template, we will get an error message (like the one above) telling us about additional permissions that we need. We will add the permissions to the role and try to do the deployment again. We may need to repeat this several times as each step of the deployment reveals new permissions that are needed.
Look at the error message above. It's says our Service Principal doesn't have permissions to perform Microsoft.Resources/deployments/validate/action
.
Let's create a role that has this permission and assign it to our SP. Start by creating a role definition file.
{
"type": "Microsoft.Authorization/roleDefinitions",
"roleName": "leastprivilegeappdeployer",
"description": "Least Privilege App Deployer",
"assignableScopes": [
"/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least"
],
"name": "leastprivilegeappdeployer",
"roleType": "CustomRole",
"permissions": [
{
"actions": [
"Microsoft.Resources/deployments/validate/action"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
}
Save this file as role.json. You see that the role is called "leastprivilegeappdeployer" and it is assigned to our least resource group. The only permission it has is the perform the deployment validation action.
Let's create this role in your User CLI.
az role definition create --role-definition role.json
And, again in our User CLI, assign this role to our Service Principal
az role assignment create --role leastprivilegeappdeployer --assignee 55555555-5555-5555-5555-555555555555 --scope "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least"
We have now added a role to our SP that allows it to validate deployments. Let's try our deployment again. Switch over to the SP CLI. Before we redeploy, we should logout and log back in to make sure the CLI is updated with the SP's role.
In the SP CLI
az logout
az login --service-principal -u http://leastsp --tenant 00000000-0000-0000-0000-000000000000 -p SuPerSecretP@ssw0rd
Now try the deployment again in the SP CLI
az group deployment create --resource-group least --template-uri https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/umbraco-webapp-simple/azuredeploy.json --p '@parameters.json'
We should now get this REALLY LONG error:
{"error":{"code":"InvalidTemplateDeployment","message":"Deployment failed with multiple errors: 'Authorization failed for template resource 'umbracolzybcxduxe526' of type 'Microsoft.Sql/servers'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Sql/servers/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Sql/servers/umbracolzybcxduxe526'.:Authorization failed for template resource 'umbracolzybcxduxe526/umbraco-db' of type 'Microsoft.Sql/servers/databases'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Sql/servers/databases/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Sql/servers/umbracolzybcxduxe526/databases/umbraco-db'.:Authorization failed for template resource 'umbracolzybcxduxe526/AllowAllWindowsAzureIps' of type 'Microsoft.Sql/servers/firewallrules'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Sql/servers/firewallrules/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Sql/servers/umbracolzybcxduxe526/firewallrules/AllowAllWindowsAzureIps'.:Authorization failed for template resource 'lzybcxduxe526standardsa' of type 'Microsoft.Storage/storageAccounts'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Storage/storageAccounts/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Storage/storageAccounts/lzybcxduxe526standardsa'.:Authorization failed for template resource 'umbracolzybcxduxe526serviceplan' of type 'Microsoft.Web/serverFarms'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Web/serverFarms/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Web/serverFarms/umbracolzybcxduxe526serviceplan'.:Authorization failed for template resource 'umbracolzybcxduxe526' of type 'Microsoft.Web/Sites'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Web/Sites/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Web/Sites/umbracolzybcxduxe526'.:Authorization failed for template resource 'umbracolzybcxduxe526/MSDeploy' of type 'Microsoft.Web/Sites/Extensions'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Web/Sites/Extensions/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Web/Sites/umbracolzybcxduxe526/Extensions/MSDeploy'.:Authorization failed for template resource 'umbracolzybcxduxe526/connectionstrings' of type 'Microsoft.Web/Sites/config'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Web/Sites/config/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Web/Sites/umbracolzybcxduxe526/config/connectionstrings'.:Authorization failed for template resource 'umbracolzybcxduxe526/web' of type 'Microsoft.Web/Sites/config'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'Microsoft.Web/Sites/config/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/Microsoft.Web/Sites/umbracolzybcxduxe526/config/web'.:Authorization failed for template resource 'umbracolzybcxduxe526serviceplan-scaleset' of type 'microsoft.insights/autoscalesettings'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'microsoft.insights/autoscalesettings/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/microsoft.insights/autoscalesettings/umbracolzybcxduxe526serviceplan-scaleset'.:Authorization failed for template resource 'umbracolzybcxduxe526-appin' of type 'microsoft.insights/components'. The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have permission to perform action 'microsoft.insights/components/write' at scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least/providers/microsoft.insights/components/umbracolzybcxduxe526-appin'.'"}}
This is scary but it's also great because it gives us everything we need to fix it! Our SP can now validate the deployment but the validation shows that we need more permissions. We can pull all of these permissions out of the error message and update our custom role. Let's add these permissions to our role.json file.
{
"type": "Microsoft.Authorization/roleDefinitions",
"roleName": "leastprivilegeappdeployer",
"description": "Least Privilege App Deployer",
"assignableScopes": [
"/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least"
],
"id": "/subscriptions/11111111-1111-1111-1111-111111111111/providers/Microsoft.Authorization/roleDefinitions/33333333-3333-3333-3333-333333333333",
"name": "33333333-3333-3333-3333-333333333333",
"roleType": "CustomRole",
"permissions": [
{
"actions": [
"Microsoft.Resources/deployments/validate/action",
"Microsoft.Sql/servers/write",
"Microsoft.Sql/servers/databases/write",
"Microsoft.Sql/servers/firewallrules/write",
"Microsoft.Storage/storageAccounts/write",
"Microsoft.Web/serverFarms/write",
"Microsoft.Web/Sites/write",
"Microsoft.Web/Sites/Extensions/write",
"Microsoft.Web/Sites/config/write",
"microsoft.insights/autoscalesettings/write",
"microsoft.insights/components/write"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
}
You'll see that I also included the id field in the json. This value was generated for us by Azure and since we are going to update the role, we need to include the generated id going forward. You can get the id via the command az role definition list
in the User CLI. Here's an example:
az role definition list --scope /subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least --custom-role-only -n leastprivilegeappdeployer --query [0].id
Result
------------------------------------------------------------------------------------------------------------------------------------------
/subscriptions/11111111-1111-1111-1111-111111111111/providers/Microsoft.Authorization/roleDefinitions/33333333-3333-3333-3333-333333333333
In the User CLI, run the role update command:
az role definition update --role-definition role.json
Let's try the deployment again.
Back in the SP CLI, log out and login again and then try the deployment again.
az logout
az login --service-principal -u http://leastsp --tenant 00000000-0000-0000-0000-000000000000 -p SuPerSecretP@ssw0rd
az group deployment create --resource-group least --template-uri https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/umbraco-webapp-simple/azuredeploy.json --p '@parameters.json'
We are SO close! 😉
There's another error, but I promise this is the LAST one.
Azure Error: AuthorizationFailed
Message: The client '22222222-2222-2222-2222-222222222222' with object id '22222222-2222-2222-2222-222222222222' does not have authorization to perform action 'Microsoft.Resources/deployments/write' over scope '/subscriptions/11111111-1111-1111-1111-111111111111/resourcegroups/least/providers/Microsoft.Resources/deployments/azuredeploy' or the scope is invalid. If access was recently granted, please refresh your credentials.
Our role just needs the permission to actually write the deployments. Let's update our role.json one more time to give it this permission.
{
"type": "Microsoft.Authorization/roleDefinitions",
"roleName": "leastprivilegeappdeployer",
"description": "Least Privilege App Deployer",
"assignableScopes": [
"/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/least"
],
"id": "/subscriptions/11111111-1111-1111-1111-111111111111/providers/Microsoft.Authorization/roleDefinitions/33333333-3333-3333-3333-333333333333",
"name": "33333333-3333-3333-3333-333333333333",
"roleType": "CustomRole",
"permissions": [
{
"actions": [
"Microsoft.Resources/deployments/validate/action",
"Microsoft.Resources/deployments/write",
"Microsoft.Sql/servers/write",
"Microsoft.Sql/servers/databases/write",
"Microsoft.Sql/servers/firewallrules/write",
"Microsoft.Storage/storageAccounts/write",
"Microsoft.Web/serverFarms/write",
"Microsoft.Web/Sites/write",
"Microsoft.Web/Sites/Extensions/write",
"Microsoft.Web/Sites/config/write",
"microsoft.insights/autoscalesettings/write",
"microsoft.insights/components/write"
],
"notActions": [],
"dataActions": [],
"notDataActions": []
}
]
}
In the User CLI run the role update command
az role definition update --role-definition role.json
And now, in the SP CLI, logout, login and do the deployment.
az logout
az login --service-principal -u http://leastsp --tenant 00000000-0000-0000-0000-000000000000 -p SuPerSecretP@ssw0rd
az group deployment create --resource-group least --template-uri https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/umbraco-webapp-simple/azuredeploy.json --p '@parameters.json'
If it worked, congratulations! 👏 You built a role scoped to a single Resource Group with the least amount of permissions required to deploy this ARM template. You assigned the role to a Service Principal and did the deployment. You can visit the web app that got built and start configuring Umbraco. Awesome!
Remember, you can follow these steps with any other ARM Template you want to use.
If it didn't work, let me know what went wrong and I'd be happy to help you figure it out.
Clean Up
When you're done trying this out, remember to delete your Service Principal, your Custom Role and your Resource Group with all the resources from this template. In the User CLI you can use these commands with the appropriate flags:
az ad sp delete
az role definition delete
az group delete
Final Thoughts
Providing your operations team, your devops pipeline or your customers with an ARM Template is only a part of a secure automated deployment process. You should also provide a role that has the least amount of privileges to deploy that ARM Template. If a new service gets added to the ARM Template, the role should also be updated to reflect that change. Adopting this process helps reduce risk and exposure, especially from a security, compliance and cost control perspective.
If you're building roles that will be doing automated deployments, you can assume that you'll need the Microsoft.Resources/deployments/validate/action
and the Microsoft.Resources/deployments/write
permissions, so you can always start a role with those permissions.
I hope this was a helpful tutorial and that you'll be able to use this to secure your automated deployments in the future! If you have any feedback or suggestions please share them or leave them in the comments.
Thanks!
Top comments (5)
Hi Mike. Clearly explained and actionable - love it! At some point I did something similar with AWS while deploying a Cloudformation stack. Run it, see the error, fix the IAM permissions, repeat. It's great to see what the process is for Azure.
Something I was confused by: when you create a role definition, in the
role.json
file, should you provide theid
property explicitly? Or should the ID be generated by AAD when the definition is created? Thanks.Thanks @maxivanov . It was hard keeping track of the json file! :) I updated the article. Yes, Azure creates the id for you when you create a new role. You need to add that id into the json when you update the role. I added the command for finding that id too. Thanks!
It all makes sense now. Thanks!
Hi Mike, thanks for the step-by-step instructions!
One thing I run into is that when you grant the role App Developer to a Service Principal, and create (enterprise) apps using that account, then that SP lacks permissions to delete those 'owned' applications. I also can't find a way to make a custom admin-role that would grant that granular delete privileges. Am I missing something or is this not possible (yet)?
Cool explanation, this is what i am looking for. Appreciated!