DevTools Hub

Search tools

Search for a developer tool

Enforcing Least Privilege at Scale

Part of the AWS Toolkit

Least Privilege Explained covers narrowing one policy — a wildcarded Action and Resource becoming the exact permissions a workload actually uses. That works when one person is narrowing one policy they understand well. It breaks down the moment permissions management is delegated to other people, or a hundred teams are creating their own roles — hand-narrowing doesn't scale, and nobody's checking every policy anyone writes. The tools for that layer are structural: they cap what's possible rather than trusting everyone to write a narrow policy correctly.

Permissions boundaries: let people create roles without trusting their judgment

A permissions boundary is a managed policy that sets the maximum permissions an identity-based policy can grant a user or role — it never grants anything by itself, only caps. Per AWS's own documentation, the flagship use case isn't capping one person's own permissions — it's letting a less-trusted principal create IAM roles and users on your behalf without being able to grant those new identities more than you're comfortable with, no matter what permissions policy they attach.

The mechanism is a condition key, not magic:

{
  "Effect": "Allow",
  "Action": ["iam:CreateRole", "iam:PutRolePermissionsBoundary"],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/TeamServiceBoundary"
    }
  }
}

This grants iam:CreateRole only when the role being created has TeamServiceBoundary attached as its permissions boundary — creating a role with no boundary, or a different one, fails. Whatever permissions policy someone then attaches to that role, the boundary is the hard ceiling on top of it. A team lead can be trusted to create roles for their own services without being trusted to accidentally (or deliberately) create one with account-wide access.

One real limit worth knowing before treating this as airtight: a permissions boundary only constrains what the identity-based policy grants. AWS's own worked example of this pattern notes that a delegate who can attach any permissions policy to a newly-created role can still grant that role access to a service the boundary allows but the delegate's own policy never mentioned — the boundary caps the ceiling, it doesn't audit every combination someone might assemble underneath it.

SCPs: the same ceiling, for an entire account or OU

A service control policy applies the identical intersection logic — never a grant, always a ceiling — at the AWS Organizations level, capping every identity in an account or organizational unit at once regardless of what any individual identity-based policy says. There are two ways to use one, and they trade off differently:

  • Deny list — leave the default FullAWSAccess SCP attached, and add a second SCP with explicit Deny statements for what you want blocked (a Region restriction, a specific dangerous action). Low effort, but anything not explicitly listed stays allowed.
  • Allow list — detach FullAWSAccess and replace it with an SCP that only allows a specific set of services and actions. Genuinely enforces least privilege at the account level, at the cost of maintaining that list as legitimate needs grow.

The sequencing matters: every root, OU, and account needs at least one SCP attached at all times, so detaching FullAWSAccess before attaching its allow-list replacement locks every identity in that account out of everything, including the ability to fix the mistake through the console.

ABAC: scoping by tag instead of rewriting a policy per resource

Hand-narrowing a Resource to an exact ARN works until the hundredth resource needs the same treatment and nobody wants to edit a policy every time one gets created. Attribute-based access control sidesteps that by scoping access through tags instead of fixed ARNs:

"Condition": {
  "StringEquals": {
    "aws:ResourceTag/team": "${aws:PrincipalTag/team}"
  }
}

One statement, written once, now correctly scopes access for every team as long as both identities and resources are tagged consistently — a new resource inherits the right access the moment it's tagged, with no policy edit at all. See IAM Policy Examples for this exact pattern with a full working statement. The tradeoff moves the enforcement problem rather than removing it: ABAC is only as trustworthy as your tagging discipline, and a resource created without the expected tag falls through to whatever the fallback permissions allow.

These layer, they don't replace each other

A realistic setup uses all three at once, each solving a different scale problem: an SCP caps what's possible anywhere in the account, a permissions boundary caps what a specific delegated identity — or anything it creates — can ever reach, and ABAC keeps individual policies from needing a rewrite every time a new resource shows up. None of them grant anything on their own; every one of them is a ceiling an identity-based policy still has to fit under. That's the same intersection rule from How AWS Evaluates Multiple IAM Policies showing up three times at three different scopes.

Try it yourself

IAM Policy Generator and IAM Policy Viewer work with any of the policy shapes above — a permissions boundary and an SCP are both just policy documents, structurally no different from an identity-based one. IAM Policy Simulator is scoped to identity-based policies only, which is exactly the boundary this post is about — it can't tell you what a permissions boundary or SCP would do to the result, only AWS's own evaluation (with real credentials and organization visibility) can. All three run entirely in your browser.

Related tools