Back to blog
AzureBest PracticesCloud SecurityIdentity & AccessOperations & Compliance

Azure Custom Role Uses Wildcard Actions: Risks and Fixes

Wildcard actions in Azure custom roles break least privilege and enable privilege escalation. Learn how to detect, fix, and prevent them with CLI and IaC.

TL;DR

This check flags Azure custom role definitions that grant wildcard (*) actions, which hand out far more permission than anyone needs. Replace the wildcards with the specific actions the role actually requires, and gate role definitions in CI so broad grants never reach production.

Azure RBAC gives you a clean way to scope permissions, but custom roles make it just as easy to throw that away. A single "*" in the actions array of a role definition turns a carefully named role like StorageOperator into something that can do almost anything in its assigned scope. This check catches those role definitions before an attacker, or a well meaning automation script, finds them first.


What this check detects

The authorization_wildcardactions check inspects every custom role definition in your Azure tenant and flags any that use a wildcard in the permissions block. Specifically, it looks at the permissions object inside a role definition and raises a finding when:

  • The actions array contains "*"
  • The dataActions array contains "*"
  • A scoped wildcard like "Microsoft.Compute/*" grants every operation on an entire resource provider (depending on your policy thresholds)

Here is what a problematic role definition looks like:

{
  "properties": {
    "roleName": "AppTeamOperator",
    "description": "Lets the app team manage their resources",
    "assignableScopes": [
      "/subscriptions/00000000-0000-0000-0000-000000000000"
    ],
    "permissions": [
      {
        "actions": [
          "*"
        ],
        "notActions": [],
        "dataActions": [],
        "notDataActions": []
      }
    ]
  }
}

That single "*" grants the assignee every management operation available across the entire subscription. The friendly role name hides how dangerous it actually is.

Note: Azure separates actions (management plane operations like creating a VM or reading a storage account's properties) from dataActions (data plane operations like reading blob contents or queue messages). A wildcard in either is a problem, but a dataActions wildcard is often overlooked because it does not show up in many RBAC reviews.


Why it matters

The whole point of a custom role is precision. If you needed everything, you would assign the built in Owner or Contributor role and move on. A custom role with a wildcard is the worst of both worlds: it looks scoped and intentional, but it behaves like a god mode grant.

The blast radius problem

When a role with "actions": ["*"] is assigned at a subscription scope, anyone holding that role can:

  • Modify or delete production resources across every resource group
  • Change network security group rules and open inbound access
  • Read and rotate secrets in Key Vault if no notActions restrict it
  • Create new role assignments and escalate privileges further

That last point is the dangerous one. If the wildcard role includes Microsoft.Authorization/roleAssignments/write (which "*" does), the holder can grant themselves or any compromised identity the Owner role. A medium severity service principal compromise becomes a full subscription takeover.

Danger: A wildcard custom role assigned to a service principal is a common privilege escalation path. If that service principal's credentials leak through a committed .env file, a misconfigured CI pipeline, or a phished developer, the attacker inherits everything the role can do, including the ability to create new role assignments and persist access.

The audit and compliance problem

Frameworks like CIS Azure Foundations, SOC 2, and ISO 27001 all expect least privilege to be enforced and demonstrable. A wildcard role makes that impossible. You cannot tell an auditor what an identity can do when the answer is "everything," and you cannot reason about the impact of a compromise.


How to fix it

The fix is conceptually simple: replace the wildcard with the explicit actions the role actually needs. The work is in figuring out what those actions are.

Step 1: Find your wildcard roles

List custom roles and inspect their permissions with the Azure CLI:

# List all custom role definitions in the current subscription
az role definition list --custom-role-only true \
  --query "[].{name:roleName, id:name, actions:permissions[0].actions}" \
  --output table

To pull the full definition of a specific role so you can edit it:

az role definition list --name "AppTeamOperator" --output json

Step 2: Determine what the role actually needs

The safest way to scope a role is to base it on real usage. Activity logs tell you which operations the assignees have actually performed:

# Pull recent write/action operations performed in the subscription
az monitor activity-log list \
  --start-time 2024-01-01T00:00:00Z \
  --query "[].{op:operationName.value, caller:caller, status:status.value}" \
  --output table

Tip: If you do not have enough activity log history, start from a relevant built in role instead of a blank slate. Run az role definition list --name "Storage Account Contributor" and copy its actions array as your baseline, then trim what you do not need.

Step 3: Rewrite the role with explicit actions

Replace the wildcard with the specific operations. Here is the corrected version of the earlier example, scoped to what an app team operator genuinely needs:

{
  "properties": {
    "roleName": "AppTeamOperator",
    "description": "Manage app team compute and storage resources, no role assignment rights",
    "assignableScopes": [
      "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/app-team-rg"
    ],
    "permissions": [
      {
        "actions": [
          "Microsoft.Compute/virtualMachines/read",
          "Microsoft.Compute/virtualMachines/start/action",
          "Microsoft.Compute/virtualMachines/restart/action",
          "Microsoft.Compute/virtualMachines/deallocate/action",
          "Microsoft.Storage/storageAccounts/read",
          "Microsoft.Storage/storageAccounts/listKeys/action",
          "Microsoft.Resources/subscriptions/resourceGroups/read"
        ],
        "notActions": [],
        "dataActions": [],
        "notDataActions": []
      }
    ]
  }
}

Note two other improvements here: the assignableScopes is narrowed to a single resource group rather than the whole subscription, and the description states the security intent explicitly.

Step 4: Apply the update

Warning: Tightening a role that is already assigned can break running workloads or automation. Before you apply, confirm which identities hold the role with az role assignment list --role "AppTeamOperator" and validate the new action set against their actual usage. Roll out to a non production subscription first if you can.

# Update the existing custom role from a definition file
az role definition update --role-definition app-team-operator.json

If you decide the role should not exist at all, remove it after confirming there are no active assignments:

Danger: Deleting a role that still has active assignments will strip those identities of their permissions immediately. Check and remove assignments first, then delete the role definition.

# Check for active assignments first
az role assignment list --role "AppTeamOperator" --all --output table

# Only then delete the definition
az role definition delete --name "AppTeamOperator"

Fixing it in infrastructure as code

If you manage roles with Terraform, the same principle applies. Avoid actions = ["*"] and list explicit operations:

resource "azurerm_role_definition" "app_team_operator" {
  name        = "AppTeamOperator"
  scope       = data.azurerm_resource_group.app_team.id
  description = "Manage app team compute and storage, no role assignment rights"

  permissions {
    actions = [
      "Microsoft.Compute/virtualMachines/read",
      "Microsoft.Compute/virtualMachines/start/action",
      "Microsoft.Compute/virtualMachines/restart/action",
      "Microsoft.Storage/storageAccounts/read",
      "Microsoft.Storage/storageAccounts/listKeys/action",
    ]
    not_actions = []
  }

  assignable_scopes = [
    data.azurerm_resource_group.app_team.id
  ]
}

The Bicep equivalent follows the same shape, with each action spelled out in the actions array rather than collapsed into a wildcard.


How to prevent it from happening again

Manual review does not scale. The reliable way to keep wildcards out is to block them before deployment and continuously scan what is already live.

Gate role definitions in CI/CD

Scan Terraform and Bicep for wildcard actions with a policy as code tool. With OPA / Conftest, a Rego rule like this fails the build:

deny[msg] {
  resource := input.resource.azurerm_role_definition[name]
  action := resource.permissions[_].actions[_]
  action == "*"
  msg := sprintf("Role '%s' uses wildcard action '*'. List explicit actions instead.", [name])
}

Run it as a required check in your pipeline:

terraform show -json plan.tfplan | conftest test --policy ./policies -

Enforce with Azure Policy

Azure Policy can audit or deny custom role creation at the management group level, giving you a backstop that catches roles created outside your IaC pipeline (for example, by someone clicking through the portal).

Note: Azure does not ship a built in policy that inspects custom role actions directly, but you can restrict who is allowed to create or modify role definitions at all by limiting Microsoft.Authorization/roleDefinitions/write to a small set of trusted identities. Combine that with a deny rule in CI, and the portal path closes too.

Continuous scanning

Roles drift. Someone always finds a reason to create a quick wildcard role "just for testing" that never gets cleaned up. Lensix runs the authorization_wildcardactions check on a schedule so a wildcard role that slips past your gates still surfaces as a finding within hours rather than at your next audit.


Best practices

  • Prefer built in roles. Azure ships hundreds of granular built in roles. Reach for a custom role only when none of them fit, and start from the closest built in role's action set.
  • Scope tightly. Set assignableScopes to the narrowest resource group or resource that makes sense, not the whole subscription, and never the management group unless the role genuinely needs to span subscriptions.
  • Use notActions deliberately, not as a fix for wildcards. Granting "*" and then carving out a few notActions is still a wildcard role. Anything you forget to exclude is granted. List what you allow, not what you deny.
  • Separate role assignment rights. Keep Microsoft.Authorization/*/write out of operational roles entirely. Privilege to grant privilege belongs to a small, audited set of administrators.
  • Review assignments regularly. Use Azure AD access reviews or Privileged Identity Management to re-certify who holds powerful roles, and remove stale assignments.
  • Name roles for their intent. A clear, specific name and description makes over scoped roles obvious in review and reminds the next engineer what the role is for.

A custom role should answer one question precisely: what can this identity do, and nothing more. Every wildcard you remove makes that answer something you can actually trust.

Azure Custom Role Wildcard Actions: Risks & Fixes | Lensix