Manage MCSB Control Overrides¶
The engineer's guide to switching off Microsoft Cloud Security Benchmark (MCSB) controls. Use this runbook to:
- Disable an MCSB control that is permanently not-applicable by design.
- Re-enable a control you previously disabled.
- Bring a new management group into scope.
- Work out why a subscription still shows a recommendation you disabled.
For what Secure Score measures and why changing this list moves it, see Defender for Cloud Secure Score.
Disable or exempt?
Disable a control only when it can never be met under the operating
model. For example, "more than one owner on subscriptions" fails forever
because Owner is only granted just-in-time through PIM. If the control
could be met one day, or only one resource can't meet it, use a
policy exemption with an expiresOn of 90 days or less.
Why there are two places to change¶
MCSB checks every subscription twice, through two separate policy assignments:
| Assignment | Scope | Owned by | Feeds | |
|---|---|---|---|---|
| A | psa-sec-mcsb-001 |
mg-intermediate-root, inherited by every subscription |
EPAC | Regulatory compliance dashboard |
| B | SecurityCenterBuiltIn ("ASC Default") |
Each subscription on its own | Defender for Cloud, which creates and keeps resetting it | Secure Score and Defender recommendations |
EPAC is told to leave Defender's own assignments alone
(keepDfcSecurityAssignments = true in global-settings.jsonc), so it can't
touch B. Terraform can't own B either: Defender would keep putting it
back. So B is handled by a small Terraform deployment that only patches
one property on it.
A control disabled in only one of them keeps showing up through the other. Always change both.
The projects¶
| Project | Lane | Holds |
|---|---|---|
azure-governance-epac |
B | Assignment A. Each control's effect lives in one row of pa-sec-MsftCloudSecurityBenchmarkV2.csv. |
azure-mcsb-overrides |
B | Assignment B, for every subscription in the mg-grinntec subtree except mg-sandboxes. The control list lives in disabled-controls.tf, the scope in management-groups.tf. |
Both are Tier 1: CI plans on the MR and applies on merge to main.
How azure-mcsb-overrides works¶
local.scanned_management_groups
│ data "azurerm_management_group" → .subscription_ids (direct children only)
▼
local.target_subscription_ids
│ for_each
▼
module "defender" → solution-manage-defender-for-cloud
→ terraform-azurerm-defender-for-cloud
▼
azapi_update_resource PATCH properties.overrides on
/subscriptions/<id>/providers/Microsoft.Authorization/policyAssignments/SecurityCenterBuiltIn
- Subscriptions are found automatically. Nobody lists them by hand.
- Terraform only changes
overrides. One override setspolicyEffect = Disabledfor every reference ID inlocal.disabled_mcsb_controls. Defender keeps owning everything else on the assignment. - A nightly pipeline schedule re-runs plan and apply on
main, so a new subscription is covered within a day. - Identity:
sp-pla-tf-mcsb-overrides-rwsigns in with OIDC. It hasReaderandResource Policy Contributoronmg-grinntecand is defined int0-azure-service-principals.
Before you start, find the control's reference ID¶
Both projects identify a control by its policyDefinitionReferenceId, the
member's name inside the MCSB initiative (for example
identityDesignateMoreThanOneOwnerMonitoring). It is not the display name
and not the policy definition GUID.
The quickest place to find it is the EPAC CSV, which lists every member it sets an effect for. If the control isn't there yet, look it up in the live initiative:
(Get-AzPolicySetDefinition -Name 'e3ec7e09-768c-4b64-882c-fcada3772047').PolicyDefinition |
Where-Object policyDefinitionId -match '<policy-definition-guid>' |
Select-Object policyDefinitionReferenceId, policyDefinitionId
Also write down why it can never apply. You'll need that for the code comment and for ADR-0001.
Disable a control¶
1. Disable it in EPAC (assignment A)¶
📁
azure-governance-epac: 📃Definitions/policyAssignments/security/pa-sec-MsftCloudSecurityBenchmarkV2.csv
Find the control's row and set prodEffect to Disabled. If it has no row,
add one. The name column is the policy definition GUID, not the reference
ID. EPAC fails Build-DeploymentPlans with "unknown Policy name" if you
put the reference ID there.
name,referencePath,policyDefinitionReferenceId,category,displayName,allowedEffects,prodEffect,prodParameters
09024ccc-0c5f-475e-9457-b7c0d9ed487b,,identityDesignateMoreThanOneOwnerMonitoring,Identity,More than one owner should be designated for subscriptions,AuditIfNotExists;Disabled,Disabled,{}
Disabled must be one of the values in allowedEffects.
Open an MR. Check the plan changes only psa-sec-mcsb-001's parameters,
then merge.
2. Disable it on every subscription (assignment B)¶
📁
azure-mcsb-overrides: 📃disabled-controls.tf
Add the same reference ID to local.disabled_mcsb_controls, with a comment
that gives the display name and the reason:
disabled_mcsb_controls = [
# "There should be more than one owner assigned to subscriptions"
# (09024ccc-…). No standing Owner role assignments by design; Owner is
# granted JIT via the PIM-eligible platform group.
"identityDesignateMoreThanOneOwnerMonitoring",
# Microsoft Defender for Key Vault should be enabled
"keyVaultsAdvancedDataSecurityMonitoringEffect",
]
Open an MR. CI posts the plan. Check it shows one in-place update per
subscription to module.defender["<sub-id>"].…azapi_update_resource.mcsb_overrides,
and that each update only changes the selectors[0].in list. It should
create or destroy nothing. Merge, and CI applies.
3. Record it in ADR-0001¶
📁
azure-governance-epac: 📃Docs/adr/ADR-0001-mcsb-audit-separate-enforcement.md
Add a row to the By-design not-applicable MCSB controls table with the reference ID, the reason, and both places it is disabled. This is the record auditors read. The code comments don't replace it.
Re-enable a control¶
Do the steps above in reverse:
- EPAC: set the CSV row's
prodEffectback to its audit effect (AuditorAuditIfNotExists). Merge. azure-mcsb-overrides: remove the reference ID fromlocal.disabled_mcsb_controls. Merge.- ADR-0001: remove the row, or mark it re-enabled with the date.
Removing the deployment does not undo the overrides
azapi_update_resource doesn't revert its change when it's destroyed.
If you delete the module call, or the whole deployment, the last override
stays on every subscription. To clear everything, set
disabled_mcsb_controls = [] and let it apply first. That sends an
empty overrides list.
Add a management group to scope¶
📁
azure-mcsb-overrides: 📃management-groups.tf
azurerm_management_group.subscription_ids only returns subscriptions that are
direct children of a management group. A subscription under a management
group that isn't listed is silently skipped.
When a new management group is added under mg-grinntec, add it here too:
scanned_management_groups = [
"mg-grinntec",
"mg-platform",
"mg-connectivity",
"mg-identity",
"mg-landingzones",
"mg-quarantine",
"mg-new-group", # ← new
]
The plan should show the new group's subscriptions being added. Leave
mg-sandboxes out on purpose: sandbox subscriptions are short-lived.
Check it worked¶
On one subscription, read the override straight from the assignment:
az policy assignment show \
--name SecurityCenterBuiltIn \
--scope /subscriptions/<sub-id> \
--query "overrides[0].selectors[0].in"
The output should match local.disabled_mcsb_controls exactly.
In the portal: Defender for Cloud → Recommendations. The control's recommendation drops out of the list, and out of Secure Score, once Defender re-assesses. That takes up to about 24 hours. Seeing it on the day of the merge doesn't mean the change failed.
When it fails¶
| Symptom | Cause | Fix |
|---|---|---|
Nightly run fails with 404 on …/policyAssignments/SecurityCenterBuiltIn for one subscription |
Defender for Cloud isn't on for that subscription yet, so the assignment doesn't exist. EPAC psa-sec-defender-001 turns it on, but only after a remediation cycle. |
Nothing. The next nightly run passes once EPAC has enrolled it. If it keeps failing for days, check the psa-sec-defender-001 remediation for that subscription. |
| A subscription still shows a disabled recommendation after 24h | Its management group isn't in scanned_management_groups, or it's under mg-sandboxes |
Add the group, or accept it if the subscription is a sandbox. |
| Recommendation is gone from Secure Score but still fails on the regulatory compliance dashboard | Disabled on B only | Do step 1. |
| Compliance dashboard is clean but the recommendation still appears | Disabled on A only | Do step 2. |
Build-DeploymentPlans fails with "unknown Policy name" |
CSV name column holds the reference ID instead of the policy definition GUID |
Put the GUID in name and the reference ID in policyDefinitionReferenceId. |
Plan wants to change selectors on every subscription when you didn't change the list |
Someone changed disabled-controls.tf on main without an apply, or the solution ref was bumped |
Read the diff. If it matches what's on main, let it apply. |
Secure Score will move
Disabling a control takes it out of the score calculation, so the score usually goes up after this change. Re-enabling one usually takes it down. Mention the expected effect in the MR so a jump in the weekly governance report isn't mistaken for real drift.