Least Privilege in Practice -- Not Aspirationally
What This Concept Is
Least privilege means each identity has exactly the permissions it actually uses -- and no more. Not "the role that works." Not "the role named app-role." Not "the managed policy closest to what it needs." The specific permissions the job performs, scoped to the specific resources it touches.
"In practice, not aspirationally" means measuring least privilege by experiment, not by intention. The test is mechanical: tighten the policy until something real breaks. Widen just enough to un-break it. Document the widening. The final policy is the smallest set of grants that still allows the service to do its job today.
Least privilege is also a blast-radius control. If an attacker gains your API's credentials, the damage is bounded by what that identity can do. A wide AmazonS3FullAccess policy means the blast radius is every bucket in the account. A narrow s3:PutObject on capstone-audit-logs/api/* means the blast radius is a single prefix in a single bucket. The ratio of those blast radii is the value of this practice.
In the cloud, identity is the new perimeter -- the NSA's Zero Trust Maturity Model and CISA's Zero Trust architecture both call this out explicitly. Network boundaries have eroded; the security boundary that still holds is who the caller is and what they are allowed to do. Least privilege is the enforcement of that boundary at the call site.
Why It Matters Here (In the Capstone)
Over-permissive roles are the highest-leverage foothold an attacker gets in the cloud. A leaked key for a role with s3:* on * is a breach; a leaked key for a role with s3:GetObject on one bucket prefix is an incident and a rotation. Same initial failure, different outcomes.
Capstones tend to drift wide because the fastest way to un-break a deploy is to add a permission. That drift is invisible until an audit, or until something goes wrong. Doing the tightening before the PRR turns the drift into a one-time event instead of a compounding liability. The PRR (concept 15) has a specific checklist row for this: "every runtime identity has a documented minimum-permissions policy."
Concrete Example -- from a real capstone
Webhook-handler capstone. Three identities we actually use:
api-runtime-- the role the API service assumes at runtimeworker-runtime-- the role the queue consumer assumesci-deployer-- the role CI uses to deploy new versions
Before (drifted, default)
api-runtime has the AWS managed policy AmazonS3FullAccess attached "because we needed to write audit logs to a bucket once."
Tightening experiment
Replace the managed policy with a narrow inline policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AuditLogsWriteOnly",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::capstone-audit-logs/api/*"
},
{
"Sid": "SecretsRead",
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:us-east-1:*:secret:capstone/api/*"
}
]
}
Deploy to staging. Three minutes later an ingestion test fails with AccessDenied: the API reads a config blob from s3://capstone-config/* at startup.
Widen just enough
Add one statement, scoped to that exact prefix, read only:
{
"Sid": "ConfigReadOnly",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::capstone-config/api/*"
}
Redeploy. Tests pass. Commit. Write a one-line commit message: "api-runtime: scoped s3 access to audit write + config read per startup failure in 2026-04-22 staging run".
Document
In library/raw/iam.md:
| Role | Purpose | Permissions (summary) | Why this scope | Last widened |
|---|---|---|---|---|
| api-runtime | API service runtime | s3:PutObject to audit prefix; s3:GetObject to config prefix; secretsmanager:GetSecretValue | Writes audit, reads startup config, reads signing secret | 2026-04-22 |
| worker-runtime | Queue consumer runtime | sqs:ReceiveMessage/DeleteMessage on one queue; rds-db:connect to one DB user | Consumes queue, writes DB | 2026-04-20 |
| ci-deployer | CI deploy only | ecr:PushImage to one repo; lambda:UpdateFunctionCode on named functions; no read of runtime secrets | Build and ship | 2026-04-15 |
Three rows. Each defensible. Each was tightened until something broke.
Verification
Use aws iam simulate-principal-policy -- or the equivalent GCP gcloud iam policies analyze or Azure az role assignment list -- to answer "can this principal do X on resource Y?" in CI. One test case per row of the table turns your policy into a suite of assertions, and future policy drift fails CI the same way a type error would.
Common Confusion / Misconceptions
"We'll use managed policies; AWS knows best." AWS managed policies are designed for most customers, which means they are wider than you need. AmazonS3FullAccess is the canonical trap. Prefer narrow inline or customer-managed policies scoped to specific resources.
"The CI role needs to be wide so deploys don't break." The CI role needs to be wide for exactly the services CI deploys, not wide at all. A CI role with *:* can, on compromise, create new IAM roles for an attacker. Scope CI to the target resources. OIDC-federated short-lived tokens (see S9 M04 Cluster 5) reduce blast radius even further than long-lived keys do.
"Least privilege means read-only." No. It means exactly what is needed. Some roles legitimately need write. The point is the write is scoped to the resources they should be writing to -- not "the account."
"We'll get to it after launch." After launch, the role is in production and widening-by-accident has already happened. Do the tightening drill before PRR, on every runtime and CI identity, even if you only do it once.
"Wildcards are always wrong." Wildcards on resource names with a prefix (e.g., arn:aws:s3:::capstone-config/api/*) are fine. Wildcards on actions (s3:*) or on whole resources (Resource: "*") are the smell.
How To Use It (In Your Capstone)
- List every runtime identity in your capstone. Usually 3-6.
- For each, write down what it actually does in one sentence.
- Replace whatever policy it has with the minimum policy you think it needs.
- Deploy to staging. Watch it break.
- Widen one permission at a time, each scoped as narrowly as possible. Record each widening with a commit message that names the failing test.
- Once nothing breaks for a full test run, commit. Put the table in
library/raw/iam.mdand link it from the PRR. - Add an
aws iam simulate-principal-policytest to CI for each "MUST allow" and "MUST deny" assertion, so policy drift fails the build.
See also (integrative)
- S9 M01 Cloud Platform Fundamentals -- IAM principals, policies, roles vs users:
../../../../semester-09-cloud-devops/module-01-cloud-platform-fundamentals/concepts/cluster-05-identity-and-accounts/13-iam-principals-policies-roles-vs-users-primary.md. - S9 M05 Cluster 1 -- identity-centric security as "the new perimeter":
../../../../semester-09-cloud-devops/module-05-cloud-security-observability/concepts/cluster-01-cloud-security-foundations/02-identity-centric-security-the-new-perimeter-primary.md. - S9 M05 Cluster 1 -- defense in depth layers; least privilege is one layer, not the only one:
../../../../semester-09-cloud-devops/module-05-cloud-security-observability/concepts/cluster-01-cloud-security-foundations/03-defense-in-depth-layers-primary.md. - S9 M04 Cluster 5 -- pipeline security, OIDC, least privilege in CI -- the CI-role tightening this concept references:
../../../../semester-09-cloud-devops/module-04-ci-cd-pipelines-release-engineering/concepts/cluster-05-quality-gates-and-safety/13-pipeline-security-secrets-oidc-least-privilege-primary.md. - AWS IAM best practices -- apply least-privilege permissions -- the canonical vendor guidance this concept operationalises.
- NIST SP 800-207 -- Zero Trust Architecture -- why identity is the new perimeter and why this practice is foundational.
- OWASP ASVS V4 -- Access Control -- the verification checklist you can use to argue your roles meet a standard.
Check Yourself
- Why is
Action: s3:*worse thanResource: "*"with a narrow action set? Construct a scenario where each fails differently. - What is the argument for tightening CI / deploy roles more aggressively than runtime roles?
- Give one example of a legitimate wildcard in a least-privilege policy, and explain why it is acceptable.
- How does short-lived OIDC-federated credentials for CI reduce the impact of a leaked credential relative to static access keys?
- Why should the tightening of a role be the default operational move, and the widening the one that requires justification?
- What is the smallest evidence you would present at PRR to claim each runtime identity is least-privileged?
Mini Drill or Application (Capstone-scoped)
- Pick one runtime role in your capstone whose policy you did not personally write line-by-line. Diff its current effective permissions against what the service actually calls (CloudTrail,
aws iam simulate-principal-policy, GCP Policy Troubleshooter, or equivalent). - Replace the policy with a narrow inline version of the actually-used actions and resources.
- Deploy to staging, observe the breakage, widen narrowly.
- Add CI tests using
simulate-principal-policythat assert the role can do its three main actions and cannot do three sensitive ones (e.g.,iam:CreateUser,s3:DeleteBucket). - Commit the before/after diff to the repo and log the exercise in
library/raw/iam.md. - Repeat for the CI deployer role. This is usually the wider one and the higher-impact win.
Source Backbone
Capstone operations applies security, reliability, and distributed-systems material. These books are the source backbone for readiness review.
- Building Secure and Reliable Systems - primary security and reliability backbone.
- Software Engineering at Google - operational process and engineering discipline.
- Designing Distributed Systems - service and reliability pattern support.