Skip to content

Least privilege and scoping

The sixth and final deep page in the trust arc. By now the workload has a passwordless identity, the user is verified at the edge, you've picked an attribution pattern, you've bolted the approval workflow onto sensitive writes, and you understand external-write provenance. The last question is the quiet one: how much can the workload identity actually do, and against which resources?

This page names the three traps that turn "I gave it the right role" into "I gave it the entire subscription." Every trap has a cross-cloud name and a fix; the sibling per-cloud scoping guide has the concrete IaC + CLI snippets.

The three traps

Trap 1 — Grant at too broad a scope

Every cloud lets you write a role assignment at multiple levels of a resource hierarchy. Grant at the top and it inherits everywhere below; grant at a leaf and the workload sees only that leaf. The default when copy-pasting from a starter is almost always the broadest scope, and role assignments are boring — nobody re-reads them a month later.

Cloud Broadest → narrowest scope levels
Azure Management group → Subscription → Resource group → Resource → Sub-resource (e.g., a single blob container inside a storage account)
GCP Organization → Folder → Project → Resource (e.g., a bucket, a secret, a topic)
AWS Account (via IAM policy on the identity) → Resource ARN patterns in the Resource element of the policy statement

The mapping isn't clean — AWS scopes via policy statements on the identity + Resource element ARNs, not via a per-resource role assignment at all — but the principle is the same: narrow the grant until you can't narrow it any more without breaking the actual workload.

Fix: always assign at the narrowest scope the workload will function at, and put a "why-here-not-lower" comment next to any assignment that isn't at the leaf.

Trap 2 — Data-plane vs control-plane confusion

Every major cloud has (at least) two separate authorization surfaces for a resource:

  • Control plane — the "manage the resource" surface. List it, describe it, delete it, resize it, read its config. On Azure this is Azure Resource Manager (ARM); on GCP the Resource Manager APIs; on AWS the service's control APIs.
  • Data plane — the "read/write the contents" surface. Read the blobs, run the queries, publish to the topic. On Azure this is the data-service endpoint (e.g., <account>.blob.core.windows.net); on GCP the resource-specific API (Storage, BigQuery, Pub/Sub); on AWS the resource-specific service API (S3, DynamoDB, Kinesis).

The trap: the two surfaces don't overlap. A role that grants everything on the control plane may grant zero on the data plane. And a third surface — credential extraction (get the account key / HMAC key / long-lived access key) — is orthogonal to both and needs a third distinct grant.

The Azure Storage instance of this trap is the clearest example anywhere:

Role Sees the storage account exists? Reads blob contents? Can list account keys?
Reader (control plane) ✅ ❌ ❌
Storage Blob Data Reader (data plane) ❌ (account resource is invisible via ARM) ✅ (via Entra ID) ❌
Storage Blob Data Contributor (data plane, RW) ❌ ✅ + write ❌
Storage Account Contributor (control plane, privileged) ✅ + manage ❌ directly, but grants listkeys — see next row ✅ (Microsoft.Storage/storageAccounts/listkeys/action)
Storage Account Contributor → shared-key auth — ✅ (via the extracted key, bypassing Entra ID entirely) —

The load-bearing consequence: someone with "Reader" cannot read blob data. Someone with "Storage Blob Data Reader" cannot escalate to extracting the account key. And someone with "Storage Account Contributor" holds the key that bypasses every Entra ID-based data-plane grant. Pick the role that matches the layer the workload actually needs, not the one that sounds closest.

The equivalent trap on GCP:

  • roles/storage.legacyBucketReader (control-plane list) does NOT grant roles/storage.objectViewer (data-plane object read); you need both to list and read objects.
  • Neither grants HMAC key access — that needs roles/storage.hmacKeyAdmin and is a separate surface.

The equivalent trap on AWS:

  • s3:ListBucket (control-plane list) does NOT imply s3:GetObject (data-plane read); the two need separate statements. And neither grants access to the account's root credentials — those are managed outside IAM policy entirely.

Fix: for every workload, name the plane + action it needs (control-plane read? data-plane read? credential extraction?) before picking a role. If you catch yourself picking a role because "it sounds like storage," you're about to fall into this trap.

Trap 3 — Role assignment propagation is asynchronous + you may not be allowed to assign

Two secondary traps that trip pipelines the first time:

Propagation is async. On all three clouds, a role assignment / policy binding update is not immediately effective globally. Azure docs are explicit that role assignment changes can take several minutes to propagate; GCP IAM policy propagation is typically fast but not guaranteed instant; AWS IAM policy propagation is also typically fast but explicitly "eventually consistent." Do not treat "the terraform apply completed" as "the workload has the permission." CI/CD that creates a role assignment and immediately invokes the workload will hit sporadic 403s.

Elevated rights are needed to create role assignments. On Azure, you need Microsoft.Authorization/roleAssignments/write — which is held by Owner, User Access Administrator, and the newer Role Based Access Control Administrator (least privileged of the three; can only assign roles, not do anything else). Contributor cannot assign roles. On GCP, resourcemanager.projects.setIamPolicy (held by roles/owner and roles/resourcemanager.projectIamAdmin). On AWS, iam:PutRolePolicy / iam:AttachRolePolicy and — critically — iam:PassRole to hand an assumed role to a service.

The load-bearing consequence: the pipeline identity that provisions your workload needs privileged rights to set up the workload's identity + permissions, even if the workload itself needs almost none. This is a real trust boundary: the CI/CD service principal is the most privileged identity in most Azure subscriptions, and you probably haven't thought about it that way.

Fix: grant roles/iam.securityAdmin (or Azure's RBAC Administrator, or a narrow custom AWS policy limited to specific PassRole targets) to the CI/CD identity — never the broader Owner / Editor / AdministratorAccess. And bake a short delay + a retry into any step that runs the workload immediately after a role change.

The full arc, in one paragraph

  • The workload has a passwordless identity (TA.1).
  • The user's identity survives to the workload (TA.2).
  • The database or the app enforces which user is doing what (TA.3).
  • Sensitive actions gate on human approval (TA.4).
  • External systems record who is really writing (TA.5).
  • And the workload's grant is narrow enough that an escape is a small blast radius (this page).

Together these are the trust posture. Missing any one leaves a specific class of question unanswerable in a post-incident review.

Docs verified 2026-08-09

Where the arc goes next

  • Per-cloud scoping guide — the concrete scope-narrowing snippets (Azure --scope, GCP condition binding, AWS resource ARN with condition) end-to-end, one recipe per cloud.
  • This closes the deep arc. Related surface material continues in attributed writes and revisits the arc's full posture together.