A gallery full of hardened, versioned, agent-equipped images doesn’t help the VM somebody deployed from a marketplace URN on a Friday afternoon. The image pipeline can be flawless and the fleet can still be a mix of baselined machines and one-off builds, because nothing in the platform requires anyone to use what the platform team publishes.
That’s the gap between publishing a golden image and enforcing one. Publishing is a pipeline problem. Enforcing is a governance problem, and in the Cloud Adoption Framework’s terms it belongs at the landing zone level: decided once, centrally, and applied by policy instead of by reminder emails. This post covers how to make the Compute Gallery the only way in, how to roll that out without breaking teams, and where the policy stops being able to help.
What the policy has to decide
Strip the idea down and the rule is simple: when a VM or scale set is created or updated, look at where its image came from. If it came from an approved gallery, allow it. If it didn’t, flag it or block it, unless there’s a documented exception.
Three details make that harder than it sounds:
- The image reference lives in different places depending on the resource type. Azure Policy hides most of that behind the
Microsoft.Compute/imageIdalias, which maps to the image reference on VMs, scale set VM profiles and disks. - Gallery images are identified by resource ID, which includes the gallery’s subscription and resource group. If your platform gallery lives in a shared-services subscription and is shared out via RBAC, the ID the consuming VM carries still points at that central gallery. That’s what you match on.
- Not every legitimate deployment uses your images. Vendor appliances, platform-managed services, and migration tooling all have reasons to deploy something else. The policy design needs an explicit answer for those, not a hole someone discovers later.
The policy definition
Here’s a definition built around those constraints. It takes a list of approved gallery IDs, an optional list of allowed marketplace publishers, and an effect parameter so the same definition can run in audit mode first and deny mode later.
policy-rule.json:
{
"if": {
"allOf": [
{
"field": "type",
"in": [
"Microsoft.Compute/virtualMachines",
"Microsoft.Compute/virtualMachineScaleSets"
]
},
{
"anyOf": [
{ "field": "Microsoft.Compute/imageId", "exists": "true" },
{ "field": "Microsoft.Compute/imagePublisher", "exists": "true" }
]
},
{
"count": {
"value": "[parameters('approvedGalleryIds')]",
"name": "gallery",
"where": {
"field": "Microsoft.Compute/imageId",
"like": "[concat(current('gallery'), '/*')]"
}
},
"equals": 0
},
{
"field": "Microsoft.Compute/imagePublisher",
"notIn": "[parameters('allowedPublishers')]"
}
]
},
"then": {
"effect": "[parameters('effect')]"
}
}policy-params.json:
{
"effect": {
"type": "String",
"allowedValues": ["Audit", "Deny", "Disabled"],
"defaultValue": "Audit",
"metadata": { "displayName": "Effect" }
},
"approvedGalleryIds": {
"type": "Array",
"metadata": {
"displayName": "Approved Compute Gallery resource IDs",
"description": "Full resource IDs of the galleries images may be deployed from."
}
},
"allowedPublishers": {
"type": "Array",
"defaultValue": [],
"metadata": {
"displayName": "Allowed marketplace image publishers",
"description": "Publishers permitted without a gallery image. Keep this list short and reviewed."
}
}
}Create it at the management group where you want it to be assignable:
az policy definition create \
--name vm-approved-gallery-images \
--management-group mg-platform \
--mode Indexed \
--display-name "VMs and VMSS must use approved Compute Gallery images" \
--rules policy-rule.json \
--params policy-params.jsonA few of the choices in that rule are deliberate:
A value count over the gallery list, with like. The like condition accepts only one wildcard, so each approved gallery ID gets a trailing /* appended and the count checks how many approved galleries the image matched. Zero matches means the image didn’t come from anywhere you approved. This scales to several galleries (per region, per business unit, a separate one for AVD) without rewriting the rule.
The exists guard. The rule only evaluates resources that carry an image reference at all. A VM created by attaching an existing OS disk, which is what restores and many migration tools produce, has no image to judge. That’s a conscious trade-off, not an oversight: those machines sit outside this policy, so you need another way to see them, which is where the Resource Graph post in this series comes in.
An allowed-publisher escape hatch, kept in the parameters. Rather than hardcoding exceptions into the rule, legitimate marketplace publishers are a parameter on the assignment, so the list is visible, reviewable, and different per scope.
What the policy can’t see
Be clear-eyed about the edges before you promise anyone full coverage:
- Direct shared and community gallery images are referenced through separate properties (
sharedGalleryImageIdandcommunityGalleryImageId) rather than the standard image ID. Don’t assume theimageIdalias covers them. Test deployments from each, and check the aliases available in your tenant (the Azure Policy extension for VS Code andGet-AzPolicyAliasboth list them) before relying on the policy to catch them. - Platform-managed services that deploy their own compute, such as AKS node pools and similar managed offerings, use images you don’t control and won’t match your gallery. Expect to carve these out with exclusions or exemptions rather than discovering it when a cluster upgrade fails.
- Existing resources aren’t blocked retroactively. Deny acts on create and update requests. Already-deployed machines show up as non-compliant at the next compliance evaluation cycle, roughly daily, and stay running.
- It checks the source, not the version. A VM deployed from last year’s end-of-life image version is perfectly compliant with this rule. Version hygiene is a separate control, covered by pinning in your deployment pipelines and by reporting.
Rolling it out without breaking anyone
Going straight to deny at the root management group is the fastest way to teach teams to route around the platform. A staged rollout looks like this:
Stage 1: audit, assigned wide. Assign with the effect set to Audit at a high scope. Nothing is blocked. You get a compliance picture of what’s out there, which almost always contains surprises: a vendor appliance nobody mentioned, a team on a marketplace image because the gallery doesn’t have their OS, a pile of VMs from before the gallery existed. If you want to preview the impact of a deny assignment without any enforcement or Activity log entries, the assignment’s enforcementMode: DoNotEnforce setting does exactly that.
Stage 2: fix and communicate. Work through the non-compliant list. Missing images get added to the gallery (gaps in your catalog are your problem, not the application team’s). Real exceptions get exemptions. Everything else gets a migration date. Write a clear non-compliance message on the assignment, because that’s what a developer sees when a deployment is rejected, and “policy violation” with no explanation generates support tickets:
targetScope = 'managementGroup'
param approvedGalleryIds array
param allowedPublishers array = []
@allowed(['Audit', 'Deny', 'Disabled'])
param effect string = 'Audit'
resource assignment 'Microsoft.Authorization/policyAssignments@2024-04-01' = {
name: 'enforce-gallery-images'
properties: {
displayName: 'VMs and VMSS must use approved Compute Gallery images'
policyDefinitionId: extensionResourceId(
managementGroup().id,
'Microsoft.Authorization/policyDefinitions',
'vm-approved-gallery-images'
)
parameters: {
effect: { value: effect }
approvedGalleryIds: { value: approvedGalleryIds }
allowedPublishers: { value: allowedPublishers }
}
nonComplianceMessages: [
{
message: 'Deploy from an approved Compute Gallery image. Browse the catalog in the platform gallery, or request an image or an exemption through the platform team.'
}
]
}
}Stage 3: deny, scoped by management group. Flip the effect to Deny starting at sandbox and non-production management groups, then production once the compliance numbers are clean. Because assignment scope follows the management group hierarchy, different parts of the estate can move at different speeds without separate definitions.
One consequence of deny worth planning around: it evaluates updates as well as creates. A non-compliant scale set that someone later modifies can have that routine change rejected, so migrate before you enforce, not after.
Handling exceptions properly
Some exceptions are legitimate and permanent, some are temporary. Azure Policy exemptions model both, and using them properly is what keeps the policy credible:
az policy exemption create \
--name vendor-firewall-appliance \
--policy-assignment "/providers/Microsoft.Management/managementGroups/mg-platform/providers/Microsoft.Authorization/policyAssignments/enforce-gallery-images" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-network-appliances" \
--exemption-category Waiver \
--expires-on "2027-03-31T23:59:00Z" \
--display-name "Vendor firewall appliance, marketplace image only" \
--description "Vendor does not support custom images. Revisit at renewal." \
--metadata ticketRef=PLAT-1423 approvedBy=platform-governanceTwo categories matter. Mitigated means the policy’s intent is satisfied another way (the appliance is vendor-hardened and independently scanned, say). Waiver means the non-compliance is temporarily accepted. Use waivers for anything that should eventually migrate, and always set an expiry. Treat a waiver without an end date as a permanent hole with better paperwork.
Keep two things in mind about expiry. An exemption that passes its expiresOn date stops applying, but the exemption object itself isn’t deleted, so stale ones accumulate. And the people who approve exemptions need a reporting view on them, which is a good use of Resource Graph.
Protect the source, not just the door
Enforcement at deployment time only means something if the gallery itself is trustworthy. If anyone with a contributor role on a subscription can publish a version into the approved gallery, you’ve turned the policy into a rubber stamp for whatever they pushed. Restrict write access on the gallery to the image-build pipeline’s identity, give consumers read access only, and keep the build pipeline itself behind the same review and approval you’d apply to production infrastructure code. The policy decides which gallery counts as trusted. The gallery’s RBAC decides who can put things in it. You need both.
The platform engineering takeaway
Golden images were never really about the image. They’re about being able to say that every machine in the environment started from a known, reviewed, patched baseline, and being able to prove it. Publishing the image gets you part of the way. Policy is what turns “we provide a baseline” into “the baseline is the only way in”, and it’s what makes the rebuild-instead-of-patch model enforceable instead of aspirational. Start in audit, fix your catalog gaps, exempt with an expiry, enforce by management group, and lock down who can publish.
Discover more from ksharp
Subscribe to get the latest posts sent to your email.