Introduction: The Convenience Trap

Many organizations adopt a “set‑and‑forget” approach to identity and access management (IAM) when they span Amazon Web Services (AWS), Microsoft Azure, and Google Cloud Platform (GCP). Cloud consoles automatically propagate newly created roles or service‑account bindings across projects, subscriptions, or accounts that share a common organization unit. While this feature saves time, it also creates a silent attack surface: over‑privileged identities drift into workloads that never needed them. This article explains why that silent threat matters, walks through the internal mechanics of automatic propagation, and provides a step‑by‑step tutorial for detecting and hardening the problem using Terraform, the Open Policy Agent (OPA), and native cloud audit logs.

How Automatic Propagation Works Under the Hood

Each cloud provider offers a “resource inheritance” model:

  • AWS Organizations: When you attach an IAM policy to an OU, the policy is automatically evaluated for every account underneath that OU.
  • Azure Management Groups: Role assignments made at the management‑group level flow down to every subscription and resource group beneath it.
  • GCP Resource Hierarchy: IAM bindings on a folder or organization automatically apply to all projects under that node.

The propagation engine runs asynchronously, updating the effective permission set for each principal. The engine does not record a “just‑in‑time” justification for each inherited permission, making it difficult for security teams to audit why a given service account can perform a privileged action in a downstream workload.

# Example: AWS IAM policy inheritance (simplified JSON)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetObject"
      ],
      "Resource": "*"
    }
  ]
}

In the example above, attaching the policy to the root OU instantly grants every account in the organization read access to every S3 bucket—even those that store highly sensitive logs. The same pattern repeats across Azure and GCP.

Why the Silent Threat Is Dangerous

Privilege creep is the most obvious symptom: a role that started with a single purpose expands to cover dozens of services. Over time, an attacker who compromises a low‑privilege workload can pivot laterally by exploiting an inherited permission that was never intended for that workload. Moreover, because the inheritance is implicit, most cloud‑native scanning tools flag only the explicit bindings, missing the inherited ones entirely.

Compliance blind spots are another issue. Regulations such as PCI‑DSS or HIPAA require explicit justification for each access to regulated data. Inherited permissions lack that justification, potentially causing audit failures.

Step‑by‑Step Tutorial: Detecting Inherited Permissions

The following sections show how to enumerate inherited IAM bindings, compare them against a baseline of allowed permissions, and enforce a deny‑list using OPA. The tutorial assumes you have a Terraform workspace that provisions resources across all three clouds.

1. Export Current IAM Bindings

Use the respective CLIs to dump the effective policy for each principal. The scripts below aggregate the data into a unified JSON file merged_iam.json.

# AWS: list all attached policies for every account
for acct in $(aws organizations list-accounts --query 'Accounts[].Id' --output text); do
  aws iam list-attached-user-policies --user-name root --profile $acct \
    --query 'AttachedPolicies[].PolicyArn' --output json >> aws_policies_$acct.json
done

# Azure: export role assignments at the management‑group level
az role assignment list --scope "/providers/Microsoft.Management/managementGroups/RootGroup" \
  --output json > azure_assignments.json

# GCP: export IAM policy for the organization
gcloud organizations get-iam-policy $ORG_ID --format=json > gcp_iam.json

# Merge into a single file (requires jq)
jq -s 'reduce .[] as $item ({}; . * $item)' aws_*.json azure_assignments.json gcp_iam.json > merged_iam.json

The merged_iam.json file now contains every explicit and inherited binding across the three clouds.

2. Define a Baseline of Allowed Permissions

Store the baseline as a JSON document allowed_permissions.json. Only list actions that are required for each workload type.

{
  "aws": {
    "s3:ListBucket": ["log-collector"],
    "dynamodb:Query": ["session-store"]
  },
  "azure": {
    "Microsoft.Storage/storageAccounts/listKeys/action": ["storage‑monitor"]
  },
  "gcp": {
    "storage.buckets.get": ["backup‑service"]
  }
}

Notice the mapping from action to the specific service that needs it. Anything not on this list should trigger an alert.

3. Write an OPA Policy to Detect Violations

Save the following policy as iam_violation.rego. The policy loads the merged IAM data and the allowed list, then flags any binding that does not have a matching justification.

# iam_violation.rego
package iam.violation

import data.allowed_permissions
import data.merged_iam

# Helper to extract cloud, action, and principal
get_binding[cloud, action, principal] {
  cloud := "aws"
  binding := merged_iam.aws[_]
  action := binding.PolicyArn
  principal := binding.Principal
}
get_binding[cloud, action, principal] {
  cloud := "azure"
  binding := merged_iam.azure[_]
  action := binding.roleDefinitionId
  principal := binding.principalId
}
get_binding[cloud, action, principal] {
  cloud := "gcp"
  binding := merged_iam.gcp[_]
  action := binding.role
  principal := binding.member
}

# Violation rule
violation[{
  "cloud": cloud,
  "principal": principal,
  "action": action,
  "reason": "No explicit allowance in baseline"
}] {
  get_binding[cloud, action, principal]
  not allowed_permissions[cloud][action][_]
}

Run the policy with OPA’s command‑line interpreter:

# Evaluate violations
opa eval -i merged_iam.json -d iam_violation.rego "data.iam.violation"

The output lists every inherited permission that is not covered by the baseline, giving you a precise inventory of silent threats.

4. Automate Remediation with Terraform

For each violation, you can generate a Terraform “deny” rule that revokes the over‑privileged binding. Below is a Terraform module that accepts a list of principals and actions to remove.

# modules/iam_revoker/main.tf
variable "cloud" { type = string }
variable "principal" { type = string }
variable "action" { type = string }

resource "aws_iam_policy_attachment" "revoke" {
  count      = var.cloud == "aws" ? 1 : 0
  name       = "revoke-${var.principal}-${replace(var.action, \"/\", \"-\")}"
  policy_arn = var.action
  users      = [var.principal]
  lifecycle {
    prevent_destroy = false
  }
}

resource "azurerm_role_assignment" "revoke" {
  count                = var.cloud == "azure" ? 1 : 0
  scope                = "/subscriptions/${var.subscription_id}"
  role_definition_name = var.action
  principal_id         = var.principal
  lifecycle {
    prevent_destroy = false
  }
}

resource "google_organization_iam_binding" "revoke" {
  count   = var.cloud == "gcp" ? 1 : 0
  org_id  = var.org_id
  role    = var.action
  members = [var.principal]
}

Hook the OPA output into a simple Bash wrapper that calls the module for each violation:

# revoke_violations.sh
#!/usr/bin/env bash
set -euo pipefail

violations=$(opa eval -i merged_iam.json -d iam_violation.rego "data.iam.violation" -f json | jq -c '.result[0].expressions[0].value[]')

for v in $violations; do
  cloud=$(echo $v | jq -r '.cloud')
  principal=$(echo $v | jq -r '.principal')
  action=$(echo $v | jq -r '.action')
  terraform -chdir=modules/iam_revoker apply \
    -var="cloud=$cloud" \
    -var="principal=$principal" \
    -var="action=$action" \
    -auto-approve
done

Schedule this script with a cron job (e.g., nightly) to keep inherited permissions in check.

Security and Best Practices

Never rely solely on default inheritance. Explicitly attach policies only where they are needed. Use management‑group or organization‑level bindings only for truly global, read‑only actions such as billing.

Enable CloudTrail, Azure Activity Log, and GCP Cloud Audit Logs. Forward those logs to a SIEM where you can correlate permission changes with deployment events.

Version‑control IAM definitions. Store the allowed_permissions.json file in a Git repository, review changes via pull‑requests, and treat them as code.

"Inheritance is a convenience