DEV Community

loading...

How to Deploy to Azure with Least Privilege

michaelsrichter profile image Mike Richter ・13 min read

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.

  1. 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.
  2. 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.
  3. A simple text editor. I'm using VS Code.
  4. 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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

In the SP CLI try logging in again:

az login --service-principal -u http://leastsp --tenant 00000000-0000-0000-0000-000000000000 -p SuPerSecretP@ssw0rd
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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."}}
Enter fullscreen mode Exit fullscreen mode

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": []
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'.'"}}
Enter fullscreen mode Exit fullscreen mode

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": []
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the User CLI, run the role update command:

az role definition update --role-definition role.json
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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": []
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

In the User CLI run the role update command

az role definition update --role-definition role.json
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (4)

pic
Editor guide
Collapse
maxivanov profile image
Max Ivanov

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 the id property explicitly? Or should the ID be generated by AAD when the definition is created? Thanks.

Collapse
michaelsrichter profile image
Mike Richter Author • Edited

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!

Collapse
maxivanov profile image
Max Ivanov

It all makes sense now. Thanks!

Collapse
envegter profile image
Eric Vegter

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