{"slug": "least-privilege-is-a-workaround-for-a-missing-specification", "title": "Least Privilege is a Workaround for a Missing Specification", "summary": "Jerome Saltzer and Michael Schroeder's 1975 principle of least privilege (PoLP) is increasingly inadequate for modern cloud, microservices, and AI systems, according to a developer's analysis. The original assumptions of static jobs, understandable code, monolithic programs, and simple failure modes no longer hold, leading to permission bloat, Heisenbugs, and security gaps. The core problem is the lack of a machine-readable specification for 'needed' permissions, making least privilege a retrospective comparison rather than a proactive control.", "body_md": "✓ Human-authored analysis; AI used for formatting and proofreading.\n\nIt was first formally articulated by **Jerome Saltzer and Michael Schroeder** in their seminal 1975 paper, *\"The Protection of Information in Computer Systems.\"*\n\nThey 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.\"**\n\nWhile 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.\n\nSaltzer and Schroeder built PoLP on several assumptions inherent to the computing landscape of the 1970s:\n\nModern computing—especially with the advent of Cloud, Microservices, and AI—has broken these assumptions.\n\nIn 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.\n\n**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.\n\nSaltzer and Schroeder assumed an admin could define the \"least\" set of rights.\n\n**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.\"\n\nPoLP assumed the \"program\" was a monolith you controlled.\n\n**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.\n\nSaltzer and Schroeder focused on *granting* rights at the gate.\n\n**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.\n\nThe original principle assumes that if you restrict a program too much, it just stops working and you fix the permission.\n\n**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.\n\nLeast 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.\n\nWhat 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.\n\nWithout 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.\n\nAWS 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.\n\nNobody sits down and enumerates the exact 47 actions required. They attach a managed policy like `PowerUserAccess`\n\nor 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.\n\nPermissions 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.\n\nThe 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.\n\nJust-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.\n\nIndividual permissions are evaluated in isolation. Compound paths are evaluated by nobody.\n\nRole A has `sts:AssumeRole`\n\n— reasonable for cross-account access. Role B has `s3:GetObject`\n\non 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.\n\nThis 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.\n\nThe industry has built an entire category of tools around treating the symptom:\n\n**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.\n\n**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).\n\n**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.\n\nEvery 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.\n\nThe principle of least privilege assumes an artifact that the industry never builds: a machine-readable declaration of intent.\n\nWhat 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.\n\n```\nINTENT SPECIFICATION (the missing artifact):\n  \"Deployment service reads container images from ECR repository \n   'prod-images', deploys to ECS cluster 'prod-cluster', \n   and writes deployment logs to CloudWatch log group '/deploy/prod'\"\n\nDERIVED POLICY (automatically generated):\n  ecr:GetDownloadUrlForLayer on arn:aws:ecr:*:*:repository/prod-images\n  ecr:BatchGetImage on arn:aws:ecr:*:*:repository/prod-images\n  ecs:UpdateService on arn:aws:ecs:*:*:cluster/prod-cluster/*\n  logs:PutLogEvents on arn:aws:logs:*:*:log-group:/deploy/prod:*\n  (+ the specific IAM actions for each operation)\n\nVERIFICATION (automatic):\n  Does the IAM policy match the derived policy?\n  If wider → finding: \"permissions exceed declared intent\"\n  If narrower → finding: \"permissions insufficient for declared intent\"\n  If matching → pass\n```\n\nThe 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.\n\nThe shift is architectural.\n\n**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.\n\n**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.\n\n| Least privilege (current) | Declared privilege (correct) | |\n|---|---|---|\n| Compares against | Historical usage | Declared intent |\n| Direction | Backward-looking | Forward-looking |\n| Granularity problem | Human must enumerate actions | System derives actions from intent |\n| Temporal problem | \"Unused for 90 days\" ≠ \"not needed\" | Intent specification updates when requirements change |\n| Composition problem | Evaluates identities individually | Evaluates compound paths against declared intent |\n| Findings | \"25 unused services\" — nobody acts | \"3 permissions exceed declared intent\" — actionable |\n| Failure mode | Privilege creep (inevitable) | Specification drift (detectable and fixable) |\n\nLeast 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.\n\nIAM 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:\n\n```\nArchitecture (what services exist and how they connect)\n  → changes quarterly — new services, retired integrations\n\nOperations (what the system does day-to-day)\n  → changes weekly — new features, new data flows\n\nPermissions (what IAM actions are granted)\n  → changes at provisioning time — and then never again\n```\n\nPermissions 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.\n\nDeclarative intent separates these concerns into layers that evolve independently at their natural rates:\n\n```\nLayer 1: INTENT SPECIFICATION (changes when architecture changes)\n  \"This service reads from S3 bucket X and writes to DynamoDB table Y\"\n  → changes quarterly, reviewed at design time\n  → owned by the team that builds the service\n\nLayer 2: DERIVED POLICY (changes automatically when intent changes)\n  The minimum IAM actions required by the declared intent\n  → computed, not authored — no human manages 15,000 actions\n  → updates automatically when Layer 1 changes\n\nLayer 3: DEPLOYED CONFIGURATION (changes at deployment time)\n  The actual IAM policy attached to the role\n  → verified against Layer 2 on every snapshot\n  → any deviation = finding, not creep\n```\n\nThe 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.\n\nWhen 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.\n\nWithout 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.\n\nThe 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.\n\nThis 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.\n\nThree structural forces keep the industry on the symptom treatment:\n\n**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.\n\n**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.\n\n**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.\n\nBut 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.\n\nThe 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.\n\nInfrastructure 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.\n\nThere 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`\n\nresource, 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`\n\n), not the meaning.\n\nWe 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.\n\nAWS CDK already does it. In CDK, a developer writes `myBucket.grantRead(myLambda)`\n\n— a single intent statement. The CDK compiler synthesizes the `s3:GetObject`\n\nand `s3:ListBucket`\n\nactions, 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.\n\nThe 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`\n\n). 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.\n\nThe 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`\n\n, `s3:ListBucket`\n\n, `s3:GetBucketLocation`\n\nin 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`\n\nas input, the developer is forced to use the abstraction. The constraint is structural.\n\nNot 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`\n\n\" 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:*`\n\non `*`\n\n, 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.\n\nStart 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.\n\nTrail 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.\n\nThe 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.\n\nThe 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.\n\nLeast privilege is not the goal. It's the symptom of not having declared the goal.", "url": "https://wpnews.pro/news/least-privilege-is-a-workaround-for-a-missing-specification", "canonical_source": "https://dev.to/bala_paranj_059d338e44e7e/least-privilege-is-a-workaround-for-a-missing-specification-mje", "published_at": "2026-06-30 12:11:47+00:00", "updated_at": "2026-06-30 12:18:42.522292+00:00", "lang": "en", "topics": ["ai-safety", "ai-policy", "ai-infrastructure", "developer-tools"], "entities": ["Jerome Saltzer", "Michael Schroeder", "AWS IAM"], "alternates": {"html": "https://wpnews.pro/news/least-privilege-is-a-workaround-for-a-missing-specification", "markdown": "https://wpnews.pro/news/least-privilege-is-a-workaround-for-a-missing-specification.md", "text": "https://wpnews.pro/news/least-privilege-is-a-workaround-for-a-missing-specification.txt", "jsonld": "https://wpnews.pro/news/least-privilege-is-a-workaround-for-a-missing-specification.jsonld"}}