This check flags Azure resource groups that have no management lock, leaving everything inside them open to accidental or malicious deletion. Add a CanNotDelete or ReadOnly lock to critical resource groups to prevent costly mistakes.
Resource groups are the unit of organization in Azure, and they hold everything from production databases to networking and storage accounts. A single mistaken az group delete, an overly broad Terraform destroy, or a fat-fingered click in the portal can wipe out an entire environment in seconds. Management locks are Azure's built-in guardrail against exactly this, and this check tells you which resource groups are running without one.
The Resource Group Has No Management Lock check (resources_nolock) inspects each resource group in your subscription and reports the ones that lack a lock at the group scope.
What this check detects
Azure management locks let you restrict who can modify or delete resources, regardless of their RBAC permissions. They come in two flavors:
- CanNotDelete — authorized users can still read and modify the resource, but nobody can delete it.
- ReadOnly — authorized users can read the resource, but cannot modify or delete it. This is stricter and behaves like assigning the Reader role to everyone.
This check looks at the resource group scope and raises a finding when no lock object exists there. A resource group without a lock means any principal with Microsoft.Resources/subscriptions/resourceGroups/delete permission can tear it down, and so can anyone who runs an infrastructure pipeline that targets it.
Note: Locks are inherited downward. A lock applied at the resource group scope applies to every resource inside it, including resources created later. You do not need to lock each resource individually.
Why it matters
Management locks sit above RBAC. Even a subscription Owner cannot delete a resource protected by a CanNotDelete lock without first removing the lock, which adds a deliberate, auditable step. That extra friction is the entire point.
Here are the scenarios this check is designed to prevent:
- Accidental deletion. An engineer cleaning up a test environment selects the wrong resource group in the portal and confirms the delete. Without a lock, the production database goes with it.
- Runaway IaC. A Terraform plan with a state drift issue or a bad variable points
terraform destroyat the wrong workspace. A lock causes the destroy to fail loudly instead of succeeding silently. - Compromised credentials. An attacker who phishes a privileged account can use deletion as a destructive or extortion tactic. Locks slow them down and force an extra action that monitoring can alert on.
- Automation gone wrong. A misconfigured cleanup script or a CI job with broad scope sweeps resources it should never touch.
Warning: Deleted Azure resources are usually gone for good. Some services offer soft-delete windows (Key Vault, storage blobs, recovery vaults), but most do not. A deleted SQL database or VM with no backup is unrecoverable. Locks are cheaper than recovery.
The business impact is straightforward: lost data, broken production, hours of restoration work, and in regulated environments, a reportable incident. For the cost of a single API call, a lock removes an entire class of failure.
How to fix it
You can add a lock through the Azure CLI, the portal, PowerShell, or infrastructure as code. Pick the one that fits your workflow.
Azure CLI
Create a CanNotDelete lock at the resource group scope:
az lock create \
--name "prevent-delete" \
--lock-type CanNotDelete \
--resource-group "prod-app-rg" \
--notes "Protects production resource group from deletion"
Verify the lock was applied:
az lock list \
--resource-group "prod-app-rg" \
--output table
If you want the stricter read-only behavior for a resource group that should never change, use ReadOnly instead:
az lock create \
--name "freeze-rg" \
--lock-type ReadOnly \
--resource-group "shared-network-rg" \
--notes "Network is managed centrally, no ad-hoc changes"
Warning: ReadOnly locks block more than you might expect. Some read operations are implemented as POST calls under the hood, so listing storage account keys, restarting a VM, or scaling a service can fail. Test ReadOnly in a non-production group before applying it broadly. CanNotDelete is the safer default for most resource groups.
Azure Portal
- Open the resource group in the Azure portal.
- In the left menu under Settings, select Locks.
- Click Add.
- Enter a lock name, choose Delete or Read-only, add a note explaining why, and save.
PowerShell
New-AzResourceLock `
-LockName "prevent-delete" `
-LockLevel CanNotDelete `
-ResourceGroupName "prod-app-rg" `
-LockNotes "Protects production resource group from deletion"
Terraform
If you manage infrastructure as code, define the lock alongside the resource group so it is created and tracked automatically:
resource "azurerm_resource_group" "prod" {
name = "prod-app-rg"
location = "eastus"
}
resource "azurerm_management_lock" "prod_lock" {
name = "prevent-delete"
scope = azurerm_resource_group.prod.id
lock_level = "CanNotDelete"
notes = "Protects production resource group from deletion"
}
Tip: When a lock is managed by Terraform, your IaC tooling becomes the controlled path for removing it. To run a legitimate destroy, you remove the lock resource in code, apply, then destroy. This keeps every lock change in version control and code review.
Removing a lock when you genuinely need to
Danger: Removing a lock re-exposes every resource in the group to deletion. Only do this for a deliberate, reviewed change, and re-apply the lock immediately afterward. Removing a lock requires the Microsoft.Authorization/locks/delete permission, which should be granted to a small set of principals.
az lock delete \
--name "prevent-delete" \
--resource-group "prod-app-rg"
How to prevent it from happening again
Adding locks one at a time does not scale. Bake them into provisioning and enforce them with policy so new resource groups never ship without protection.
Audit existing groups with a quick script
Find every resource group in a subscription that has no lock:
for rg in $(az group list --query "[].name" -o tsv); do
count=$(az lock list --resource-group "$rg" --query "length(@)")
if [ "$count" -eq 0 ]; then
echo "No lock: $rg"
fi
done
Enforce with Azure Policy
Azure Policy cannot create locks directly through its built-in effects, but you can use a DeployIfNotExists policy with a remediation task that calls a deployment template to add the lock. Pair it with an audit effect first so you can see the scope before enforcing.
Note: A simpler and very common pattern is to deploy locks as part of your landing zone template. Tools like the Azure Landing Zone accelerator and Bicep modules add CanNotDelete locks to platform resource groups (networking, identity, management) automatically at subscription vending time.
Gate it in CI/CD
If you use Terraform, add a check to your pipeline that fails when a resource group module is missing its companion azurerm_management_lock. A policy-as-code tool such as OPA/Conftest or Checkov can enforce this against your plan output.
Example Conftest rule (Rego) against a Terraform plan:
package main
deny[msg] {
rg := input.resource_changes[_]
rg.type == "azurerm_resource_group"
not has_lock(rg.change.after.name)
msg := sprintf("Resource group '%s' has no management lock", [rg.change.after.name])
}
has_lock(rg_name) {
lock := input.resource_changes[_]
lock.type == "azurerm_management_lock"
contains(lock.change.after.scope, rg_name)
}
Tip: Run the same audit logic continuously with Lensix instead of cron jobs and ad-hoc scripts. The resources_nolock check reports unlocked resource groups across all subscriptions on every scan, so a group created outside your IaC pipeline gets flagged before it becomes a problem.
Best practices
- Lock production and shared groups by default. Anything holding databases, networking, identity, or stateful workloads should carry a
CanNotDeletelock. Treat unlocked production as a misconfiguration. - Prefer
CanNotDeleteoverReadOnlyfor general use. It protects against the worst outcome (deletion) without breaking day-to-day operations. ReserveReadOnlyfor genuinely frozen environments. - Always add a note. The
notesfield tells the next engineer why the lock exists and who owns it. An undocumented lock that blocks a deploy at 2 a.m. causes more friction than it should. - Manage locks in code. Locks defined in Terraform or Bicep are visible, reviewable, and reproducible. Locks added by hand in the portal drift and get forgotten.
- Restrict who can delete locks. The
Microsoft.Authorization/locks/*actions sit under the Owner and User Access Administrator roles. Keep that list short and audit changes to it. - Do not rely on locks alone. Locks are one layer. Combine them with backups, soft-delete where available, least-privilege RBAC, and activity log alerts on delete operations.
- Skip locks on ephemeral environments. Short-lived dev or PR-preview resource groups that are meant to be torn down should not be locked. Scope your enforcement to the environments that matter.
Management locks are one of the cheapest controls in Azure and one of the highest leverage. A few minutes of setup, or a few lines in your landing zone template, removes an entire category of accidental and malicious destruction. Find your unlocked resource groups, lock the ones that matter, and enforce it in your pipeline so the gap never reopens.

