β Human-authored analysis; AI used for formatting and proofreading.
It was first formally articulated by Jerome Saltzer and Michael Schroeder in their seminal 1975 paper, "The Protection of Information in Computer Systems."
They defined it as the requirement that "every program and every user of the system should operate using the least set of privileges necessary to complete the job."
While this remains a cornerstone of cybersecurity, the world for which Saltzer and Schroeder designed this principle (1970s mainframes) has changed so much that several of their foundational assumptions no longer hold true.
Saltzer and Schroeder built PoLP on several assumptions inherent to the computing landscape of the 1970s:
Modern computingβespecially with the advent of Cloud, Microservices, and AIβhas broken these assumptions.
In 1975, a "Tape Operator" needed access to the tape drive. Today, a microservice might need access to a database for 10 seconds to run a specific query and never again.
The Flaw: We no longer have static "jobs." We have ephemeral tasks. If you grant a service "least privilege" for its entire lifecycle, you are over-privileging it for 99% of its existence.
Saltzer and Schroeder assumed an admin could define the "least" set of rights.
The Flaw: Modern AWS environments have over 14,000 distinct permissions. In a system built with AI, the developer often doesn't fully understand the logic paths of the code. If you don't fully understand what the code does, it is mathematically impossible to define the least amount of privilege it needs. You end up guessing, which leads to "Permission Bloat."
PoLP assumed the "program" was a monolith you controlled.
The Flaw: Modern software is a "Russian Doll" of dependencies. A simple Node.js app might have 1,500 sub-dependencies. If you grant the app access to a file system, you are granting that access to 1,500 unknown authors. The Actor is no longer a single entity. Itβs a massive, invisible supply chain.
Saltzer and Schroeder focused on granting rights at the gate.
The Flaw: We now know that identity is the new perimeter. Because attackers move laterally using valid but "least" privileges, the assumption that "Least Privilege = Safety" is wrong. An attacker with "least privilege" to a single sensitive database is still a catastrophic failure.
The original principle assumes that if you restrict a program too much, it just stops working and you fix the permission.
The Flaw: In complex, distributed systems, "too little privilege" creates Heisenbugs β errors that only appear under specific load or rare logic paths. This makes developers "Default to Admin" (over-privileging) simply to ensure the system is reliable, defeating the principle entirely.
Least privilege says: grant only the permissions needed to perform the task. The word needed does all the work in that sentence and nobody defines it.
What permissions does a deployment service need? What S3 buckets does a data pipeline need to access? What IAM actions does a CI/CD role need? The answers exist in someone's head, in a Slack thread, in a ticket that was closed without documentation. They do not exist as a machine-readable specification that a system can enforce.
Without a definition of needed, least privilege reduces to a retrospective comparison: what was granted versus what was used. That comparison drives the entire ecosystem of tools, dashboards, and compliance checks the industry has built. It is the wrong comparison.
AWS IAM offers approximately 15,000 distinct actions across more than 300 services. A developer needs to deploy the application. That intent maps to somewhere between 5 and 500 IAM actions depending on the architecture, the deployment method, the target environment, and the dependencies involved.
Nobody sits down and enumerates the exact 47 actions required. They attach a managed policy like PowerUserAccess
or a custom policy with wildcards, and the system works. The gap between the human intent ("deploy the app") and the IAM implementation (47 specific actions with resource-level scoping and condition keys) is too large for manual enumeration. It's a translation problem with no translator.
Permissions are granted at provisioning time. Requirements emerge over time. A role created for a project few months ago may have different needs today. New services added, old integrations retired, architecture changed. But permissions don't update because the specification of what's needed doesn't exist and therefore can't change.
The industry's answer: compare what's granted against what's been used in the last 90 days, and flag the difference as excessive. Remove a permission that was "unused for 90 days" and you may break a disaster recovery process that runs annually. Keep it and the compliance tool flags it as excessive privilege. There's no right answer because needed was never declared. So used becomes the proxy, and proxies drift from reality.
Just-In-Time access partially addresses this mismatch. Instead of granting permanent permissions, identities request elevated access for a bounded window (two hours, one deployment, one incident). JIT mitigates the stale permission problem because permissions expire automatically. But JIT still requires someone to define what each JIT role should grant. That definition is the intent specification by another name. JIT without declared intent is "temporary broad access" instead of "permanent broad access." The window is shorter. The specification is still missing.
Individual permissions are evaluated in isolation. Compound paths are evaluated by nobody.
Role A has sts:AssumeRole
β reasonable for cross-account access. Role B has s3:GetObject
on a sensitive data bucket β approved for the data team. Least privilege evaluated each role independently and approved both. But A can assume B, and therefore A can read the sensitive data bucket. The compound path was never evaluated because least privilege operates on individual identities, not on the graph of relationships between them.
This is not an edge case. In environments with 10,000 identities, the number of compound paths numbers in the millions. Justin Kohler at SpecterOps reports 22 million paths to Tier 0 assets in environments of that scale, with 70-100% of identities having a path at deployment. Least privilege, evaluated per-identity, approved every one of those 22 million paths individually because each individual permission looked reasonable.
The industry has built an entire category of tools around treating the symptom:
Access Advisor and CIEM tools compare granted permissions against used permissions. They report: "This role has 30 accessible services. Only 5 were used in the last 90 days. The other 25 are excessive." The number is precise. The recommendation is dangerous. Nobody knows if those 25 services are needed for the quarterly process, the annual audit, the disaster recovery plan, or the feature that launches next month.
Periodic access reviews ask managers: "Does this person still need these permissions?" The manager has no way to answer. They don't know what the permissions do, what would break if they were removed, or what the person's role will require next quarter. They click approve because clicking deny has consequences (broken access) while clicking approve has postponed the consequences (the breach).
Automated remediation removes unused permissions after a threshold. This causes outages when legitimately needed but infrequently used permissions are revoked. Teams learn to distrust the automation, create exceptions, and eventually ignore it. The tool becomes noise.
Every tool in this category measures the same thing: the delta between what's granted and what's historically used. None of them measure what matters: the delta between what's granted and what's intended.
The principle of least privilege assumes an artifact that the industry never builds: a machine-readable declaration of intent.
What does this identity need access to, and why? That's the intent specification. If it existed, least privilege wouldn't be a principle to follow. It would be an automatic property of the system.
INTENT SPECIFICATION (the missing artifact):
"Deployment service reads container images from ECR repository
'prod-images', deploys to ECS cluster 'prod-cluster',
and writes deployment logs to CloudWatch log group '/deploy/prod'"
DERIVED POLICY (automatically generated):
ecr:GetDownloadUrlForLayer on arn:aws:ecr:*:*:repository/prod-images
ecr:BatchGetImage on arn:aws:ecr:*:*:repository/prod-images
ecs:UpdateService on arn:aws:ecs:*:*:cluster/prod-cluster/*
logs:PutLogEvents on arn:aws:logs:*:*:log-group:/deploy/prod:*
(+ the specific IAM actions for each operation)
VERIFICATION (automatic):
Does the IAM policy match the derived policy?
If wider β finding: "permissions exceed declared intent"
If narrower β finding: "permissions insufficient for declared intent"
If matching β pass
The declaration is the artifact that makes everything else work. The policy derivation eliminates the granularity mismatch. The system translates intent to IAM actions. The verification eliminates the temporal mismatch. When intent changes, the declaration updates, and the verification catches the drift automatically. The declaration eliminates the composition mismatch. Compound paths can be evaluated against the declared intent of each identity in the chain.
The shift is architectural.
Least privilege is backward-looking. It compares what's granted against what's used. It treats history as a proxy for need. It operates on individual identities without considering their relationships. It generates findings nobody can act on because nobody knows if the excessive permission is really excessive.
Declared privilege is forward-looking. It compares what's granted against what's intended. It treats the specification as the source of truth. It can evaluate compound paths because the intent of each identity in the chain is declared. It generates findings that are actionable because the specification defines what correct looks like.
| Least privilege (current) | Declared privilege (correct) | |
|---|---|---|
| Compares against | Historical usage | Declared intent |
| Direction | Backward-looking | Forward-looking |
| Granularity problem | Human must enumerate actions | System derives actions from intent |
| Temporal problem | "Unused for 90 days" β "not needed" | Intent specification updates when requirements change |
| Composition problem | Evaluates identities individually | Evaluates compound paths against declared intent |
| Findings | "25 unused services" β nobody acts | "3 permissions exceed declared intent" β actionable |
| Failure mode | Privilege creep (inevitable) | Specification drift (detectable and fixable) |
Least privilege fails because it conflates things that change at different rates into a single artifact β the IAM policy. Stewart Brand described this problem in How Buildings Learn: buildings are composed of shearing layers β site, structure, skin, services, space plan, stuff β each changing at a different rate. Buildings that work well are designed so faster-changing layers don't disrupt slower-changing ones. Buildings that fail force renovations of the structure every time the space plan changes.
IAM has the same problem. The policy tries to simultaneously express what the system is (architecture), what the system does (operations), and what the system may do (permissions). These three concerns change at different rates:
Architecture (what services exist and how they connect)
β changes quarterly β new services, retired integrations
Operations (what the system does day-to-day)
β changes weekly β new features, new data flows
Permissions (what IAM actions are granted)
β changes at provisioning time β and then never again
Permissions are granted once and frozen. Architecture and operations evolve continuously. The gap between them is privilege creep. A structural consequence of coupling things that change at different rates into a single artifact that doesn't change at all.
Declarative intent separates these concerns into layers that evolve independently at their natural rates:
Layer 1: INTENT SPECIFICATION (changes when architecture changes)
"This service reads from S3 bucket X and writes to DynamoDB table Y"
β changes quarterly, reviewed at design time
β owned by the team that builds the service
Layer 2: DERIVED POLICY (changes automatically when intent changes)
The minimum IAM actions required by the declared intent
β computed, not authored β no human manages 15,000 actions
β updates automatically when Layer 1 changes
Layer 3: DEPLOYED CONFIGURATION (changes at deployment time)
The actual IAM policy attached to the role
β verified against Layer 2 on every snapshot
β any deviation = finding, not creep
The verification math lives here. It already exists. AWS's IAM Access Analyzer uses Zelkova, an automated reasoning engine built on SMT solvers, to formally prove properties about IAM policies: whether a policy grants public access, whether it's more permissive than a reference policy, whether two policies are functionally equivalent. Zelkova can prove that a deployed policy is a subset of a derived policy. The exact mathematical operation Layer 3 requires. The engine exists. The missing piece is Layer 1: the intent specification that Layer 2 would translate into the reference policy that Layer 3 verifies against. The math to verify is built. The specification to verify against is not built.
When the architecture changes (new S3 bucket added to the pipeline), the team updates Layer 1 β the intent declaration. Layer 2 recomputes the minimum policy. Layer 3 is verified against Layer 2 on the next snapshot. The permission tracks the intent. No creep, because the layers are separated and each one changes at its own rate.
Without this separation, changing the architecture means someone must manually update the IAM policy. They don't, because the system works with the old policy (it has more permissions than needed). Privilege creep is inevitable under least privilege due to the property "works fine with stale permissions". The system never signals that permissions are stale because stale permissions don't cause failures. They only cause breaches.
The granularity problem also dissolves. Intent is expressed at the level humans think about: "reads from this bucket, writes to that table." The translation to 47 specific IAM actions with resource-level ARNs and condition keys happens in Layer 2 β automatically, consistently, and correctly. The human manages intent. The system manages granularity. Each operates at the level of abstraction where it's most effective.
This is the same architectural principle that Brand identified in buildings and that makes every successful layered system work: separate the things that change at different rates. Networking protocols separate physical, transport, and application layers because they evolve at different rates. Operating systems separate hardware abstraction from user-space applications for the same reason. IAM should separate intent from permissions for the same reason. It's the only layer in the stack that degrades monotonically over time. In Brand's terms, IAM policies are a building where the structure must be demolished every time someone rearranges the furniture.
Three structural forces keep the industry on the symptom treatment:
Force 1: The principle is in every framework. NIST, CIS, ISO 27001, SOC2, PCI-DSS β every compliance framework mandates least privilege. Auditors check for it. Tools measure it. Careers are built on implementing it. Questioning the principle feels like questioning gravity. But gravity is a law. Least privilege is a workaround for a missing specification.
Force 2: The tools are built. The CIEM market, the access review market, the IAM analytics market β billions of dollars of tools that measure granted-vs-used. These tools have revenue, customers, and roadmaps. They're not going to tell their customers that the metric they measure is the wrong one.
Force 3: The specification is hard to write. Declaring intent is design work. It requires understanding what each service does, why it exists, what it accesses, and what it should never access. Most organizations never did this design work. The services were built, the permissions were granted, and the system works. Writing the specification after the fact feels like documenting a house that's already built β expensive, tedious, and apparently unnecessary since the house is standing.
But the house is standing with every door unlocked. It hasn't been burglarized yet because nobody tried. That's not security. That's luck with a compliance checkbox.
The intent specification doesn't need to be written all at once. It doesn't need to cover every identity on day one. It needs to start with the identities that matter most. The ones with compound paths to critical assets and expand from there.
Infrastructure as Code is the natural home for this artifact. If the intent isn't in the code that deploys the resource, it won't exist anywhere. A Terraform module that creates a Lambda function should declare what that function accesses. It must specify the intent the role implements. The declaration lives next to the deployment. It changes in the same pull request. It's reviewed by the same team. If the specification isn't co-located with the infrastructure code, it becomes another document that drifts from reality. The same failure mode it's designed to prevent.
There is a technical hurdle here that reinforces the point. Most current IaC β standard Terraform, CloudFormation, Pulumi is mechanistic. It is declarative about state (what resources should exist) but silent about intent (why they exist and how they relate). A Terraform configuration declares "create this IAM role and attach this policy". It doesn't declare "this Lambda function reads from this S3 bucket." The permission is a separate aws_iam_policy_attachment
resource, disconnected from the relationship it's meant to express. The intent ("Lambda reads from S3") is in the developer's head. The code expresses the mechanism (iam:AttachRolePolicy
), not the meaning.
We need relation-oriented IaC β where permissions are a property of the relationship between two resources, not a separate resource managed independently. "This Lambda function reads from this S3 bucket" would be expressed as a relationship, and the minimum IAM policy would be derived from that relationship automatically. The developer declares the edge in the graph. The system derives the permissions.
AWS CDK already does it. In CDK, a developer writes myBucket.grantRead(myLambda)
β a single intent statement. The CDK compiler synthesizes the s3:GetObject
and s3:ListBucket
actions, scopes the ARN to that specific bucket, creates the IAM policy, and attaches it to the Lambda's execution role. The developer declared the relationship. The system derived the permissions. The intent specification exists as a method call.
The industry rejected this model in favor of raw Terraform. Why? Because security tooling β Checkov, Rego, tfsec can only analyze the mechanism (the JSON/HCL policy document), not the intent (grantRead
). InfoSec teams demanded to see the explicit IAM policy so they could run static analysis against it. The industry actively fought the intent-translator because the verification layer couldn't read intent, only mechanism. The tools designed to enforce least privilege made it harder to adopt the architecture that would make least privilege automatic. The gap in the principle reflects a gap in the tooling. The tooling actively resists the fix.
The specification must be simpler than the IAM policy it replaces. If declaring intent is as complex as writing the policy, developers will copy-paste the policy into the specification and the problem reproduces itself. The specification must operate at the level of intent. "Reads from this bucket, writes to that table" β not at the level of IAM actions. The translation from intent to actions is the system's job, not the developer's. If the developer must enumerate s3:GetObject
, s3:ListBucket
, s3:GetBucketLocation
in the specification, the specification is the policy with a different file extension. The way to prevent this is strict schema typing. The specification accepts predefined relationship verbs (reads, writes, invokes, administers) and rejects raw IAM action strings. If the schema physically cannot accept s3:PutObject
as input, the developer is forced to use the abstraction. The constraint is structural.
Not all intent is static. In a multi-tenant architecture, a Lambda function writes to whichever S3 bucket is assigned to the requesting customer. A target resolved at runtime from a DynamoDB lookup, not declared at provisioning time. The intent specification cannot name a specific bucket because the bucket doesn't exist yet when the specification is written. In these cases, the declaration becomes pattern-based β "writes to S3 buckets matching tag tenant-bucket: true
" and the derived policy uses condition keys rather than specific ARNs. This is less precise than a static declaration, but still vastly more constrained than s3:*
on *
, which is what the Lambda gets today. The dynamic case doesn't invalidate the approach. It bounds how precise the specification can be. For the majority of identities β service roles, CI/CD pipelines, automation accounts β the targets are static and fully declarable. For the dynamic minority, tag-based and pattern-based declarations constrain the blast radius even when they can't eliminate it.
Start with one identity. Declare what it should access in plain, high-level terms that a developer would naturally write. Derive the minimum policy. Compare it against the actual policy. Fix the delta. Move to the next identity. Each declaration is a ratchet β once the intent is specified, any deviation from it is detectable, actionable, and fixable.
Trail of Bits has practiced invariant-driven development for smart contract security since 2019. Declaring properties that must hold, verifying them mechanically, reporting violations. The methodology is proven. The domain is different but the principle is identical: declare what should be true, verify what is true, fix the difference.
The cloud security industry has spent a decade optimizing the wrong comparison. Granted-vs-used is a useful signal. It is not the right foundation. The right foundation is granted-vs-intended. It requires the artifact the industry never built.
The principle of least privilege is incomplete. It assumes a specification exists and measures compliance against it. The specification doesn't exist, so the measurement is against a proxy (historical usage) that diverges from reality over time. Fix the specification, and least privilege becomes automatic. Without it, least privilege remains what it has always been: a principle everyone agrees with and nobody achieves.
Least privilege is not the goal. It's the symptom of not having declared the goal.