How to Audit AWS Cross-Account Trust Policies: Lessons from the Qubole Role Abuse
Between 14 and 26 October 2026, an attacker used IAM credentials from an AWS account linked to the data platform Qubole to assume roles in customer AWS accounts. They read secrets, checked which ones worked and downloaded S3 data. Wiz, working with Sentra and Mandiant, published the details on 9 November. AWS says its services “were not compromised by this issue and operated as designed.” That is the uncomfortable part. Every step the attacker took was something a customer’s trust policy allowed. This guide shows how to audit AWS cross-account trust policies in about 30 minutes: find every role an outside account can assume, decide whether it should still exist, and check whether anyone has used it.
What happened, and why External ID didn’t help
The facts below come from Wiz’s report, unless a line says otherwise.
- The way in. Customers who integrated Qubole created IAM roles that trust AWS account
805246085872. Wiz says Qubole’s platform uses this account to assume roles in customer environments. The attacker used IAM credentials from that account. Wiz does not say how those credentials were obtained. - External ID. Every
AssumeRolerequest Wiz linked to the attacker included an External ID, and every one succeeded. Wiz’s advice is to “treat the External ID as non-secret”. It appears in plaintext in CloudTrail, and vendors and infrastructure-as-code often store it. - Dormant roles. Wiz says “most of the roles the threat actor assumed were not otherwise in use.” In most environments it examined, the attacker’s requests were the only
AssumeRoleactivity from the vendor account in October. The trust policies were still in place, so the requests succeeded. - What the attacker did after assuming a role:
- listed buckets, databases, instances, KMS keys and roles
- read Lambda environment variables with
GetFunctionConfiguration - read Secrets Manager values (
GetSecretValue) and SSM parameters (GetParameter) - downloaded S3 objects, sometimes whole buckets in a single session
- Credential testing. TruffleHog checked the stolen credentials: AWS keys through
GetCallerIdentity, Entra ID service-principal secrets and Okta client credentials. - Scale. BleepingComputer, citing Sentra, reports dozens of victims and millions of files taken from one customer alone.
One sentence in the report explains most of the risk: “a source IP condition alone would have blocked every role assumption we observed.” The legitimate Qubole traffic Wiz observed came from AWS infrastructure. The attacker connected from OVH and Mullvad VPN addresses.
We looked at vendor access as a governance question after the Northflank breach. This guide is the hands-on version: commands you can run today.
Step 1: List every role an outside account can assume (5 minutes)
Run this with read-only IAM credentials in each account. It prints every role whose trust policy lets in an AWS principal from outside the current account, including a wildcard *.
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
aws iam list-roles --output json | jq -r --arg acct "$ACCOUNT" '
.Roles[] | .RoleName as $name
| [ .AssumeRolePolicyDocument.Statement
| (if type=="array" then .[] else . end)
| select(.Effect=="Allow")
| (if .Principal=="*" then "*" else (.Principal.AWS // empty) end)
| (if type=="array" then .[] else . end)
| select(. != $acct and (contains(":" + $acct + ":") | not)) ]
| select(length > 0)
| "\($name)\t\(join(","))"'
In an AWS Organization, many results will be your own accounts. Keep the list anyway, because a stale internal trust is still a path. For an ongoing view, turn on IAM Access Analyzer with an external-access analyzer. It reports each role shared outside your zone of trust (your account or your organisation) as a finding:
aws accessanalyzer create-analyzer --analyzer-name external-access --type ORGANIZATION
aws accessanalyzer list-findings --analyzer-arn <analyzer-arn> \
--filter '{"resourceType":{"eq":["AWS::IAM::Role"]},"status":{"eq":["ACTIVE"]}}'
Use --type ACCOUNT if you are not the organisation’s management or delegated admin account.
Step 2: Find out which ones are dormant (5 minutes)
For each role from Step 1, check when it was last used:
aws iam get-role --role-name <role-name> \
--query 'Role.{Created:CreateDate,LastUsed:RoleLastUsed.LastUsedDate,Region:RoleLastUsed.Region}'
Sort the results into three groups:
- No
LastUsed, or nothing in the last 90 days. Treat it as a candidate for removal. If the vendor contract has ended, delete the role, or at least its trust policy, now. Don’t wait for a quarterly review. - Used recently by a vendor you still pay. Keep it, and harden it in Step 4.
- Used recently by a vendor you thought you had stopped using. Go straight to Step 3.
Most of the Qubole victims fell into the first group. The roles had nothing to do day to day, so nobody noticed when someone else started using them.
Step 3: Hunt for cross-account role assumptions (10 minutes)
CloudTrail records every AssumeRole call into your account. The userIdentity.accountId field holds the caller’s account. This loop prints assumptions made by other accounts since 1 October, in every enabled region:
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
for r in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
aws cloudtrail lookup-events --region "$r" \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRole \
--start-time 2026-10-01T00:00:00Z --output json |
jq -r --arg acct "$ACCOUNT" '.Events[].CloudTrailEvent | fromjson
| select(.userIdentity.accountId != null and .userIdentity.accountId != $acct)
| [.eventTime, .userIdentity.accountId, .userIdentity.principalId, .sourceIPAddress,
.requestParameters.roleArn, .requestParameters.roleSessionName] | @tsv'
done
lookup-events covers 90 days of management events and is rate-limited. Across a large organisation, run the same filter in CloudTrail Lake or Athena over your organisation trail.
If you ever integrated Qubole, look for the indicators Wiz published:
- the source account
805246085872calling from a source IP that is not an AWS address - any activity by principal ID
AIDA3W7DIFLYOLTK63SFS, which Wiz says only the attacker used - principal ID
AIDAJYRRH5Y3A7AAJM2HWfrom non-AWS IPs (this ID also carries legitimate Qubole traffic) - role session names starting
quiam- GetCallerIdentitycalls with aTruffleHoguser agent
Wiz’s report also lists the source IPs and full user agents. Some IPs are shared Mullvad VPN exit nodes, so a match on one of those alone is not proof.
For every suspicious session, review what came next, especially Secrets Manager, SSM, Lambda and S3 activity. Remember a gap Wiz points out: CloudTrail management events show the role assumption and the bucket listing, but not the object reads. Without S3 data events or server access logs, you cannot see which files left.
Step 4: Harden the trusts you keep (10 minutes per vendor)
Wiz’s recommendations come down to four changes in the trust policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<vendor-account-id>:role/<vendor-integration-role>" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "<unique-value-for-this-role>" },
"IpAddress": { "aws:SourceIp": ["<vendor-published-egress-cidr>"] }
}
}]
}
- Narrow the principal. Trust the vendor’s specific integration role, not
:root, which means any principal in their account. - Pin the network. Add
aws:SourceIp, oraws:SourceVpceif the vendor connects through a VPC endpoint. Ask the vendor for its published egress ranges. If it can’t give you any, record that answer in your risk register. - Treat External ID as a label, not a password. Use a unique value per role, and rotate it with the vendor. Don’t rely on it as the only condition. It prevents the confused-deputy problem; it was never designed to stop someone holding the vendor’s own credentials.
- Cut the permissions behind the role. Lambda configuration, Secrets Manager, SSM and broad S3 read access are often granted “for convenience”. Each one was a source of stolen data in this campaign.
Impact assessment
- Risk to unaudited AWS estates: high. No CVE and no exploit were needed, only a trust relationship nobody was watching.
- Who is affected: any AWS customer that trusts a third-party account. That includes data platforms, observability tools, backup services and cost-management tools. Qubole is the current example, not the only exposure.
- What makes it worse: roles that can read secrets. Every stolen secret is a way into another system. In this campaign that meant Entra ID and Okta, so one dormant AWS role became an identity incident.
CloudShieldSecure perspective
At CloudKonsult, we built CloudShieldSecure on a simple idea: your cloud’s real perimeter is its identity graph, and most of that graph is invisible until someone draws it. A trust policy is a standing permission that nobody re-approves. Nothing expires when a contract ends, and AWS will honour it until a person deletes it. The hard part of this audit isn’t the commands. It’s doing it continuously: knowing when a new external trust appears, when one goes quiet, and when a quiet one suddenly wakes up from an IP address that doesn’t match the vendor. That continuous view of trusts, permissions and activity is what CloudShieldSecure is designed to provide. See cloudshieldsecure.cloudkonsult.cloud.
Recommended actions
- Today: run Step 1 in every account and delete the trust on any role belonging to a vendor you no longer use.
- If you ever integrated Qubole: run the Step 3 hunt from 1 October. If anything matches, rotate every secret, parameter and Lambda environment variable the role could read. Then revoke the role’s active sessions with the Revoke active sessions action in the IAM console, because rotating a credential does not end sessions that were already issued.
- This week: add
aws:SourceIporaws:SourceVpceconditions to every vendor trust you keep, and narrow:rootprincipals. - This month:
- turn on an organisation-wide external-access analyzer in IAM Access Analyzer
- enable S3 data events, or server access logs, for buckets that hold sensitive data
- alert on third-party role assumptions from new networks or VPNs
- Process: add “remove cloud trust relationships” to your vendor offboarding checklist, next to “revoke SSO accounts”.
Sources & references
- Wiz, “Trusted by Default: Qubole Cross-Account Access Abused in the Wild” (9 Nov 2026): https://www.wiz.io/blog/qubole-cross-account-access-abused-in-the-wild
- BleepingComputer, “Hackers exploit trusted AWS access to breach multiple cloud environments” (10 Nov 2026, updated 11 Nov with AWS statement): https://www.bleepingcomputer.com/news/security/hackers-exploit-trusted-aws-access-to-breach-multiple-cloud-environments/
- Cloud Security Alliance research note on the Qubole cross-account role abuse (10 Nov 2026): https://labs.cloudsecurityalliance.org/research/csa-research-note-qubole-cross-account-role-abuse-20261110-c/
- AWS IAM User Guide — access to AWS accounts owned by third parties (External ID): https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_third-party.html
- AWS IAM Access Analyzer — external access findings: https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html
- Related on CSS Daily: Third-party cloud access review — the Northflank breach · RMM agent inventory — the N-able N-central shutdown
Assess your security posture today
CloudShield Secure scans, validates, and prioritises threats across your entire attack surface.
Explore CloudShield Secure →