How to Deploy to Azure with Least Privilege
Tags: azure, devops, security, cloud
Published: 2021-02-14 18:38:36

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](https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals#service-principal-object) - Essentially a Service Account that you can use to automate Azure.
* [ARM Template](https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/overview) - A declarative json file used for deploying Infrastructure As Code in Azure.
* [Azure Subscription](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits) and [Resource Groups](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal) 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](https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles) and [Azure Custom Roles](https://docs.microsoft.com/en-us/azure/role-based-access-control/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](https://docs.microsoft.com/en-us/azure/role-based-access-control/custom-roles#how-to-determine-the-permissions-you-need) - 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](https://docs.microsoft.com/en-us/cli/azure/get-started-with-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](https://code.visualstudio.com/).
4. An ARM Template to deploy. You can find 100s of examples on the [Azure Quickstart Templates](https://azure.microsoft.com/en-us/resources/templates) site. I am going to use the [Simple Umbraco CMS Web App](https://azure.microsoft.com/en-us/resources/templates/umbraco-webapp-simple/) 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. :frowning:

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](https://azure.microsoft.com/en-us/resources/templates/umbraco-webapp-simple/) 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. :smile:

__*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! :sweat_smile:

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! :wink:

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! :clap: 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!