Secure Azure Deployments with Security Groups
Posted 07 Sep 2023
Automating infrastructure deployments to Azure invariably involves handing out permissions using Role Based Access Control (RBAC). A deployment pipeline can be allowed to grant roles at various scopes within the Azure environment. I've seen several environments where the pipeline was operating with owner permissions. This may be convenient but it opens up a path for unwanted privilege escalation because any developer with access to the pipeline can (ab)use it to grant unwanted privileges.
The solution described here was conceived before Azure had a way to constrain what roles could be assigned by an identity. This solves a lot of problems around delegating of role management to pipelines. Please check the docs.
A more secure solution would allow the pipeline to assign only narrow permissions at specific scopes. We can do this using Azure AD security groups. The group gets very specific permissions in Azure and all the pipeline can do is add and remove identities from the group.
This mitigates a large risk but has some tradeoffs.
The Microsoft Cloud Adoption Framework recommendation is to "avoid user-specific permissions. Instead, assign access to groups in Azure AD."
Context
Before getting into the tradeoffs, I'll set the stage with a concrete example.
Consider a hub-and-spoke architecture with Azure Container Apps deployed into the spokes. The hub and the spokes are all in separate Azure subscriptions. There's an Azure Container Registry in the hub repository where the build pipelines push the container images.
To pull down the container images into the container apps, the container apps environment needs to pull permission on the container registry. This is provisioned through a managed identity that is associated with the container app.
We'd like to enable teams to roll out their container apps so the pipeline deploying the resources to the spoke subscription needs to be able to grant the managed identity pull permission on the container registry, in the hub subscription.
Now, we could give the service connection rights to only manage RBAC on the container registry in the hub subscription by granting it the Role Based Access Control Administrator role. This would limit the scope of what the service connection can do but it's still a lot more privileges than it needs.
Imagine for a minute what a bad actor could do with this permission: the service connection could grant push permission to an arbitrary identity allowing that identity to poison images in the container registry.
Introducing Security Groups
So, an alternative solution to limit what the service connection can do is to set up security groups in Azure AD and grant specific rights to the group. The service connection will get the rights to manage users in the group.
Trade-offs
Security group set up
The first trade-off is that an Azure AD security group is not an Azure resource and cannot be deployed with Bicep or ARM. Either create it manually or automate it with Azure CLI or Azure Powershell.
From a security perspective, I'd favor manual creation. From a DevOps perspective, I'd automate it in a pipeline. Either way, you'll get extra complexity in your deployment process to set up the security groups.
Limitations of Azure AD
The second trade-off to this solution is that the internal permission structure of Azure AD is not fine-grained enough to allow an identity to only be able to add and remove security group members. There are permissions to allow this but these can only be applied at the tenant scope. The least privileged access that can be granted on a security group is to make the service principal that runs the pipeline a group owner.
Group owner is a bit too broad but a lot less risky overall because it prevents rights escalation beyond what the group can do.
Assigning users to groups from a pipeline
Assigning RBAC permissions with IaC solutions like bicep is pretty straightforward. Each assignment is an Azure resource so they are part of the resource template.
When we use group membership instead of direct RBAC role assignments, we need to resort to scripting with Azure CLI or Azure Powershell. The scripts can execute from within ARM or Bicep templates or in the pipeline after the infrastructure deployment completes. So there's the third trade-off: more complexity in the deployment pipeline.
In the latter case, outputting a list of member object IDs in an output parameter is required.
In keeping with the least privileged access principle, we do not want to assign a service principal read access to the Azure AD tenant. Read permission is required to resolve names to object IDs in Azure AD. So in our scripting, we need to specify members and the group itself to be as object IDs.
Implementation
The following bit runs after the infrastructure deployment and takes the user IDs to assign to the group from the outputs of the deployment.
- task: AzureCLI@2
displayName: 'Add members to group Az-Acr-Pull'
inputs:
azureSubscription: ${{parameters.serviceConnection}}
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$outputs=ConvertFrom-Json '$(deploymentOutputs)'
$acrPullMembers=$outputs.acrPullMembers.value
$groupId="1111111-2222-3333-444-555555555555" # Object ID for your group
$acrPullMembers | ForEach-Object {
Write-Host -ForegroundColor Green "Adding member $_ to security group $groupId"
$consoleOut = az ad group member add --group $groupId --member-id $_ 2>&1
if ($consoleOut -match "already exist") {
Write-Host -ForegroundColor Yellow "Already a member of the group"
$global:LASTEXITCODE=0 # prevent script from returning an error
}
elseif ($consoleOut -match 'ERROR:')
{
Write-Error $consoleOut -ErrorAction Stop
}
}
Summary
Using security groups we can get closer to implementing the least-privilege principle in our Azure environments. It is not a perfect solution because it requires us to set up security groups in Azure AD and hand out rights to manage the group. This adds complexity to our deployment process.
Additionally, Azure AD does not have fine-grained management permissions to manage group memberships.
I would recommend using this setup when protecting shared resources that may have a direct impact on the production environment.
General advice
- Run deployments from your own private set of agents
This limits the risks of privileged credentials falling into the wrong hands - Do not grant the owner role to any service principal on any scope.
The owner role is far too permissive and includes access to, for example, billing information, which a pipeline never needs. - Use security groups to enable pipelines to assign very narrow and scoped permissions.
- Grant the Role Based Access Control Administrator at the most narrow scope possible if you need to allow the pipeline to manage Azure RBAC permissions.