Multi-Cloud

Multi-Cloud Policy Drift: How It Happens and What to Do About It

Amit Megiddo
Multi-cloud policy drift concept art

Policy drift in a single cloud is a known problem. You write an SCP, attach it to an OU, and then three months later someone modifies the OU structure to route a new workload account through a different hierarchy. The SCP no longer covers what it was supposed to cover. That's drift, and it has a discoverable fix.

Multi-cloud policy drift is harder because it has no single canonical state to drift from. When the same workload runs in AWS and Azure, "consistent policy enforcement" isn't a fact about either cloud individually — it's a relationship between two separate control planes that have completely different policy models. The two systems will never naturally agree. You have to make them agree.

Why the Two Systems Diverge by Default

AWS SCPs and Azure Policy have fundamentally different data models. An SCP is a JSON document that attaches to an AWS Organizations node (root, OU, or account) and evaluates conditions against IAM API calls. An Azure Policy definition is a JSON schema that attaches to a management group, subscription, or resource group and evaluates against ARM resource properties on create/update events.

The same security intent expressed in both systems looks nothing alike. "Deny creation of storage resources with public access enabled" in AWS is:

{
  "Effect": "Deny",
  "Action": [
    "s3:PutBucketAcl",
    "s3:PutBucketPublicAccessBlock"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "s3:x-amz-acl": [
        "public-read",
        "public-read-write",
        "authenticated-read"
      ]
    }
  }
}

The equivalent Azure Policy definition targets Microsoft.Storage/storageAccounts with an auditIfNotExists or deny effect on the allowBlobPublicAccess property:

{
  "mode": "All",
  "policyRule": {
    "if": {
      "allOf": [
        {
          "field": "type",
          "equals": "Microsoft.Storage/storageAccounts"
        },
        {
          "field": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
          "equals": "true"
        }
      ]
    },
    "then": {
      "effect": "Deny"
    }
  }
}

These are the same intent. But they have different trigger conditions (IAM API call vs ARM resource property), different attachment points (OU vs management group), different evaluation timing (before the API call vs before the resource is persisted), and different exception mechanisms. If a team manages them separately — which is almost always how they start — they drift the moment someone patches one without patching the other.

The Four Drift Vectors

In practice, multi-cloud policy drift enters through four vectors:

1. Uncoordinated exception management. Your AWS SCP has a condition that exempts a specific service account ARN for a data pipeline. The Azure Policy was written before that pipeline existed, so it doesn't have the equivalent exemption. Now the pipeline works in AWS and breaks in Azure — or worse, a copy of the exemption is added to Azure without the same scope restrictions, creating a broader gap there.

2. New cloud service coverage gaps. AWS releases a new storage service. Your team writes an SCP to control it. No one checks whether Azure has an equivalent service that the existing Azure Policy covers. Or vice versa. Over time, the coverage footprint of each cloud's policies grows out of sync with the other.

3. OU/management group restructuring. Your AWS org restructures to add a new OU for a new team. The SCP that was attached to the parent OU now doesn't reach the new OU's accounts — or it does reach them in ways that aren't intended. The Azure management group structure was restructured separately, by a different team, on a different timeline. The hierarchical inheritance in both clouds now differs from the original design.

4. Effect mode drift. Azure Policy supports multiple effect types: Deny, Audit, AuditIfNotExists, DeployIfNotExists. An enforcement policy set to Audit mode records violations but doesn't block them. It's easy for a policy to sit in Audit mode because someone set it there during testing and forgot to change it. AWS SCPs are always either Allow or Deny — there's no "audit only" mode at the SCP level. So a team that equates an Azure Audit-mode policy with an AWS SCP Deny is comparing a detection control with a preventive control. That's not a drift issue, it's a design issue — but it's often discovered during a drift audit.

The Detection Problem

Detecting multi-cloud policy drift requires a normalized view across both control planes. Neither cloud provides this natively. AWS Organizations shows you your SCP structure. Azure Policy Compliance shows you your Azure assignment state. Neither shows you the gap between them.

The practical approach we use is a policy intent inventory — a structured record of what each security requirement is supposed to look like in each cloud, maintained separately from the cloud provider's native policy store. Then we compare the cloud's current state against the intent record.

For AWS, the comparison uses the Organizations API:

aws organizations list-policies-for-target \
  --target-id ou-xxxx-xxxxxxxx \
  --filter SERVICE_CONTROL_POLICY \
  --query 'Policies[*].{Id:Id,Name:Name}' \
  --output json

For Azure, the Policy Assignments REST API or CLI:

az policy assignment list \
  --scope "/providers/Microsoft.Management/managementGroups/prod-mg" \
  --query '[*].{Name:name,PolicyDefinitionId:policyDefinitionId,EnforcementMode:enforcementMode}' \
  --output table

The structured comparison asks: for every security intent in our inventory, does the AWS SCP exist, is it attached to the correct OUs, and is its condition equivalent in scope to the Azure Policy assigned to the corresponding management group? When the answer is no, that's drift.

Making the Two Systems Agree

We're not suggesting you try to abstract both cloud policy systems into a single unified language — that approach tends to produce a least-common-denominator policy model that works poorly in both clouds and loses the nuance of each provider's native mechanisms. AWS SCPs have condition operators that have no Azure equivalent, and Azure Policy's resource field selectors are far more expressive than SCP conditions for resource property checks.

What we are suggesting: maintain a canonical policy intent library, and translate each intent to the appropriate native mechanism per cloud. The library records the intent ("no public object storage"), the AWS implementation (which SCP, which OU), the Azure implementation (which Policy definition, which management group assignment), and the last-verified date for each.

Drift detection then becomes a structured diff between the library and the live cloud state, rather than a subjective comparison of JSON documents.

For a team running a multi-cloud deployment — say, a web application with AWS as primary and Azure as DR — the minimum viable policy intent library covers about 12-15 intents: public access controls, region restrictions, IAM/identity privilege escalation prevention, encryption at rest requirements, network egress controls, and logging/monitoring requirements. These are the controls that most commonly drift and that auditors most frequently examine.

Fixing Drift When You Find It

When you find a drift condition, the fix is one of three things:

The AWS SCP is correct and the Azure Policy needs updating. This is the most common case after a change cycle that only touched one cloud. Write the equivalent Azure Policy definition, test it in a non-production management group, then assign it to the production scope.

The Azure Policy is correct and the AWS SCP needs updating. Less common, but happens when the security requirements evolve through the Azure team's work. Extract the Azure Policy's intent, translate to an SCP condition block, test in a sandbox account, then update the production SCP.

Both implementations are correct but cover different scopes. An OU restructure or management group restructure changed the effective coverage without changing the policy content. This requires an OU/management group hierarchy audit to determine the intended coverage, then re-attaching policies to the correct nodes.

The one thing to avoid: making both systems match by loosening the stricter one. If AWS is denying something and Azure isn't, the answer is to fix Azure, not to relax AWS. Policy drift remediation should always resolve to the more restrictive state unless there's an explicit business reason to accept a different posture.

GCP Adds a Third Control Plane

If your workload also runs on GCP, Organization Policy constraints are the third leg. GCP's model is more similar to Azure than to AWS — constraints apply to resource properties at create/update time and attach to the org resource hierarchy (organization, folders, projects). The GCP Org Policy API and gcloud resource-manager org-policies list give you the current attachment state.

The same intent library approach works for three clouds. The complexity is linear, not exponential — for each additional cloud, you add one more translation column to the intent library and one more diff step to your detection routine. The intent itself doesn't change.

What does get harder with three clouds is exception management. An exception carved out in AWS for a specific service account, if it's not tracked in the intent library, can be missed when creating the equivalent Azure and GCP exceptions. Tracking exceptions as first-class items in the intent library — with the same cloud-specific translation requirement as the base controls — is the discipline that keeps three-cloud policy state manageable.

Multi-cloud policy drift isn't an edge case or a sophisticated attack scenario. It's what happens when two separate cloud accounts grow under separate teams on separate timelines. The fix isn't a tool — it's a practice of treating security intent as the source of truth and treating each cloud's native policy system as a translation target.

More from the blog

All articles