open atlas
↑ Back to track
Security Foundations SECF · 05 · 03

Least privilege in practice

Least privilege is the principle everyone cites and almost nobody holds. The decay is privilege creep — grants accumulate, nothing is ever revoked. The cure is operational: deny by default, just-in-time elevation, and a revocation loop.

SECF Middle ◷ 15 min
Level
FoundationsJuniorMiddleSenior

You pull the IAM policy attached to your team’s main service role to debug a permissions error, and there it is: s3:* on Resource: "*". Nobody decided the billing worker should be able to delete every bucket in the account — it just ended up that way. Eighteen months ago someone needed read access to one bucket, hit a deny, slapped a wildcard on it to unblock a release, and moved on. The grant outlived the task, the person, and the reason. That role is now a single leaked key away from wiping production storage, and the scariest part is that everything works perfectly. Least privilege didn’t fail loudly. It eroded, one expedient grant at a time.

By the end of this lesson you’ll know why least privilege decays into over-privilege, and the three operational moves — deny by default, just-in-time access, and a revocation loop — that actually hold the line.

The principle, and why stating it isn’t enough

The principle of least privilege says every subject — a user, a service, a process — should hold exactly the permissions it needs to do its job, and not one more. NIST calls it out as a foundation of access control; the OWASP Authorization Cheat Sheet opens with “enforce least privileges” and “deny by default.” Every engineer nods along. Almost no production system actually has it.

The reason is that least privilege is not a property you configure once — it’s a property you have to maintain against constant pressure. Permissions are sticky in one direction. Granting is easy: someone is blocked, you add an allow, the ticket closes, everyone is happy today. Revoking is hard: nobody is blocked by a permission you still have, so there is no pull to remove it, no failing test, no paged alert. The system that grants on demand and never revokes doesn’t drift toward least privilege — it drifts away from it, monotonically. That asymmetry is the whole problem, and it has a name.

Privilege creep: the slow leak

Privilege creep is the gradual accumulation of permissions a subject no longer needs. It comes from a handful of utterly mundane sources, none of which feels like a security decision at the time:

  • The role change that adds without subtracting. An engineer moves from the payments team to platform. They get the platform group’s access on day one. The payments access? It’s still there a year later — nobody owns the subtraction, and revoking it might break something, so it stays.
  • The expedient wildcard. A deny blocks a release at 4pm. The fix that ships is s3:* / Resource: *, because narrowing it to the three actions and one bucket actually needed takes twenty minutes you don’t have. The wildcard is permanent; the deadline was temporary.
  • The standing admin. Break-glass access for a 2am incident is never revoked the next morning. Now it’s just “the access I have,” and three months later it’s load-bearing for some routine task it was never meant for.
  • The copied role. New service needs permissions? Clone the role from the service next to it. Now it inherits every grant that service accreted, including the ones nobody can explain.

Each step is locally rational and globally corrosive. The endpoint is a fleet of identities that each hold far more power than their actual job requires — which means the blast radius of any single compromised credential is enormous, and totally invisible until the day it’s exploited.

Deny by default: making “no” the starting point

The structural fix for creep starts at the foundation: deny by default. Authorization begins from a refusal, and access is granted only by an explicit, positive rule that matches the subject, the action, and the resource. Anything not explicitly allowed is denied — including, critically, anything new.

This sounds obvious until you contrast it with the failure mode it prevents. An allow-by-default system (or any allowlist with a wildcard fallback) fails open: add a new endpoint, a new resource type, a new admin action, and it’s reachable unless someone remembered to write a deny rule for it. Deny by default fails closed: the new thing is unreachable until someone writes the allow. The difference is whether a forgotten rule leaks access or merely produces a 403 that someone files a ticket about. Fail-closed turns your omissions into inconveniences instead of breaches.

In practice this means: no Resource: "*" and no action: * in policies you can avoid them in; permissions scoped to specific actions on specific resources; and the default answer to “can this identity do this thing” is no unless a rule says yes. The cost is real — you write more, narrower rules, and you eat more 403s during development. That cost is the point. It’s the friction that keeps the wildcard out.

Why this works

Why not just audit permissions once a quarter and prune the excess? Because a periodic audit fights an asymmetry it can’t win. Grants accrue continuously — every day, driven by real deadlines — while pruning happens in a batch four times a year and is nobody’s favorite work. Worse, you can’t safely remove a permission you can’t prove is unused, and proving a negative (“nothing relies on this”) across a live system is genuinely hard, so audits err toward keeping things. The audit isn’t useless, but it’s a mop, not a faucet. The durable fix changes how grants are made — short-lived and self-expiring — so the excess never accumulates in the first place.

Just-in-time access: grants that expire on their own

The asymmetry that drives creep is “easy to grant, hard to revoke.” Just-in-time (JIT) access attacks it directly by making revocation automatic: instead of holding a standing permission forever, a subject requests elevated access for a specific task, gets it for a bounded window, and the system takes it back when the window closes — no human has to remember.

The pattern shows up at every layer:

  • Human admins: standing administrator membership is removed; an engineer requests elevation (often with a reason and an approval) and gets it for, say, 60 minutes, after which the role membership is automatically stripped. The default state of every human is not privileged.
  • Services: instead of a long-lived API key with broad scope baked into the deployment, the workload assumes a role and receives short-lived credentials (minutes to an hour) that auto-rotate and auto-expire. A leaked credential is only useful inside its tiny window.
  • Break-glass: emergency access is itself a JIT grant — explicitly requested, time-boxed, loudly logged, and auto-revoked — so the 2am incident doesn’t leave a permanent admin grant behind.

JIT flips the steady state. With standing access, the default is “everyone has more than they need, all the time,” and you spend effort removing excess you’ll never fully catch. With JIT, the default is “nobody has anything until they ask,” and privilege exists only for the minutes it’s actually in use. You’re no longer fighting creep with cleanup — you’ve removed the surface creep accumulates on.

Pick the best fit

A service role has `s3:*` on `Resource: *` because someone added a wildcard 18 months ago to unblock a release. The service actually only ever calls GetObject and PutObject on one bucket. What's the right fix?

How seniors hold the line

Holding least privilege is an operational loop, not a one-time config. The senior moves: make deny the default so new surface fails closed; make grants narrow (specific actions on specific resources, no reflexive wildcards) and short-lived (JIT with hard expiry) so privilege exists only while it’s used; and close the loop with revocation — when someone changes teams, when a project ends, when break-glass fires, the subtraction must be owned, not left to a quarterly audit that errs toward keeping. The grant that has no expiry and no owner is the one that becomes next year’s s3:* incident.

Quiz

Why does privilege creep almost always make permissions grow over time rather than shrink, even on well-run teams?

Quiz

What is the core security advantage of just-in-time access over a standing admin grant?

Order the steps

Order these steps to remediate an over-privileged service role without an outage, from first to last:

  1. 1 Observe which actions/resources the role actually uses (access logs / IAM access analyzer)
  2. 2 Write a new policy scoped to exactly those observed actions on those specific resources
  3. 3 Deploy the scoped policy and watch for deny errors / 403s in staging and canary
  4. 4 Replace the long-lived key with short-lived assumed-role credentials
  5. 5 Add an owner and an expiry/review date so the grant can't silently become standing again
Recall before you leave
  1. 01
    Explain why least privilege erodes into over-privilege over time, and name the mechanism.
  2. 02
    What do deny-by-default and just-in-time access each fix, and why is JIT the more durable cure?
Recap

Least privilege is the principle everyone cites and almost nobody holds, because it isn’t a setting you configure once — it’s a property you maintain against constant pressure. Granting is pulled by a real, immediate blocker; revoking has no equivalent pull, so permissions drift monotonically upward. That decay is privilege creep, fed by role changes that add without subtracting, expedient wildcards shipped to beat deadlines, unrevoked break-glass, and copied roles. The operational cure has three moves. Deny by default makes authorization start from “no,” so new surface fails closed — a forgotten rule yields a 403, not a breach. Narrow, short-lived grants scope permissions to specific actions on specific resources. And just-in-time access makes revocation automatic: elevation is requested per task, held for a bounded window, and stripped when it closes, so the default state of every identity is unprivileged and a leaked credential is worthless minutes later. Now when you see s3:* on Resource: *, your first question is: who owns the subtraction, and when does this grant expire?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.