Industry Insights
On Behalf of Whom? How AWS Broke Non-Repudiation and Shipped It as a Feature
July 9, 2026
·
8-Minute Read
On July 1, AWS IAM Identity Center shipped something engineers have been asking about for years: a server-side application can now exchange a user’s OIDC token for that user’s AWS access, then act in AWS as that user. Aidan Steele published a careful walkthrough within days, and it’s worth reading in full. He flags most of the sharp edges himself: you can’t scope the application below the user’s full access, you can’t prove that an app rather than the human performed an action, and the chain of events behind the credential can’t be stitched together reliably.
Before the mechanics, name what’s at stake, because it’s bigger than a missing IAM condition key. One of the first principles anyone in security absorbs, from the SOC analyst triaging their first alert to the architect designing the logging pipeline to the CISO signing the compliance attestation, is non-repudiation: when an action happens, you can establish who performed it, and that person cannot credibly deny it. It’s why audit logs are append-only, and why “shared admin account” is a phrase that makes auditors reach for their pens. Every investigation you will ever run rests on the log being an honest witness.
Hold onto that principle, because this feature breaks it in both directions at once, and we’ll get to both. Steele files the gaps he found as feature requests, things AWS will presumably close over the coming releases. I read them differently. They follow from the design choice at the center of the feature, which is letting an application act under a human identity instead of federating as itself. You can’t patch non-repudiation back into a system whose core mechanic is impersonation.
The Friction Was Doing Work
The pain this launch removes is real. Say you have a gnarly Terraform setup and you build an internal service to run it for your developers. Until now you had two options, both bad: grant the service a superset of everyone’s permissions and rebuild authorization logic inside the app, or accept that you’ve shipped a privilege escalation engine.
But look at what that annoying setup gave you. The service had its own IAM role, with a scoped policy and its own line in CloudTrail. The human came in through SSO with a separate role. Two principals, two policies, and when something happened you could tell who did it. The authorization logic you resented writing was where you stated, in a form AWS could enforce, what the service was allowed to do independent of any human. Deleting that layer doesn’t delete the need for authorization. It moves the decision somewhere you can no longer audit.
“No More and No Less Than the User” Is Not Least Privilege
Every writeup of this feature repeats the reassuring line that the app can do no more and no less than the user. Read that from the app’s side. An application’s permissions should be defined by its task. Under this model they’re defined by whoever authenticates to it. Steele’s own demo user lists fourteen accounts and picks up an AdministratorAccess role; any app acting in his name picks up exactly the same.
There is no native control for “this application may only assume these roles,” or “only in these accounts,” or “only under this session policy.” Steele asks AWS for exactly those. Until they exist, the effective scope of the application is whatever the most privileged human who ever logs into it happens to hold. The ceiling is real; it’s just set by your most over-privileged admin. And all of it arrives behind a checkbox labeled “enable AWS account access,” which is a strong contender for the most understated checkbox in the console right now.
The Log Says a Human Did It
This is the part every security team should sit with. Trace the CloudTrail events the feature leaves behind:
CreateTokenWithIAMis attributed to the application’s own IAM role. This is the only event in the entire trail where the app appears as itself. It also records acredentialId, which matters in a moment.ListAccounts, ListAccountRoles, andGetRoleCredentialsare attributed to anIdentityCenterUser, with anonBehalfOfblock naming the human. The samecredentialIdappears here, so these events can at least be joined back to the token exchange. The application itself appears nowhere in them.AssumeRoleWithSAML, the call that mints credentials in the target account, is attributed to theSAMLUser(the human) and carries noonBehalfOfand nocredentialId. The only way to link it to theGetRoleCredentialscall that triggered it is to match account, role, and timestamp: a correlation you’re guessing at, not one the platform guarantees. The chain goes dark at the exact step where a live credential is created.- Everything the app then does with those credentials is logged as the human’s SSO role. These events do carry an
onBehalfOfblock, but it names the human again, never the app. Nothing in the trail says which application was driving the session, or whether one was.
So a bucket gets deleted. CloudTrail says a user deleted it. The user says it wasn’t them, and today they have no way to prove an application did it in their name. Steele gives this exact example. It isn’t a corner case; it’s how the feature behaves by default.
This is where non-repudiation dies, and notice that it dies in both directions. The innocent user above can’t exonerate themselves. A guilty one, meanwhile, gets a gift: once applications routinely act in your name, “one of the apps must have done it” becomes a defense the logs can’t cleanly refute, because nothing in the record identifies which application, if any, was behind a session. Digital forensics has a name for “the malware did it, not me.” It’s called the trojan horse defense, and courts have spent two decades learning to be skeptical of it. AWS just shipped a variant the defendant can cite documentation for.
Add Prompt Injection and It Gets Worse
Any application that consumes untrusted input will eventually be steered by it. A coding agent reads a poisoned README, or a support bot processes a crafted ticket, and now the app is executing someone else’s intent with whatever permissions it holds. The app isn’t malicious. It’s convinced.
Before this feature, that confused deputy was bounded by the app’s own role: a policy you wrote, attached to a principal you could revoke. After it, the deputy operates with the full cross-account entitlements of whichever human authenticated, and every action lands in the log under that human’s name. Your incident responder at 2 a.m. sees a person doing all of it.
Short-Lived Credentials Are Right, Impersonation Isn’t
We’ve argued for a long time that killing long-lived keys is one of the highest-value moves in identity security, so let me be precise about the objection. If this feature merely replaced a static access key with an ephemeral federated exchange, it would be a clear win. Ephemeral credentials bound to a distinct workload identity keep the agent visible as itself, with its own scope and its own audit trail.
This launch does something different. It hands the application the human’s identity along with the short-lived credential. Those are two separable properties, and AWS fused them. The objection is to the second, not the first.
The operational cost is concrete. The lineage you need in an investigation, from person to agent to identity to action, is severed at the agent. Segregation of duties between what a person may do and what a service may do collapses behind a single toggle. And when the alert fires, you can’t say whether a human or an app acted, which app it was, or where else that trust is wired.
What to Do This Week
You don’t have to wait for AWS to ship the missing controls.
- Treat “enable AWS account access” as a high-blast-radius change. Give it a named owner and a reviewed use case; it is not a convenience toggle.
- Keep dedicated roles for services. If a service has a job, give it an identity scoped to that job, and let the task define the blast radius rather than the most privileged human who logs in.
- Alert on
CreateTokenWithIAMby application principal. ThecredentialIdin that event reappears in the userIdentity of theListAccounts,ListAccountRoles, andGetRoleCredentialscalls that follow, so join on it. The thread snaps atAssumeRoleWithSAML; from there you are back to matching account, role, and timestamp, and that guess is the only thread tying an app to what it did as a human. - Treat every
onBehalfOfevent as a machine action for attribution, even though it displays as a human. - Push AWS for the missing controls. An
applicationArninsideonBehalfOf, per-application role and account scoping, and a durable link betweenGetRoleCredentialsandAssumeRoleWithSAML. In the meantime, require your applications to identify themselves in theuserAgent.
The primitive underneath this launch is good. Federated, short-lived access should replace static keys everywhere it can. But an application should federate as itself, not as the person using it. The boundary between human and non-human identity is what keeps an access model governable, and what keeps non-repudiation a property of your environment rather than a word on a compliance slide. AWS made that boundary optional. Don’t treat it that way in yours.
