Skip to content

Initiative assignments require roleDefinitionIds of member policies whose effect is hardcoded to AuditIfNotExists — over-grants data-plane roles the assignment can never use #1393

Description

@ThojoUno

Describe the bug

When building the roles plan for a policy set (initiative) assignment, Build-DeploymentPlans computes the assignment identity's required roles as the union of roleDefinitionIds across every member policy, without considering each member's effective effect. Members whose effect is hardcoded in the initiative to AuditIfNotExists (or Disabled/Audit/Deny — effects that never deploy anything) still contribute their roleDefinitionIds to the assignment's required role assignments.

The result is standing role grants — including data-plane roles — at the assignment scope (often an intermediate-root management group) for capabilities the assignment can structurally never exercise.

Steps to reproduce

  1. Assign the built-in Microsoft Cloud Security Benchmark v2 initiative (/providers/Microsoft.Authorization/policySetDefinitions/e3ec7e09-768c-4b64-882c-fcada3772047, definitionVersion: 1.*.*-preview) at a management group via an EPAC assignment file.
  2. Run Build-DeploymentPlans.
  3. Inspect roles-plan.json.

Actual: the plan adds Azure Event Hubs Data Owner (f526a384-b230-433a-b45c-95f59c4a2dec) for the assignment MI at the MG scope:

{
  "scope": "/providers/Microsoft.Management/managementGroups/<root>",
  "roleDisplayName": "Azure Event Hubs Data Owner",
  "properties": {
    "roleDefinitionId": "/providers/Microsoft.Authorization/roleDefinitions/f526a384-b230-433a-b45c-95f59c4a2dec",
    "description": "Policy Assignment '.../Deploy-MCSB2-Monitoring': Role Assignment required by Policy, ..."
  }
}

Expected: no Event Hubs Data Owner grant. The only members of MCSB v2 that list that role are the 22 "Enable logging by category group for … to Event Hub" policies (Azure Firewalls, App Gateways, VNets, NSGs, Bastions, Load Balancers, Public IPs, ExpressRoute, Front Door/CDN, VPN/P2S gateways, Traffic Manager, Network Managers, …), and the initiative hardcodes every one of them to AuditIfNotExists (literal value in the member's parameters.effect.value, not a parameter reference — verifiable via az policy set-definition show --name e3ec7e09-768c-4b64-882c-fcada3772047). None of these members can ever run a deployment, so the identity never needs the role.

Root cause

In Build-DeploymentPlans.ps1 (module v11.5.5, same on main), the policy-set role map is built by unioning member roles with no effect check:

# Calculate roleDefinitionIds for built-in and inherited PolicySets
$readOnlyPolicySetDefinitions = $deployedPolicyResources.policysetdefinitions.readOnly
foreach ($id in $readOnlyPolicySetDefinitions.Keys) {
    $policySetProperties = Get-PolicyResourceProperties -PolicyResource $readOnlyPolicySetDefinitions.$id
    $roleIds = @{}
    foreach ($policyDefinition in $policySetProperties.policyDefinitions) {
        $policyId = $policyDefinition.policyDefinitionId
        if ($policyRoleIds.ContainsKey($policyId)) {
            $addRoleDefinitionIds = $PolicyRoleIds.$policyId
            foreach ($roleDefinitionId in $addRoleDefinitionIds) {
                $roleIds[$roleDefinitionId] = "added"
            }
        }
    }
    ...
}

The same union pattern exists in Build-PolicySetPolicyDefinitionIds.ps1 (repo-declared policy sets) and the Build-Hydration* mirrors. Build-AssignmentDefinitionAtLeaf.ps1 then consumes the pre-computed union verbatim for requiredRoleAssignments.

Note that $policyDefinition in the loop above already carries the member's parameters — including the hardcoded effect literal — so the information needed to filter is in scope at the defect site.

Proposed fix

Skip a member's roleDefinitionIds when its effect can never resolve to DeployIfNotExists, Modify, or Manual:

  • the member's parameters.effect.value is a literal Audit / AuditIfNotExists / Disabled / Deny (the MCSB v2 case), or
  • the effect is a parameter reference whose initiative-level parameter has allowedValues that exclude every deploying effect.

Where the effect is parameterizable and could flip to a deploying effect at assignment time, keep today's conservative union — the over-grant is only provable in the statically-resolvable case, and this keeps the change small and safe. (A fuller fix could resolve effective effects against assignment parameters at leaf-build time, but the static filter removes the concrete harm without a refactor.)

Happy to submit a PR along these lines if the approach is acceptable.

Why this matters

  • The grant is a data-plane owner role at MG scope (every Event Hub namespace under the root), held by an identity that can never use it. This is exactly what customer least-privilege / security reviews flag, and it must be explained away on every review.
  • Deployment SPs constrained by ABAC conditions on roleDefinitionIds (the delegation-constraint pattern) must allow-list roles nothing uses, widening what the SP itself may grant.
  • MCSB v2 makes this newly visible (large initiative, many audit-pinned members with data-plane roles), and its adoption is spreading through ALZ.

Ecosystem corroboration

Environment

  • EnterprisePolicyAsCode 11.5.5 (behavior verified in module source; union code unchanged on main)
  • PowerShell 7.4 / Azure DevOps hosted agents and local
  • Repro initiative: built-in MCSB v2 e3ec7e09-768c-4b64-882c-fcada3772047 @ 1.*.*-preview

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions