TL;DR
- Least privilege is a loop, not a policy you write once: grant what a workload demonstrably needs, watch what it actually uses (CloudTrail), remove the rest, repeat.
- Prefer roles and temporary credentials everywhere — instance profiles, IRSA, CI federation via OIDC. Long-lived access keys are the debt that keeps getting breached.
AdministratorAccess"temporarily" is how temporary becomes permanent. Start narrower; widening with data is safer than narrowing after an incident.- IAM Access Analyzer does the boring parts: it generates policies from real CloudTrail activity and flags unused and external access. Use it instead of guessing.
- Guardrails beat intentions: permission boundaries and SCPs cap the worst case even when a policy is written too wide.
Originally published on DevOps Enginer.
When I started learning AWS, I did it the way most engineers do: in my own account, on my own wallet, clicking and scripting my way through services to see what they did (and, as my cost post describes, occasionally paying for the ones I forgot to clean up). My auditing habit from that period was security-shaped: I used CloudTrail to see what had actually been created and called in the account. At the time it felt like tidiness. It turned out to be the core skill of IAM done properly — because least privilege is impossible without exactly that evidence. You cannot right-size permissions for actions you never look at.
Most IAM advice fails in one of two directions: "grant admin, ship it" (fast, until it's catastrophic) or "spend three weeks crafting the perfect policy" (safe, until the business routes around you). This guide is the middle path DevOps teams actually sustain: a repeatable loop, the AWS tools that automate the tedious parts, and the guardrails that bound the damage when a policy is still too wide.
The mental model: how AWS decides
An IAM decision is the intersection of several layers, and least privilege gets easier once you see them separately:
- Identity-based policies (attached to users/roles) say what this identity wants to do.
- Resource-based policies (on buckets, queues, etc.) say who may touch this resource — and can grant across accounts.
- Permission boundaries cap what an identity policy can ever grant, no matter what it says — the ceiling you set when delegating policy-writing to teams or pipelines.
- Service Control Policies (SCPs), in AWS Organizations, cap entire accounts the same way.
- Explicit Deny anywhere overrides every Allow everywhere. Deny is the trump card; it's how you write "never, not even for admins" rules.
A request is allowed only if every applicable layer permits it and no layer explicitly denies it. When access breaks, something in that stack said no; when access is too broad, it's usually because one layer (the identity policy) was written wide and no other layer was ever asked to constrain it.
The loop: grant, observe, trim, repeat
- Start from the job, not the person. Define roles per workload and per function ("deploys the API", "reads this bucket"), never per human with a grab-bag of everything they've ever needed.
- Grant a first approximation from documentation and the workload's code — the SDK calls it makes are knowable. Err slightly narrow; a denied call in testing is information, not failure.
- Observe real usage. CloudTrail records what principals actually call. This is the evidence base — the same habit as auditing your own account, pointed at permissions. Give new roles a settling period in staging (or production with monitoring) before judging.
- Generate and compare. IAM Access Analyzer can generate a policy from CloudTrail activity — a draft of what the role demonstrably used. Diff it against what you granted: everything granted-but-never-used is a candidate for removal.
-
Trim in stages. Remove unused actions first, then scope resources (specific ARNs instead of
*), then add conditions (source IP, tags,aws:PrincipalTag, MFA) where they fit. Re-observe after each pass. - Institutionalise the review. Access Analyzer also reports unused access — roles, keys and permissions idle beyond your threshold — and external access findings, where a resource is shared outside your organisation. Put both on a recurring review, like the cloud bill. Permissions rot exactly the way unused resources do: silently, and in your favour never.
Identities: kill the long-lived key
The mechanics matter as much as the policies:
- Humans: SSO/federated sign-in with MFA, short sessions, group- or role-based assignment. The root user gets MFA, a strong password, and no daily-driver use — ideally no access keys at all.
- Workloads on AWS: roles, always — instance profiles for EC2, task roles for ECS, IRSA (IAM Roles for Service Accounts) for EKS pods. The credentials are temporary and rotated by AWS; there is nothing to leak from a config file because there is no long-lived secret.
-
CI/CD: federate, don't store keys. GitHub Actions (and peers) can exchange an OIDC token for short-lived AWS credentials via
aws-actions/configure-aws-credentials— noAWS_SECRET_ACCESS_KEYsitting in a secrets store for years. This one change removes the most commonly leaked AWS credential type from most breaches' usual story. - Access keys that must exist (some third-party tools still insist) get: their own user, minimal policy, a rotation date, and a calendar reminder with an owner. The IAM credential report shows you every key's age and last use — run it before an auditor does.
Policy craft: the four tightenings
Given any too-broad policy, these moves, in order, buy the most safety per edit:
-
Actions: replace service wildcards (
s3:*) with the verbs actually called (from CloudTrail/Access Analyzer output). Read-only and write paths usually split naturally here. -
Resources: replace
"Resource": "*"with ARNs — this bucket, this table, this queue. Half of "least privilege" lives in this one field. - Conditions: require the context that should be true — requests from your network or VPC endpoint, resources carrying your tags, sessions with MFA. Conditions turn "whoever holds this role" into "whoever holds this role, from here, doing this".
- Split roles when duties split. One role that deploys infrastructure and reads application data will be too broad for both jobs. Two narrow roles are not bureaucracy; they're the shape of the work.
Validate before you ship: Access Analyzer's policy checks flag overly permissive grants (and plain errors) in new or edited policies — wire that check into the PR that changes the policy, next to the plan output, so review sees both what infrastructure changes and what powers change.
Guardrails: cap the worst case
Even with the loop running, individual policies will sometimes be written too wide. Guardrails limit the blast radius anyway:
- Permission boundaries on every role your pipelines or teams create — the boundary says "no role minted here can ever exceed this", which makes delegation safe.
- SCPs at the organisation level for the true never-evers: no leaving the organisation, no disabling CloudTrail, no unencrypted storage classes — whatever your regulated context demands, expressed once, applied everywhere.
- Explicit-deny policies for the handful of actions nobody should take casually (deleting the audit trail is the canonical one), relying on deny-beats-allow to hold even against admin policies.
- Break-glass, designed in advance: a tightly monitored, MFA-bound admin path that exists for incidents — so that "we needed admin fast" stops being the reason admin is standing permission for everyone.
Common mistakes
-
AdministratorAccessas a starter policy. It never gets narrowed, because narrowing it later feels risky. Start narrow with evidence and widen on data instead. - One shared IAM user for the team (or the app). No attribution in CloudTrail, no per-person revocation, one key to leak. CloudTrail's value collapses when every event says the same name.
-
Wildcard resources because ARNs are "annoying". The ARN is the privilege boundary.
*means every bucket you'll ever create, too — including the one with the backups. - Long-lived keys in CI. The OIDC federation path exists and takes an afternoon. Keys in CI secrets are the leak that keeps leaking.
- Writing policies and never reading usage. A least-privilege policy that was right in January is a historical document by December. The loop is the practice; the initial policy is just its first iteration.
-
Forgetting data-plane neighbours. The role is tight but the bucket policy allows a whole account, or the role can
iam:PassRolea bigger role. Least privilege audits the paths, not just the policy in front of you — PassRole and resource policies are where quiet escalation lives.
FAQ
Won't trimming permissions break production?
Done blind, yes; done by the loop, no — you remove what CloudTrail shows was never called, in stages, watching for denials. The breakage stories come from guessing. Evidence-based trimming is boring, which is the point.
How narrow is narrow enough?
When the role can do its job, the unused-access findings are quiet, and a compromise of that role would embarrass you rather than end you. It's a direction with checkpoints, not a destination.
Access Analyzer says a role is unused for 90 days. Delete it?
Not immediately — check whether it's a seasonal/emergency role (break-glass and quarterly jobs look unused by design). Mark intentional ones, delete accidental ones, and let the next finding cycle confirm.
Do I still need MFA if everything is SSO?
Yes — MFA at the identity provider, and AWS-side conditions where it matters. SSO without MFA moves the single point of failure; it doesn't remove it.
What about the policies AWS manages (job-function policies)?
Useful references and reasonable starting points for human job roles, but they're written for everyone's job, not your workload's. Workloads get the loop; humans get job functions plus review.
Conclusion
Least privilege stops being a slogan the moment you treat it as a measurement practice: CloudTrail shows what identities actually do, Access Analyzer drafts what they should be allowed to do, and boundaries/SCPs cap what they could ever do. My own-account habit — auditing what was created and called, then cleaning up what shouldn't be there — turned out to be the whole discipline in miniature: permissions, like resources, are easy to grant and quietly expensive to leave lying around.
Start with the highest-blast-radius identity you have — the admin-ish role everyone shares — and run one pass of the loop on it. One role, evidenced and trimmed, teaches the team more than a policy-writing project ever will. Then put the review on the calendar next to the cost review; they're the same meeting, really: what are we running, what can it do, and what of that do we still mean?
Related reading on this site:
- DevSecOps Checklist 2026 — where IAM fits in the wider pipeline-security picture
- AWS Cost Optimization 2026 — the cost twin of this discipline: measure, then trim
- Terraform State Management in 2026 — the state backend access this post says to guard like a vault
Top comments (0)