Least Privilege in One Line of CDK (Not a JSON Policy)

CDK's grant methods, like table.grantReadData(), generate scoped least-privilege IAM policies for you instead of a hand-written wildcard PolicyStatement. Plus why an explicit deny in an SCP or permission boundary beats even AdministratorAccess.

chaotictoejamSeptember 17, 2026

The third video in my AWS micro-learnings series is "What Is IAM, Really." It uses the bouncer-with-a-clipboard analogy: IAM decides who gets a key and which rooms it opens, and the biggest mistake is handing out a master key because it's faster. This is all true. But it doesn't show what least privilege actually looks like once you're past the analogy.

So, what does least privilege look like when you're not hand-writing IAM JSON?

In CDK you almost never need to write a policy statement by hand:

// The wildcard version (don't do this)
myFunction.addToRolePolicy(new PolicyStatement({
  actions: ["dynamodb:*"],
  resources: ["*"],
}));

// The least-privilege version
myTable.grantReadData(myFunction);

That second line generates a scoped policy for you, the exact actions DynamoDB read requires, scoped to that table's ARN and nothing else. Most services have these grant* methods.

For example:

  • S3 bucket.grantRead()
  • SQS queue.grantSendMessages()
  • DynamoDB table.grantWriteData()

If you're writing actions: ["*"] in 2026, there's almost always a grant method that does it correctly in one line instead.

Here's the gotcha the video didn't have room for. IAM evaluates an explicit deny before anything else, no matter how permissive the allow is.

IAM policy evaluation order flowchart

A service control policy or a permission boundary with an explicit deny beats a role policy that grants full access, every time. So "the role has AdministratorAccess and it's still failing" is almost never a role problem. It's usually a deny sitting somewhere upstream, an SCP or a permission boundary you haven't checked yet.