Partner Role Assumed After ExternalId Exposure
Helio Manufacturing runs 120 AWS accounts under Organizations. An assessment finds 18 IAM roles named PartnerAccess-* in the production account. Each trust policy allows a partner account root and requires sts:ExternalId equal to a 12-character value. The values are stored in a shared Confluence page accessible to all 900 employees and copied into a partner onboarding spreadsheet. CloudTrail shows one role, PartnerAccess-Logistics, was assumed from an IP in a country where the partner has no operations. The session then called s3:ListAllMyBuckets, s3:GetObject on two buckets, and ce:GetCostAndUsage. The role's permission policy is AmazonS3ReadOnlyAccess plus ce:Get*. The partner says the ExternalId is not secret and the access was their new contractor. No S3 objects were downloaded according to CloudTrail data events, but data events are enabled on only 6 of 22 buckets.
1. Did the ExternalId control fail? What is the likely blast radius?
Read the worked answer
ExternalId is not a secret or an authentication factor; it is a confused-deputy mitigation. Its exposure to 900 employees and a spreadsheet means the control is effectively public inside the company. Because the trust policy principal is the partner account root, any principal in that account with sts:AssumeRole permission and the ExternalId can assume the role, not just the named contractor. The unusual country may be the contractor, but it cannot be trusted as proof. Blast radius is broader than the two buckets seen: AmazonS3ReadOnlyAccess permits listing all buckets and reading objects across the account, and ce:Get* exposes cost data. Write access is absent, so integrity impact is low, but confidentiality impact is high. Missing data events on 16 buckets mean CloudTrail cannot rule out reads there; the two logged buckets are a lower bound, not the full scope. Treat this as confirmed unauthorized cross-account access until proven otherwise.
2. What would you do in the first hour, and what durable fix would you require?
Read the worked answer
First hour: attach an explicit deny for the role or remove the trust policy; if partner access must continue, change the ExternalId and communicate over a verified channel. Use a session deny with aws:TokenIssueTime to revoke existing sessions, not just future ones. Preserve CloudTrail and S3 logs before changing. Notify the partner security contact and your legal and privacy teams. Durable fix: replace account-root trust with a specific partner role ARN and conditions on aws:PrincipalArn, sts:ExternalId, and optionally aws:SourceIp or aws:PrincipalOrgID. Scope S3 permissions to named buckets and prefixes, remove ListAllMyBuckets, and split Cost Explorer into a separate role. Rotate all 18 ExternalIds and move them out of Confluence into a secrets manager. Enable S3 data events on all buckets or use an organization trail. Add Access Analyzer and anomaly detection on AssumeRole.
3. What queries and evidence would you use to determine which objects were accessed?
Read the worked answer
Start with CloudTrail management events: AssumeRole, GetCallerIdentity, ListAllMyBuckets, and any GetBucketLocation or ListBucket calls, grouped by role session ARN and source IP. For the 6 buckets with data events, query GetObject and HeadObject. For the other 16, check S3 server access logs if enabled, bucket owner logs, and any CloudFront logs; if none exist, state the limitation rather than claiming no access. Check VPC flow logs only if the role was used from inside a VPC; cross-account calls from the internet will not appear there. Correlate the source IP and ASN with the partner and contractor. Review Cost Explorer queries for scope. Export a timeline and hash evidence. The absence of data events is a logging gap, not evidence of no exfiltration.