Azure / devops-governance

Example end-to-end Governance Model from CI/CD to Azure Resource Manager. Use this project to deploy example AAD, ARM and Azure DevOps resources to learn about e2e RBAC.
MIT License
189 stars 93 forks source link

Headless Owner service principal can escalate his privileges #47

Open oliviergaumond opened 2 years ago

oliviergaumond commented 2 years ago

This is a great project and documentation. In the accompanying article the recommendation is to create a headless owner role for the service principal.

{
  "Name": "Headless Owner",    
  "Description": "Can manage infrastructure.",
  "actions": [
    "*"
  ],
  "notActions": [
    "Microsoft.Authorization/*/Delete"
  ],
  "AssignableScopes": [
    "/subscriptions/{subscriptionId1}",
    "/subscriptions/{subscriptionId2}",
    "/providers/Microsoft.Management/managementGroups/{groupId1}"
  ]
}

However, even with these reduced permissions, as long as the Microsoft.Authorization/roleAssignments/Write permission is provided, the principal can elevate his own permissions and assign himself the Owner role and then proceed and remove any locks on resources.

Microsoft.Authorization/roleAssignments/Write is required if we want to be able to assign permissions to managed identities. Is there a way to achieve that, while avoiding providing owner access to the pipeline service account?

Could an Azure Policy help here to prevent what kind of access our custom role or service principal can assign? Or limit permission assignment only to managed identity principals?

Proof of concept script for escalation is below

# Create custom role
az role definition create --role-definition headless-owner.json

# create a resource group
az group create -n test-rg --location canadaeast

# create a lock
az lock create --name cant-delete --resource-group test-rg --lock-type CanNotDelete

# Create service principal with custom role
SPPWD=$(az ad sp create-for-rbac --name sp-pipeline --role "Headless Owner" --query password -o tsv)
SPUSER=$(az ad sp list --display-name sp-pipeline --query [].appId -o tsv)
SPID=$(az ad sp list --display-name sp-pipeline --query [].objectId -o tsv)

# Login using service principal
az login --service-principal -u $SPUSER -p $SPPWD --tenant <tenand-id>

# try to delete resource with a lock
az group delete -n test-rg --yes
# OK it fails

# try to remove the lock
az lock delete --name cant-delete --resource-group test-rg
# OK it fails

# Elevate our permissions to owner
az role assignment create --assignee-object-id $SPID --assignee-principal-type ServicePrincipal --role 'Owner' --scope "/subscriptions/<subscription id>"

# try to remove the lock again
az lock delete --name cant-delete --resource-group test-rg
az group delete -n test-rg --yes
# NOT OK was able to remove lock and delete resource group

# cleanup
az login #login with our regular user
az ad sp delete --id $SPID
az role definition delete --name "Headless Owner"
julie-ng commented 2 years ago

HI @oliviergaumond sorry I missed your message until now.

However, even with these reduced permissions, as long as the Microsoft.Authorization/roleAssignments/Write permission is provided, the principal can elevate his own permissions and assign himself the Owner role and then proceed and remove any locks on resources.

Yes, you're correct. The headless owner could elevate itself and remove locks. I will check with some colleagues, but as far as I know there isn't a way around that with another Azure mechanism like Azure policy. And it's one of those risks an organization has to accept when choosing to go the automation route. 100% security doesn't exist in a distributed system.

That's why it's really really important to secure that account and any other wrappers around it, e.g. at a minimum I would add an Approval Check in an Azure Pipeline to ensure there's a human watching the automation.

Sorry my GitHub notifications are always overflowing. If I'm slow to respond, you can also ping me at Twitter @jng5.

oliviergaumond commented 2 years ago

Hi @julie-ng, I managed to create an Azure Policy that limit which identities can be an owner of a given scope (i.e. subscription). Even though not perfect it is the best workaround I found to limit privilege escalations. I'll try to share the solution here.

julie-ng commented 2 years ago

❤️ Love the idea because it provides transparency without getting in the way (assume policy is set to audit instead of deny). If you can post the solution, great. If not, no worries. I'll leave the issue open for now and as a todo reminder for me.

And glad it works! And I really love customers who are pro-active and try to find great solutions themselves. They/you ask the best questions :D

oliviergaumond commented 2 years ago

The basic idea is to create a policy that will serve as an allow-list of which identities can be owner of a scope. So it can be set to Deny.

Policy definition Note: 8e3af657-a8ff-443c-a75c-2fe8c4bcb635 is the guid of the Owner built-in role, it is the same across all Azure tenants

{
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Authorization/roleAssignments"
        },
        {
          "field": "Microsoft.Authorization/roleAssignments/roleDefinitionId",
          "contains": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
        },
        {
          "field": "Microsoft.Authorization/roleAssignments/principalId",
          "notIn": "[parameters('allowedIdentities')]"
        }
      ]
    },
    "then": {
      "effect": "deny"
    }
  }

Policy parameters

{
    "allowedIdentities": {
        "type": "Array",
        "metadata": {
            "description": "List of Ids that can be owner of resources",
            "displayName": "Approved owner identities"
        }
    }
}

Once created the policy can be applied on a given subscription (i.e. veggies-prod) while specifying who can be Owners of this subscription (i.e. veggies-admins-group). So if a developer tries to abuse the permissions of veggies-ci-prod-sp and try to add someone else as an owner of the subscription it would be denied by the policy.

If you really need to change the owners on the subscription someone with Microsoft.Authorization policyAssignments permissions will need to first update the policy parameter to allow the new identity and then add the permissions.

The idea can probably be adapted to support variations of the scenario. (i.e. right now it expects a user identifier not a group)