Quick Answer

Quick Answer: Least privilege means every identity — a person or a service — gets only the exact permissions it needs to do its job, nothing extra 'just in case.' Most real IAM-related breaches aren't caused by a stolen root password; they're caused by an ordinary, over-permissioned role that an attacker found and rode all the way to the data.

What IAM Is Actually Deciding

Every IAM decision answers one question: can this identity (a user, a role, a service) perform this action (like s3:GetObject) on this resource (a specific bucket, a specific table), under these conditions (from this IP range, with MFA, before this date)?

That decision isn't made by one policy in isolation. AWS evaluates every policy that could apply — identity-based policies attached to the user or role, resource-based policies attached to the thing being accessed, permission boundaries that cap what a role can ever be granted, and organization-level service control policies — and combines them according to a documented set of rules. Least privilege is the discipline of making sure the combined result of all of that is as narrow as it can possibly be while still letting the real job get done.

Why Teams End Up Over-Permissioned By Default

Nobody sets out to grant too much access. It happens through a series of individually reasonable shortcuts:

  • A deadline is close, and attaching a broad AWS-managed policy like PowerUserAccess gets something working in two minutes instead of twenty.
  • A tutorial or Stack Overflow answer includes a wildcard action ("Action": "s3:*") because the author didn't want to explain exactly which of the dozens of S3 actions you need, so you copy the whole thing.
  • The console itself nudges you toward convenience — picking a managed policy from a dropdown is faster than writing a scoped custom policy by hand.
  • Once the thing works, nobody circles back to tighten it. Narrowing permissions after the fact requires figuring out exactly what's actually used, which is real investigative work that rarely gets prioritized once there's no visible problem.

The common end state: a Lambda function that was only ever supposed to read one configuration file ends up with a role that can read, write, and delete every object in every bucket in the account — because that's what the example it was built from had, and nobody ever revisited it.

A Real Before/After: Rewriting an Over-Permissioned Role

Here's a realistic scenario: a Lambda function reads a single config file from one S3 bucket and writes its own logs. This is the kind of policy that commonly gets attached to make it work fast:

// BEFORE — over-permissioned (works, but far too broad)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "logs:*",
      "Resource": "*"
    }
  ]
}

This role can read, overwrite, or delete every object in every bucket in the entire account, and create or destroy any CloudWatch log group. The Lambda function never does any of that — it just needed to read one file and write its own logs. Here's the least-privilege rewrite:

// AFTER — least privilege (scoped to exactly what's needed)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-app-config-bucket/settings.json"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:ap-south-1:123456789012:log-group:/aws/lambda/my-config-reader:*"
    }
  ]
}

Line by line, what changed: s3:* became the single action actually used, s3:GetObject (not even ListBucket, since this function never lists anything). The wildcard resource * became the exact ARN of the exact object, so even a bug in the function's own code physically cannot reach any other bucket or key, no matter what the code tries to do. The logging permissions dropped logs:* (which includes deleting log groups) down to just the two actions a Lambda actually needs to write logs, scoped to its own specific log group ARN rather than every log group in the account.

If this function's credentials ever leaked — through a misconfigured environment variable, a compromised dependency, or a logging mistake that printed a token — the blast radius under the "after" policy is exactly one config file and one log stream. Under the "before" policy, it's the entire account's S3 storage.

How AWS Actually Evaluates These Policies

AWS's documented policy evaluation logic follows a specific, fixed order that's worth knowing exactly:

  • Everything is denied by default. This is called an implicit deny — unless something explicitly allows a request, it's rejected.
  • An explicit Deny anywhere always wins. AWS's own documentation states it plainly: if the enforcement logic finds even one explicit deny statement that applies, the final decision is deny — no matter how many Allow statements exist in other policies attached to the same identity or resource.
  • Only an explicit Allow grants access — and it must come from a policy that actually applies to the request (identity-based, resource-based, or both).

This has a direct, practical use for least privilege beyond just writing narrow Allow statements: you can add a deliberate explicit Deny as a guardrail that nothing else in the account can override — for example, denying s3:DeleteBucket on a specific production bucket's ARN for every role except one tightly controlled break-glass identity. Even if someone later attaches an overly broad policy by mistake, that explicit deny still wins.

Tools That Make Least Privilege Practical, Not Just Aspirational

Writing scoped policies by hand from scratch for every role doesn't scale, which is why AWS built tooling specifically for this:

  • IAM Access Analyzer's policy generation feature reads a role or user's actual CloudTrail activity history and generates a policy containing only the permissions that were genuinely used — turning "what does this actually need?" from a manual audit into a generated report.
  • Permission boundaries set a hard ceiling on what a role can ever be granted, even if someone attaches a broader policy to it later. This is especially useful for roles that other teams or automated pipelines might modify — the boundary guarantees privilege can't silently creep upward no matter what gets attached.
  • The "start from zero" habit. The most reliable (if less automated) practice: start every new role with no permissions at all, and add exactly the action that the first real permission error tells you is missing. This naturally produces a minimal policy, because you only ever add what a genuine failure proved was necessary — compare that to starting broad and hoping someone narrows it down later, which in practice almost never happens.

Why This Is About Blast Radius, Not Just Checkbox Compliance

It's easy to file least privilege under "security compliance paperwork." The more useful way to think about it: least privilege is entirely about limiting blast radius when something goes wrong — and something going wrong doesn't require a sophisticated attacker.

A dependency in your Lambda's package gets compromised in a supply-chain attack and starts quietly using whatever credentials are available in the execution environment. A debugging log statement accidentally prints an access token that ends up in a log aggregator with looser access controls than you think. A teammate's laptop is compromised and their session credentials get grabbed before anyone notices.

In every one of those scenarios, the question that actually determines the damage is: what could that specific role already do? If the answer is "read one config file," the incident is contained before it starts. If the answer is "full S3 access to the account" because nobody ever tightened the policy after the initial deadline passed, the same leaked credential becomes a very different kind of incident. Least privilege is the only thing standing between those two outcomes, and it's decided entirely by policy JSON written (or left unwritten) long before anything goes wrong.

If you're preparing for cloud or DevOps interviews, this exact before/after rewrite — explaining why each line changed, not just that it did — is a strong way to demonstrate real understanding rather than reciting the phrase "principle of least privilege" without being able to show it. More interview-prep content lives on the Priodemy blog.

Frequently Asked Questions

Isn't least privilege slower to develop with?

Initially, yes — scoping a policy down takes more thought than attaching a broad managed policy. It's an upfront cost paid once, versus a security debt that compounds (and gets paid with interest, during an actual incident) later.

What's the fastest way to find out what permissions a role actually uses?

IAM Access Analyzer's policy generation feature, which reads CloudTrail history for that specific principal and proposes a minimal policy based on what it genuinely called.

Can a more specific Allow ever override an explicit Deny?

No. Per AWS's documented evaluation logic, any applicable explicit Deny always wins over any Allow, regardless of specificity — there's no 'more specific rule wins' exception.

Is using AWS managed policies automatically a bad practice?

Not inherently — they're fine for prototyping and learning. The risk is leaving broad managed policies like PowerUserAccess attached to production roles indefinitely instead of replacing them with scoped custom policies once the real usage pattern is known.

What's a permissions boundary, and how is it different from a regular policy?

A permissions boundary sets the maximum permissions a role can ever have, even if a broader policy is attached to it later. It's a backstop against privilege creep, not a grant of access by itself — the role still needs an actual Allow within that boundary to do anything.