Identity is the new perimeter. Get IAM wrong and you’ll be cleaning up breaches. Each cloud has a different model — AWS uses policies attached to principals, GCP binds roles to resources, and Azure uses role assignments with scope. Let’s untangle them.

IAM Model Comparison

ConceptAWSGCPAzure
User IdentityIAM UserGoogle AccountAzure AD User
Service IdentityIAM RoleService AccountManaged Identity
Permission SetIAM PolicyIAM RoleRole Definition
AssignmentPolicy AttachmentIAM BindingRole Assignment
HierarchyAccount → ResourceOrg → Folder → ProjectManagement Group → Subscription → RG
InheritanceNoYes (downward)Yes (downward)

AWS IAM

IAM Policies

# Policy document
data "aws_iam_policy_document" "s3_access" {
  statement {
    sid    = "ReadBucket"
    effect = "Allow"
    actions = [
      "s3:GetObject",
      "s3:ListBucket"
    ]
    resources = [
      aws_s3_bucket.data.arn,
      "${aws_s3_bucket.data.arn}/*"
    ]
  }

  statement {
    sid    = "WriteLogs"
    effect = "Allow"
    actions = [
      "s3:PutObject"
    ]
    resources = [
      "${aws_s3_bucket.logs.arn}/app-logs/*"
    ]
    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
  }

  statement {
    sid    = "DenyDelete"
    effect = "Deny"
    actions = [
      "s3:DeleteObject",
      "s3:DeleteBucket"
    ]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "s3_access" {
  name        = "s3-access-policy"
  description = "S3 access for application"
  policy      = data.aws_iam_policy_document.s3_access.json
}

IAM Roles for Services

# Role for Lambda
resource "aws_iam_role" "lambda" {
  name = "lambda-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy_attachment" "lambda_s3" {
  role       = aws_iam_role.lambda.name
  policy_arn = aws_iam_policy.s3_access.arn
}

# Role for EKS service account (IRSA)
resource "aws_iam_role" "eks_pod" {
  name = "eks-pod-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRoleWithWebIdentity"
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.eks.arn
      }
      Condition = {
        StringEquals = {
          "${replace(aws_iam_openid_connect_provider.eks.url, "https://", "")}:sub" = "system:serviceaccount:default:my-app"
        }
      }
    }]
  })
}

Cross-Account Access

# In account A: Role that account B can assume
resource "aws_iam_role" "cross_account" {
  name = "cross-account-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        AWS = "arn:aws:iam::${var.account_b_id}:root"
      }
      Condition = {
        StringEquals = {
          "sts:ExternalId" = var.external_id
        }
      }
    }]
  })
}

# In account B: Policy to assume role
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    resources = [
      "arn:aws:iam::${var.account_a_id}:role/cross-account-role"
    ]
  }
}

Permission Boundaries

# Boundary that limits maximum permissions
resource "aws_iam_policy" "boundary" {
  name = "developer-boundary"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:*",
          "dynamodb:*",
          "lambda:*",
          "logs:*",
          "cloudwatch:*"
        ]
        Resource = "*"
      },
      {
        Effect = "Deny"
        Action = [
          "iam:*",
          "organizations:*",
          "ec2:*NetworkAcl*",
          "ec2:*SecurityGroup*"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_role" "developer" {
  name                 = "developer-role"
  permissions_boundary = aws_iam_policy.boundary.arn
  # ... assume_role_policy
}

GCP IAM

IAM Bindings

# Project-level binding
resource "google_project_iam_member" "storage_viewer" {
  project = var.project_id
  role    = "roles/storage.objectViewer"
  member  = "serviceAccount:${google_service_account.app.email}"
}

# Resource-level binding (more granular)
resource "google_storage_bucket_iam_member" "data_access" {
  bucket = google_storage_bucket.data.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.app.email}"
}

# Conditional IAM binding
resource "google_project_iam_member" "conditional" {
  project = var.project_id
  role    = "roles/storage.objectAdmin"
  member  = "serviceAccount:${google_service_account.app.email}"

  condition {
    title       = "expires_after_2025"
    description = "Expires at end of 2025"
    expression  = "request.time < timestamp('2026-01-01T00:00:00Z')"
  }
}

# Multiple bindings with google_project_iam_binding
resource "google_project_iam_binding" "editors" {
  project = var.project_id
  role    = "roles/editor"

  members = [
    "user:[email protected]",
    "serviceAccount:${google_service_account.cicd.email}",
  ]
}

Service Accounts

resource "google_service_account" "app" {
  account_id   = "my-app"
  display_name = "My Application Service Account"
  project      = var.project_id
}

# Key (avoid if possible - use Workload Identity instead)
resource "google_service_account_key" "app" {
  service_account_id = google_service_account.app.name
}

# Allow GKE to use service account (Workload Identity)
resource "google_service_account_iam_member" "workload_identity" {
  service_account_id = google_service_account.app.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${var.project_id}.svc.id.goog[default/my-app]"
}

Custom Roles

resource "google_project_iam_custom_role" "custom" {
  role_id     = "customStorageReader"
  title       = "Custom Storage Reader"
  description = "Read-only access to specific buckets"
  permissions = [
    "storage.buckets.get",
    "storage.buckets.list",
    "storage.objects.get",
    "storage.objects.list",
  ]
  project = var.project_id
}

Organization Policy Constraints

resource "google_organization_policy" "disable_sa_keys" {
  org_id     = var.org_id
  constraint = "iam.disableServiceAccountKeyCreation"

  boolean_policy {
    enforced = true
  }
}

resource "google_organization_policy" "allowed_locations" {
  org_id     = var.org_id
  constraint = "gcp.resourceLocations"

  list_policy {
    allow {
      values = ["us-locations", "eu-locations"]
    }
  }
}

Azure IAM

Role Assignments

# Built-in role assignment
resource "azurerm_role_assignment" "contributor" {
  scope                = azurerm_resource_group.main.id
  role_definition_name = "Contributor"
  principal_id         = azuread_service_principal.app.object_id
}

# At subscription level
resource "azurerm_role_assignment" "reader" {
  scope                = data.azurerm_subscription.current.id
  role_definition_name = "Reader"
  principal_id         = azuread_group.developers.object_id
}

# At resource level
resource "azurerm_role_assignment" "storage" {
  scope                = azurerm_storage_account.main.id
  role_definition_name = "Storage Blob Data Contributor"
  principal_id         = azurerm_user_assigned_identity.app.principal_id
}

Managed Identities

# System-assigned identity (tied to resource lifecycle)
resource "azurerm_linux_virtual_machine" "app" {
  name = "app-vm"
  # ...

  identity {
    type = "SystemAssigned"
  }
}

# User-assigned identity (independent lifecycle)
resource "azurerm_user_assigned_identity" "app" {
  name                = "app-identity"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
}

# Attach to resource
resource "azurerm_linux_virtual_machine" "app" {
  name = "app-vm"
  # ...

  identity {
    type         = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.app.id]
  }
}

Custom Role Definitions

resource "azurerm_role_definition" "custom" {
  name        = "Custom VM Operator"
  scope       = data.azurerm_subscription.current.id
  description = "Start/stop VMs without full contributor"

  permissions {
    actions = [
      "Microsoft.Compute/virtualMachines/start/action",
      "Microsoft.Compute/virtualMachines/powerOff/action",
      "Microsoft.Compute/virtualMachines/restart/action",
      "Microsoft.Compute/virtualMachines/read",
    ]
    not_actions = []
  }

  assignable_scopes = [
    data.azurerm_subscription.current.id,
  ]
}

Azure AD Groups

resource "azuread_group" "developers" {
  display_name     = "Developers"
  security_enabled = true
}

resource "azuread_group_member" "developer" {
  group_object_id  = azuread_group.developers.id
  member_object_id = azuread_user.developer.object_id
}

# Assign role to group
resource "azurerm_role_assignment" "developers" {
  scope                = azurerm_resource_group.dev.id
  role_definition_name = "Contributor"
  principal_id         = azuread_group.developers.object_id
}

Cross-Cloud Identity Federation

AWS to GCP (Workload Identity Federation)

# In GCP: Create workload identity pool
resource "google_iam_workload_identity_pool" "aws" {
  workload_identity_pool_id = "aws-pool"
  display_name              = "AWS Pool"
  project                   = var.project_id
}

resource "google_iam_workload_identity_pool_provider" "aws" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.aws.workload_identity_pool_id
  workload_identity_pool_provider_id = "aws-provider"
  display_name                       = "AWS Provider"
  project                            = var.project_id

  aws {
    account_id = var.aws_account_id
  }
}

# Grant access to GCP service account
resource "google_service_account_iam_member" "aws_access" {
  service_account_id = google_service_account.app.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.aws.name}/attribute.aws_role/arn:aws:sts::${var.aws_account_id}:assumed-role/${var.aws_role_name}"
}

GCP to AWS (OIDC Federation)

# In AWS: Create OIDC provider
resource "aws_iam_openid_connect_provider" "gcp" {
  url = "https://accounts.google.com"

  client_id_list = [
    google_service_account.app.unique_id
  ]

  thumbprint_list = [var.google_oidc_thumbprint]
}

# Role that GCP can assume
resource "aws_iam_role" "gcp_access" {
  name = "gcp-access-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRoleWithWebIdentity"
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.gcp.arn
      }
      Condition = {
        StringEquals = {
          "accounts.google.com:sub" = google_service_account.app.unique_id
        }
      }
    }]
  })
}

Best Practices Checklist

# IAM security checklist
general:
  - Use groups/roles, not individual user permissions
  - Implement least privilege from day one
  - Enable MFA for all human users
  - Use service accounts/managed identities for machines
  - Rotate credentials regularly
  - Log all IAM changes

aws_specific:
  - Use permission boundaries for delegated admin
  - Enable AWS CloudTrail for IAM actions
  - Use AWS Organizations SCPs for guardrails
  - Prefer IRSA over node roles for EKS

gcp_specific:
  - Use Workload Identity instead of SA keys
  - Apply organization policies for constraints
  - Use conditional IAM bindings for time-based access
  - Grant roles at lowest scope possible

azure_specific:
  - Use managed identities over service principals
  - Implement Privileged Identity Management (PIM)
  - Use Azure AD groups for role assignments
  - Enable Azure AD Identity Protection

Key Takeaways

  1. GCP IAM is hierarchical — permissions inherit down from org to folder to project
  2. AWS uses explicit policy attachment — no inheritance, more control
  3. Azure centers on AD — groups and PIM are essential
  4. Avoid long-lived credentials — use federation and managed identities
  5. Audit everything — CloudTrail, Audit Logs, Activity Logs are non-negotiable

“The principle of least privilege isn’t just security theater — it’s the difference between a contained incident and a complete breach.”