Least Privilege
Give every identity — human, service, job or agent — exactly the permissions its job requires, because the permissions you grant are the definition of how bad a compromise of that identity can be.
Frame the problem
Security starts with a concrete asset, attacker capability and trust crossing.
Permissions are a damage budget
Every compromise has the same structure: an attacker obtains an identity and then does whatever that identity is permitted to do. Nothing about the initial bug determines the damage; the *permissions* determine the damage. This reframing is the whole point — least privilege is not hygiene, it is the mechanism that decides whether an incident is a paragraph or a headline.
Consider three services in the same system. The frontend renders pages and needs to read a product catalogue. The image worker resizes uploads and needs to read one bucket prefix and write another. The billing service issues refunds and needs write access to payments. If all three run with the same database credentials — which is the default outcome of a shared DATABASE_URL — then an XSS bug in the frontend, a malicious image file in the worker, and a logic bug in billing all have the same maximum consequence: everything.
The uncomfortable part is that least privilege is *invisible when it works*. A correctly scoped worker looks identical to an over-privileged one until the day it is compromised. That is why it must be enforced structurally — by making the narrow credential the easy one to use — rather than by intention.
- The frontend should not hold database administrator credentials — it should not hold database credentials at all, only an API it can call.
- The image worker should not have payment permissions; it should have read on
uploads/and write onthumbnails/and nothing else. - A background job that sends email needs the email API, not the user table.
- An analytics reader should read a replica, not the primary, and should not have
DELETEon anything. - An AI agent should not automatically inherit its operator's permissions; destructive tools require explicit grants and, usually, approval — see Agent Tool Capability Security.
Why it decays, and what to do about it
Least privilege fails in practice for a predictable reason: permissions are granted under time pressure and removed never. A deploy fails at 23:00, someone attaches a broad policy to make it work, the incident ends, and the policy stays. Six months later nobody remembers whether it is load-bearing, so nobody dares remove it. Multiply by every service and every engineer and you arrive at the normal state of most systems, where the median identity has far more access than its job requires.
The countermeasures are process-shaped rather than clever. Start closed: a new service gets a policy with nothing in it and gains permissions as its deploy fails, which produces a minimal set as a by-product of getting it working. Grant temporarily: expiring elevated access — an hour, a day — means the default drift is downward instead of upward. Measure usage: cloud providers and databases can report which granted permissions were actually exercised over 90 days, and unused permissions are the safest thing in the world to remove.
There is a real cost, and pretending otherwise is how the discipline gets abandoned. Narrow permissions cause outages when a legitimate new code path is not covered; they slow down debugging; they require more identities and therefore more credential management. The answer is not to grant broadly, but to make narrow grants cheap: infrastructure-as-code so a permission change is a reviewed diff, short-lived credentials so rotation is automatic, and good error messages so "permission denied" is diagnosable in seconds rather than hours.
1-- what most systems actually run2CREATE ROLE app WITH LOGIN PASSWORD '...' SUPERUSER;3-- every service uses DATABASE_URL=postgres://app:...@db/prod4-- an SQL injection anywhere reads every table, writes every row, and can read files1CREATE ROLE catalog_reader LOGIN PASSWORD '...';2GRANT USAGE ON SCHEMA public TO catalog_reader;3GRANT SELECT ON products, categories TO catalog_reader;4 5CREATE ROLE billing_writer LOGIN PASSWORD '...';6GRANT SELECT, INSERT, UPDATE ON payments, refunds TO billing_writer;7REVOKE ALL ON users FROM billing_writer;8 9-- the frontend's worst case is now "read the product catalogue"The damage of an injection or a leaked credential is bounded by the role, not by the bug. The same SQL injection that exfiltrates every user record under the shared superuser returns a product list under `catalog_reader` — the vulnerability is identical, the incident is not.
Key points
- Permissions define blast radius; the initial bug does not. This is the highest-leverage thing you can change after a compromise.
- Privilege drifts upward because grants happen in incidents and removals happen never — counter it with expiry and usage-based pruning.
- Start every new identity closed and let failing deploys reveal the minimum set.
- One identity per workload, not one per environment: shared credentials make every service as dangerous as the most dangerous one.
- Narrow grants have real costs; make them cheap with infrastructure-as-code and clear denial messages rather than abandoning them.
Boundary control exercise
This lesson uses the shared boundary-control exercise.
Follow the attack
Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.
- 1Attacker → obtain an identity: leaked key, compromised container, phished session, malicious dependency, or a tool call an agent was persuaded to make.
- 2Identity → enumerate permissions: ask the provider what this credential may do. Cloud APIs answer this directly; databases answer it through catalogue queries.
- 3Permissions → pivot: use the broadest granted verb — read a secret, assume another role, write to a deployment pipeline — to obtain a *better* identity.
- 4Better identity → asset: repeat until the identity has direct access to data, money or infrastructure.
- Over-privileged service: one application bug becomes full data access across every table the shared role can reach.
- Over-privileged human: a phished laptop becomes an administrative session over production infrastructure.
- Over-privileged CI: a malicious pull request or dependency obtains deploy rights and can ship code to production.
- Over-privileged agent: a single successful prompt injection reaches every tool the agent holds, which is why tool grants are the real security boundary.
Defend, detect, recover
One prevention is a single point of security failure. Layer it and make failure observable.
- • One identity per workload, scoped to specific resources and verbs; no shared `DATABASE_URL` across services.
- • Deny by default and add permissions from observed failures rather than from imagination.
- • Time-bound elevated access, with automatic expiry rather than a reminder to remove it.
- • Separate read and write identities so that analytics, exports and reporting cannot mutate anything.
- • Keep destructive verbs (`DELETE`, `DROP`, `iam:*`, refund, deploy) out of routine identities entirely, behind a separate approved path.
- • Alert on grants of wildcard permissions, on new administrative role assignments, and on any policy change that widens scope.
- • Report unused permissions per identity over a 90-day window and treat the report as a work queue.
- • Alert when an identity uses a permission it has never used before — the strongest single signal that a credential changed hands.
- • Revoke the identity rather than the individual permission; a compromised credential should stop working entirely.
- • Enumerate what the identity was permitted to do and audit *all* of it, not just what you saw it do.
- • Rotate anything the identity could read that was itself a credential, transitively.
- • Replace the broad grant with the narrow one during recovery — the incident is the only time this work reliably gets prioritised.
- • Some identities legitimately need broad access (backup jobs, migration runners, break-glass accounts) and remain high-value targets.
- • Permission systems are complex enough that a policy can be narrower than it looks and broader than intended — especially with inheritance and wildcards.
- • Least privilege bounds damage but does not prevent it: an attacker with exactly the right permissions can still do exactly the wrong thing.