A Vendor Role, an Org Condition, and Three IMDSv1 Fleets
Helio Health runs 60 AWS accounts under AWS Organizations with a central security account. A third-party billing vendor needs read access to cost and usage data. The current design is a role VendorBillingRead in the payer account whose trust policy sets Principal to "*" with the condition "StringEquals": {"aws:PrincipalOrgID": "o-abc123"}. The vendor's own AWS account is a member of that organization, because two years ago they were onboarded as a partner account. Separately, three EC2 fleets in the production account still answer IMDSv1, and the account's SCPs only deny leaving the organization. A review found 14 roles carrying Action: "*" on Resource: "*", all created by a CI pipeline that authenticates with a long-lived IAM user access key stored in a build server's environment variables. CloudTrail is enabled in all accounts and delivered to a bucket in the log-archive account that has no bucket policy denying s3:DeleteObject.
1. Is `aws:PrincipalOrgID` with `Principal: "*"` doing what the team believes? Explain precisely what it constrains and what it leaves open.
Read the worked answer
It constrains who in the organization may call, and nothing else. With Principal: "*" plus that condition, any principal in any of the 60 accounts - any IAM user, any role, any stolen CI credential - can assume VendorBillingRead, as long as the request comes from an account in the org. That is the shape of both a confused-deputy exposure and post-compromise lateral movement: one compromised account anywhere in the organization inherits the grant.
It also does not give the vendor what the vendor actually needs, which is a narrow, attributable identity. The trust subject is an entire AWS account they control, so every workload in that account inherits the access, and role session names are chosen by the caller, so your audit trail is vendor-authored.
And the condition only looks benign because of an earlier decision: onboarding the vendor into the organization as a partner account is what made aws:PrincipalOrgID match them. The policy did not change; the organization did.
Better shape: scope Principal to the vendor's specific role ARN, require an sts:ExternalId unique to Helio, and add aws:PrincipalArn conditions where useful. If any call is granted to an AWS service principal rather than an account, use aws:SourceAccount and aws:SourceArn instead, since those bind the specific resource rather than the whole tenant.
2. Rank the remediation work by risk removed per unit of effort. What do you do in the next hour, and what can you not fix with an SCP?
Read the worked answer
Next hour: enforce IMDSv2. Set http-tokens to required on the three fleets, or set an account-wide default via modify-instance-metadata-defaults, and use an SCP to require it for new launches. It is the shortest path from an SSRF or a compromised workload to account credentials, the change needs no reboot, and it is reversible. Two cautions: some older SDKs and third-party agents do not speak IMDSv2, and a few monitoring agents will break - stage per fleet and have rollback ready.
Second: migrate the CI pipeline off the long-lived access key to OIDC federation with short-lived role assumption. Every one of the 14 wildcard roles traces back to that key, so rotating it buys you a week and the OIDC change ends the class. Third: put an Object Lock or a bucket policy on the log archive denying s3:DeleteObject and s3:PutBucketPolicy to everything except a break-glass role - cheap, and it is what makes the trail usable as evidence after an incident rather than during one. Fourth: the vendor trust policy.
SCPs cannot fix the vendor role. An SCP limits principals inside the accounts it attaches to; it does not shrink a cross-account grant you have already made, and it cannot constrain what the vendor's account does in its own accounts. That is a resource-policy edit, and it needs the org-membership decision revisited at the same time.
3. You cannot prove the vendor role has not already been abused. What evidence would you hunt for, and why is this role hard to monitor?
Read the worked answer
Confirm the organization trail delivers to the immutable archive first, otherwise you are hunting in a log an attacker can edit - which is why the bucket policy fix in this scenario is a prerequisite for the investigation, not a separate ticket.
Then filter for AssumeRole events against the VendorBillingRead role ARN. For each, record the calling account in userIdentity.accountId, the role session name, source IP, and the user agent, and compare against the legitimate billing sync's known cadence, ASN, and session-naming convention. Anomalies worth escalating: sessions from a different account in the org, sessions at hours the sync never runs, sessions with names the vendor does not generate, and any AssumeRole immediately followed by GetCostAndUsage across an unusual date range.
The difficulty is that this role is inherently hard to watch. Principal: "*" means you cannot enumerate the legitimate set of callers, session names are supplied by the caller, and the vendor's normal traffic looks exactly like an attacker's. That is an argument for changing the design rather than tuning the detection: require sts:SourceIdentity so the vendor's identity is asserted upstream and cannot be silently rewritten, scope the principal to one role ARN, and add ExternalId. Also turn on CloudTrail Insights and a guardrail alert on any assume of the role outside the expected source CIDR set - imperfect, but it converts a silent grant into a noisy one.