Blog Post

Azure Governance and Management Blog
8 MIN READ

Automating Compliance Scope Enforcement with Azure Policy

gurjsing's avatar
gurjsing
Icon for Microsoft rankMicrosoft
Sep 01, 2026

Cloud compliance certifications like C5, ISO 27001, HIPAA, or PCI DSS never cover the whole platform. They only cover a defined set of services, listed in an official document published by Microsoft. A common and costly mistake is to assume that “Azure is certified for X” automatically means every Azure service is covered. In reality, the list of covered services changes with every new document version, and it is often different for Azure Public compared to Azure Government.

Let's walk through this technically, step by step, using Microsoft's C5 (Germany) compliance offering as a concrete example. The same approach works for any other compliance standard that publishes a similar service-scope table — only the source document and the resulting parameter values change.

Within that C5 example, an even more common mistake goes one step further: "Azure is C5-certified, so my workloads are automatically C5-compliant." That is not true. C5 works on a shared-responsibility model. The customer still has to set up things like:

  • 🔐 RBAC and Privileged Identity Management (PIM) 

  • 🌐 Network Segmentation 

  • 📊 Logging & Monitoring 

  • 🔑 Key Management 

  • 💾 Backup & Recovery Concepts

One thing almost always gets missed on top of that: whether the Azure service itself is actually C5-certified in the first place. Across numerous Landing Zone assessments🔍, this is consistently the point that gets overlooked. If a service isn't certified, it should be blocked as a resource type before anyone can even deploy it - which is exactly the process we walk through next.

🛠️A Five-Step Approach

📄Step 1 - Identify the Authoritative Source 

For C5, Microsoft publishes an "Azure Compliance Offerings" document (Service Trust Portal). It has two appendices listing exactly which services are covered for Azure Public and for Azure Government - and the two lists are not the same. 

📑Step 2 - Extract the Scope Data in a Structured Form 

This document is published as a PDF, with the actual data laid out as tables: services listed against certification columns. Instead of typing rows into a spreadsheet by hand, there are a few practical ways to turn that into structured data. A small one-time script (for example, using a PDF text-extraction library) is the most repeatable option, but a tool like Microsoft 365 Copilot can also read the table directly out of the PDF and export it as CSV or JSON, which is a fast alternative if nobody on the team wants to write or maintain a script.

Either way, the goal is the same: end up with a structured CSV or JSON file, not a static PDF, since that file becomes the single source of truth for everything that follows. The real benefit shows up the second time: when a new document version comes out, redoing the extraction gives a clean diff showing exactly which services were added or removed.

Below is an example of how that extract can look once it's structured - two services from the August 2026 document that are not part of the Germany C5 scope, each mapped to its actual Azure resource provider and resource types:

{
  "NonAllowedResources": [
    {
      "name": "Azure Kubernetes Fleet Manager",
      "resourceProvider": "Microsoft.ContainerService",
      "resourceTypes": ["fleets"]
    },
    {
      "name": "Azure Storage Actions",
      "resourceProvider": "Microsoft.StorageActions",
      "resourceTypes": [
        "locations",
        "locations/asyncoperations",
        "locations/operationStatuses",
        "operations",
        "storageTasks",
        "storageTasks/reports",
        "storageTasks/storageTaskAssignments"
      ]
    }
  ]
}

Note: In addition to a dedicated PDF-parsing script, the download-and-extract step can be handed off to an LLM entirely. A Foundry Agent (or a plain Azure OpenAI call) can be given a URL or file input, download the document itself via a connected tool/function, and return the extracted table directly as JSON matching the schema above.

⚖️Step 3 - Allow-List vs. Deny-List

An allow-list only permits certified services. For highly regulated environments, allow-lists are often preferred because they enforce an explicit "only what is approved" model. It is the stricter option, but harder to maintain, because most services in a mature platform are usually already certified. A deny-list only blocks the few services that are not certified, which is usually much easier to maintain, since that list is typically short. Which option makes sense still depends on your risk appetite and the specific requirement you're working with.

Note: in some cases, it isn't the whole service that's missing certification, just a specific sub-feature of it. A single entry in the source document can bundle several sub-services under one name, and they don't always share the same status. Treating the whole entry as one block, in either direction, can silently make your scope too strict or too loose - so it's worth checking the exact granularity in the source document rather than assuming a sub-service inherits its parent's status.

🧩Step 4 - Map to Azure Policy

Not every entry in a compliance document is something Azure Policy can act on, and the document doesn't flag the difference for you. Entries fall into two buckets: services that map to an actual, deployable ARM resource type, which Azure Policy can deny or audit directly, and entries that describe a portal feature or platform capability with no deployable resource behind them at all - for example, "Microsoft Azure Portal" or the "Quota+ Usage Blade", both listed in the same table. These entries therefore require different handling during the mapping process. The real effort in this step is sorting entries into these two buckets and, for the first one, translating business-facing service names into the exact resource type strings Azure Policy expects. Once that mapping exists, the policy definition itself is relatively straightforward.

Take the two entries from the Step 2 extract. Azure Kubernetes Fleet Manager flattens to a single resource type, Microsoft.ContainerService/fleets. Azure Storage Actions flattens to several, such as Microsoft.StorageActions/storageTasks and Microsoft.StorageActions/storageTasks/reports. Once every entry has been flattened this way, they all feed into the same parameterized deny policy

param policyName string = 'deny-out-of-compliance-scope-services'
resource denyOutOfScope 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
  name: policyName
  properties: {
    displayName: 'Deny resource types outside approved compliance scope'
    policyType: 'Custom'
    mode: 'Indexed'
    parameters: {
      NonAllowedResources: {
        type: 'Array'
        metadata: {
          displayName: 'Excluded resource types'
          description: 'Resource types outside current compliance scope.'
        }
      }
    }
    policyRule: {
      if: {
        field: 'type'
        in: '[parameters("NonAllowedResources")]'
      }
      then: {
        effect: 'deny'
      }
    }
  }
}

A practical governance model must balance control with flexibility. When teams need to evaluate new capabilities, such as preview services not yet covered by existing standards, policy exemptions provide a controlled exception mechanism. By requiring an expiry date, organizations can prevent temporary deviations from evolving into long-term compliance gaps.

resource temporaryExemption 'Microsoft.Authorization/policyExemptions@2022-07-01-preview' = {
  name: 'exemption-preview-service-evaluation'
  properties: {
    policyAssignmentId: assignment.id
    exemptionCategory: 'Waiver'
    displayName: 'Temporary evaluation of a non-certified preview service'
    description: 'Approved by security review on 2026-08-15; must be re-assessed before expiry.'
    expiresOn: '2026-11-15T00:00:00Z'
  }
}

Note: A deny effect is a good default for new deployments, but not always for existing environments. For live workloads, start with audit mode to assess impact before enforcing deny. Consider the newer compliance substate for exemptions, which keeps exempted resources visible in compliance reporting instead of excluding them.

🔄Step 5 - Automate & Audit 

Keep policy definitions and assignments as code, alongside the scope file from Step 2, in the same repository as the rest of your Landing Zone infrastructure. At a high level, this splits into two workflows: one that gets hold of the latest source document and regenerates the JSON scope file, and a second one that takes an approved change to that file and redeploys the policy assignment.

A key advantage of this approach is that the document retrieval process is isolated from the rest of the workflow. For this publicly accessible document, the workflow simply queries the Trust Portal API to obtain the latest version ID and then downloads the corresponding file.

name: sync-c5-compliance-scope
 
on:
  schedule:
    - cron: '0 6 * * 1'   # weekly check for a new compliance document
  workflow_dispatch: {}
 
permissions:
  contents: write
  pull-requests: write
 
jobs:
  extract-and-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Download the compliance document (public doc, no auth needed)
        run: |
          DOC_ID="7adf2d9e-d7b5-4e71-bad8-713e6a183cf3"
          VERSION_ID=$(curl -s "https://api.servicetrust.microsoft.com/api/v2/GetLiveDocumentIncludingOldSeriesDoc/$DOC_ID" | jq -r '.id')
          curl -sS -o compliance-offerings.pdf "https://api.servicetrust.microsoft.com/api/v2/downloadDocuments/$DOC_ID/$VERSION_ID"
 
      - name: Extract current compliance scope
        run: python scripts/extract_c5_scope.py compliance-offerings.pdf --out compliance/c5-scope.json
 
      - name: Open a PR if the scope changed
        uses: peter-evans/create-pull-request@v6
        with:
          title: 'Update C5 compliance scope'
          branch: update-c5-scope
          add-paths: compliance/c5-scope.json

The second workflow runs only after approval and merge into the main branch. It leaves the extraction process untouched, instead translating the domain-based JSON from Step 2 into the flattened resource type array required by Azure Policy and redeploying the assignment defined in Step 4 with the updated parameter set.

name: deploy-c5-policy
 
on:
  push:
    branches: [main]
    paths: ['compliance/c5-scope.json']
 
permissions:
  id-token: write   # required for OIDC federated login
  contents: read
 
jobs:
  deploy-policy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
 
      - name: Flatten the extract into the policy's parameter format
        run: |
          jq '[.NonAllowedResources[] | .resourceProvider as $rp | .resourceTypes[] | "\($rp)/\(.)"]' \
            compliance/c5-scope.json > excluded-resource-types.json
 
      - name: Redeploy policy assignment with updated scope
        run: |
          az deployment mg create \
            --management-group-id ${{ vars.MG_ID }} \
            --template-file infra/policy-assignment.bicep \
            --parameters excludedResourceTypes=@excluded-resource-types.json

The key benefit of this design is the strict separation between change generation and change deployment. The extraction workflow is limited to creating reviewed pull requests, while the deployment workflow acts solely on approved and merged content. This guarantees that no unreviewed modification can reach a production policy assignment, and that safeguard remains in place regardless of how the underlying document is sourced.

There are plenty of other ways to implement this - the following are just meant as inspiration:

  • Someone downloads the public PDF into a storage account container. The GitHub workflow is then triggered manually, fetches the PDF from blob storage instead of the Trust Portal API, and continues as normal from there (extraction → diff → PR).
  • Instead of doing this through workflows at all, a Logic App could just as well be the preferred choice, handling these exact same steps end to end on its own.

🎯Compliance as Code, Not Compliance as Spreadsheet

Any list you hardcode will go out of date, sooner or later. A process that re-reads the list whenever needed stays correct indefinitely. The C5 document is a good example of why: the same service can be certified for Azure Public but not for Azure Government, or the other way around. That's a good reminder that the extraction step needs to be repeatable, not something you do once during a single audit.

✅ The real objective is not maintaining a perfect list.

✅ The objective is maintaining a repeatable process that continuously converts Microsoft's published compliance scope into enforceable controls.

Updated Sep 01, 2026
Version 1.0