At Cigna, Terraform ran in test and prod as a least-privilege IAM role vended into the account. The policies lived in an enterprise-managed GitHub repo, and the enterprise pushed them into the accounts. Changing one meant a lap: edit the policy in the repo, go through the deployment process that pushed it into my account, run Terraform as that role, see where it failed, add the missing permission. “Rinse repeat, rinse, repeat, rinse, repeat.” Each lap took an hour to an hour and a half, “if not more.” The same policy, iterated somewhere I could edit it directly, took about five minutes a lap.
The loop
The deploy roles exist for a good reason: “you don’t simply give full admin rights to everything. You want to give only the kind of rights that are necessary to do the deployments and nothing more.” The cost was the feedback. Every lap paid the full enterprise push to find the next failure.
On the project I was on, getting the roles right took “probably three days or four days of just going back and forth in this monotonous, painful process of one hour or one and a half hour cycles.” That was one greenfield application on the path to prod — the station where getting least-privilege roles into the test account was a journey in itself.
Why it surfaced at test
Dev had “full admin rights, mostly admin rights.” So the infrastructure got built in dev with nothing recording what it needed. Least privilege only became a question at test, where the vended role was the only way in. By then the build touched “a whole bunch of different services within AWS,” and the policy had to be worked out backwards from failures.
Working it out up front doesn’t close the gap: “you’re always going to miss a little bits here and there.” The enterprise’s golden modules made it worse. They were pre-baked Terraform, and “you don’t necessarily know all the things that they’re baking within their modules, and so you have to go also add all the capabilities that they need.” Trial and error was the only option at the time.
Iterating in the dev account
So I moved the loop. I stood up an EC2 instance in the dev account, where I already had admin, gave it a role of its own that I could edit directly, and ran Terraform from inside it against the environment. Same edit, same apply, same failure to read — about five minutes a lap instead of an hour and up.
It still wasn’t pleasant. “I was able to iterate a lot faster, but the whole thing was super painful.” The part that paid off came at the end. When I took the finished policy to the enterprise repo for test, it worked on the first try, because “it was an actual policy that I was working with.” I copied the policy definition into the vended role “and it simply worked.” No translation step, no approximation to reconcile. The document I iterated was the document I shipped.
I taught it to other teams in my org who were in the same spot. I didn’t try to make it an enterprise process. The pain is mostly a greenfield tax anyway: “for people that have brownfield applications, the vast majority of times you’re not modifying infrastructure.”
Module owners ship the policy
The platform fixed the golden-module half since. The role-vending service now offers “pre-canned policies that are built by the people that create the golden modules,” so whatever a golden module needs is already available when you vend the role. The team that knows what the module does writes down what it needs, once, instead of every consuming team rediscovering it one denial at a time.
That pattern isn’t standard in public Terraform. A 2019 request asked the AWS provider for the least-privilege actions each resource needs. The workarounds listed in it start with applying the plan and adding actions from the error messages. It was closed as not planned in 2025, with no substantive reply from the maintainers. Some public modules check in a minimum policy file instead, which is the same idea at the repo level.
What each tool catches
Several tools try to produce the policy before the first failure. None of them produces a complete deploy policy on its own. Each sees a different slice of what Terraform does, and the table is what their docs, and one public failure, say they miss. It’s reference, not something I ran at Cigna.
| Tool | Works from | Misses |
|---|---|---|
| IAM Access Analyzer policy generation | Up to 90 days of CloudTrail for one role |
iam:PassRole; action-level detail for data events such as S3 objects; conditions.
Some services come back as service-level templates you fill by hand. Denied calls are counted, so
a failing run inflates it
|
| Last-accessed data | IAM’s own access tracking |
It only tells you what to remove, never what’s missing. No action-level data for data-plane
events, no iam:PassRole, and failed attempts count as use
|
| iamlive, proxy mode | The provider’s HTTPS calls, live | Whatever the run didn’t exercise — a create-only run misses update-path calls (below). CSM mode records actions only; resources need proxy mode |
| Pike | A static scan of the .tf files |
Conditions; partial resources come out wildcarded; some iam:PassRole and
service-linked-role needs go unflagged
|
| IAM Policy Autopilot | terraform plan JSON |
Identity-based policies only; no native .tf input; its README says nothing about
conditions
|
Create-only capture misses updates
A capture is only as complete as the run it watched. The
EKS Blueprints project
published a minimum IAM policy generated by iamlive in its own end-to-end tests, and a user’s apply failed
on it with AccessDenied for iam:TagOpenIDConnectProvider and eks:TagResource.
The maintainer’s diagnosis: “this API will only happen if we update (AKA ‘add’) new tag, as the first
apply that we do is just a create.” He reproduced it — no tag call on create, the tag call after adding a
tag and applying again. The fix was adding the two actions by hand.
So a capture runs the whole lifecycle: create, then change a tag and apply again, then destroy. Each phase calls APIs the others don’t.
Blind spots you add by hand
Some permissions no capture is guaranteed to see. iam:PassRole is the big one:
AWS says it “is
not an API call,” so no CloudTrail event exists for it. Scope it to the role ARNs you actually pass, with
iam:PassedToService. Service-linked-role creation only shows up if the role doesn’t exist yet
in the account you capture in. Conditions — on tags, on KMS grants — are a manual hardening pass every
time. None of the tools above documents generating them.
The state backend needs no discovery at all. HashiCorp’s S3 backend docs list the bucket and lock permissions. Paste them.
A capture, as reference
None of the commands below came from that build. They’re reference, written from the iamlive README, the names are placeholders, and they haven’t been run end to end. Run it in a sandbox account with broad rights: the point is to record what the code needs, not to enforce anything yet. Proxy mode intercepts TLS, which is another reason it stays out of anything shared.
# terminal A: the listener
iamlive --mode proxy --sort-alphabetical --output-file ./captured.json
# terminal B: terraform through the proxy, every phase of the lifecycle
export HTTP_PROXY=http://127.0.0.1:10080
export HTTPS_PROXY=http://127.0.0.1:10080
export AWS_CA_BUNDLE=~/.iamlive/ca.pem
terraform apply -auto-approve # create
terraform apply -auto-approve -var 'extra_tag=changed' # update: tag calls show up here
terraform destroy -auto-approve # destroy needs its own Delete* and Detach*
What comes out is a first draft. Add iam:PassRole and the conditions by hand, then run the
residual failures through a loop where a policy edit is a direct API call on a role you own, not a push
through someone else’s pipeline. That loop is the EC2 box, without the box.
The rule
Least privilege isn’t expensive because the policy is hard. It’s expensive when every correction costs an hour. Iterate the real policy document where an edit takes minutes, then ship that exact file through the vending process once. The tools above shorten the list of laps. They don’t replace the loop. And whoever publishes a module publishes its policy — that’s where the platform landed. Getting accounts organized enough to vend roles into at all is landing-zone work.
Underneath
Docs checked 2026-10-28. Access Analyzer’s window, data-event, PassRole, and denied-call limits: IAM Access Analyzer policy generation. Last-accessed scope: Refine permissions using last accessed information. PassRole: Grant a user permissions to pass a role to an AWS service. Modes and flags: the iamlive README. The tag-on-update failure: EKS Blueprints issue 902. The provider request: terraform-provider-aws issue 9154. Static scanners: Pike, IAM Policy Autopilot. Backend permissions: S3 backend.
Related system
More field notes
Why my agent pipeline confirms merges with GitHub before marking work done, and the dashboards and alarms that still misled me
Claude Code, OpenAI Codex, GitHub CLI, Python, Bash
Newer · Oct 2026
Measured build times, payback math, and what would make renting the better choice
Apple Mac Studio, Amazon EC2 Spot Instances, Xcode, React, Python
Older · Oct 2026
Start
I’ll tell you in about a day whether I’m the right person. The first conversation is fit, not a free architecture review.