Safeguard
Incident Analysis•

The Qubole AWS breach: how a trusted vendor role became a back door into customer clouds

An attacker used credentials tied to Qubole's AWS account to assume the integration roles customers had granted it, then read secrets and pulled S3 data. How cross-account trust, external IDs and the confused deputy problem work, how to hunt in CloudTrail, and how to scope every vendor role you have.

Safeguard Security Team
35 min read

The Qubole AWS breach: how a trusted vendor role became a back door into customer clouds

Short answer. Between 14 and 26 October 2026, an attacker used IAM credentials belonging to the AWS account that Qubole's data platform uses to reach its customers, and assumed the cross-account roles those customers had created for Qubole. Every request included an external ID, and AWS accepted every one. Inside each customer account the attacker listed resources, read secrets from Secrets Manager, SSM Parameter Store and Lambda environment variables, tested the stolen credentials, and downloaded S3 data, in some sessions whole buckets. The activity was published by Wiz on 9 November 2026, which worked with Sentra and Mandiant; Sentra first spotted it in its own customers' environments. Most of the abused roles were dormant: the customers had stopped using Qubole, but the trust was still in place.

No AWS service was broken. AWS told BleepingComputer that its "services were not compromised by this issue and operated as designed." That is the uncomfortable lesson. The roles did exactly what their trust policies said. The problem is what the policies said, and how long nobody read them.

This post explains what happened, what is confirmed and what is only reported, how cross-account roles and external IDs actually work, why the external ID did not help here, how to hunt for this in CloudTrail, and how to audit and scope every role a SaaS vendor holds in your cloud. If you only have ten minutes, read "Fix it without waiting for upstream" and do those steps today.

At a glance

ItemDetail
VendorQubole, a data lake platform owned by Idera that operates inside customers' AWS accounts (The Hacker News)
Access pathsts:AssumeRole from Qubole's AWS account 805246085872 into customer integration roles
Main activity window14 to 26 October 2026 (Wiz)
Separate earlier activity8 to 11 October 2026, customer-owned AWS access keys believed to be created for Qubole integrations; Wiz could not link it to the main campaign
Disclosure9 November 2026 (Wiz and Sentra), wide coverage 9 to 10 November
What was takenSecrets from Secrets Manager, SSM Parameter Store and Lambda environment variables; S3 objects, sometimes entire buckets
Downstream credentials testedVictims' AWS access keys, Microsoft Entra ID client secrets, Okta client credentials
Victim countNot published by Wiz. Sentra's post says "more than a dozen organizations" in one place and "dozens" in another; CSO Online reports "dozens of Qubole customers" and BleepingComputer "tens of companies"
Data volumeSentra reports "millions of files" downloaded from one company; no total published
AttributionNone by the researchers. Wiz's 11 November update notes that Risky Business reported an actor calling itself "n0n" claimed responsibility. That is a claim, not a confirmed attribution
Vendor statementNone from Qubole or Idera at the time of the major reports
AWS position"AWS services were not compromised by this issue and operated as designed"

What happened, in order

The timeline below comes from the Wiz write-up unless marked otherwise. Dates are as published.

Date (2026)Event
8 OctIn a separate activity set, an actor tests a batch of customer-owned AWS access keys, believed to have been created for Qubole integrations, all within one hour. In "a small number of cases" this leads to a compromise. Wiz could not tie it to what followed
14 OctFirst role assumption. A Mullvad VPN address, 45.83.220[.]211, calls AssumeRole against customer integration roles as IAM user AIDA3W7DIFLYOLTK63SFS in account 805246085872
15 OctMost activity moves to an OVH-hosted VPS, 57.129.54[.]120
16 OctTruffleHog user agent first seen, validating harvested credentials. An OVH IPv6 address also appears
18 OctThe actor starts using a second IAM user in the same account, AIDAJYRRH5Y3A7AAJM2HW, which Qubole's legitimate traffic also uses. A different Mullvad address, 185.209.199[.]73, assumes roles across multiple victim environments in quick succession
25 OctAnother Mullvad-registered address, 89.37.63[.]175, assumes roles
26 OctLast observed activity from the OVH address and the main tooling user agents
9 NovWiz and Sentra publish. Sentra says it had flagged suspicious activity across several customer environments and passed it to Wiz
10 NovBleepingComputer and CSO Online report, following The Hacker News on 9 Nov. BleepingComputer finds no public statement from Qubole
11 NovAWS's statement is added to the BleepingComputer report. Wiz updates its post with the reported "n0n" claim

Two things are still unknown: how the attacker got credentials for two IAM users in Qubole's account, and whether those credentials have been disabled. Sentra's post makes the second point sharply: because the trust policies stayed in place, "the attacker went quiet without ever losing access."

Why a data platform holds a key to your cloud

Qubole runs big data clusters (Spark, Hive, Presto and similar engines) in the customer's own AWS account. To do that, it has to start and stop EC2 instances, read and write the customer's S3 buckets, and pass an instance role to the machines it launches. The vendor's control plane lives in its account. The compute and the data live in yours.

There are two ways to give a vendor that kind of reach. You can create an IAM user and hand over its long-lived access keys, or you can create an IAM role that the vendor's account is allowed to assume. Qubole's own documentation supports both, and the incident touched both: the 8 to 11 October activity involved access keys, and the main campaign involved roles.

Qubole's setup guide for cross-account IAM roles is typical of SaaS onboarding. It asks the customer to:

  1. Copy the Qubole AWS account ID and an external ID from the Qubole settings page.
  2. Create an EC2 policy. The broad sample allows actions such as ec2:RunInstances, ec2:TerminateInstances and ec2:Describe* on every resource, plus iam:PassRole. A second, more restrictive sample scopes these to a region, account and VPC.
  3. Create an S3 policy. The first statement grants read, write and delete on the bucket paths you name. The second statement grants s3:GetObject, s3:ListBucket and s3:GetBucketLocation on "*". The third grants s3:ListAllMyBuckets on "*".
  4. Attach a trust policy whose principal is arn:aws:iam::QUBOLE_ACCOUNT_ID:root, with an sts:ExternalId condition listing one or more external IDs.

Look at step 3 again. A sample policy that grants s3:GetObject on every resource means a role built from it can read every object in every bucket in the account, unless a bucket policy or other control says otherwise. Customers who copied the sample without editing it gave their data platform, and anyone who could act as their data platform, read access to everything.

Step 4 matters too. A principal of :root does not mean the vendor's root user. It means the whole vendor account: any IAM user or role in that account whose own permissions allow sts:AssumeRole on your role can try to assume it. When two IAM users in that account fell into the wrong hands, every customer role that trusted the account was in reach.

Cross-account IAM roles in plain terms

Every IAM role has two policies, and they answer two different questions.

The trust policy answers "who may become this role?" It names one or more principals (an AWS account, a specific user or role, an AWS service, or a federated identity) and, optionally, conditions such as an external ID or a source IP range.

The permissions policy answers "what may this role do once someone has become it?" It lists actions and resources, exactly like the policy on any user.

When a principal in the vendor's account calls the AWS Security Token Service AssumeRole API with your role's ARN, AWS checks your trust policy and the caller's own permissions. If both allow it, STS returns temporary credentials: an access key ID, a secret access key and a session token, valid for a limited time. Every call the vendor then makes with those credentials is evaluated against your permissions policy.

This beats handing out long-lived keys, as AWS says in its guidance on access for third parties: "Do not give Example Corp access to an IAM user and its long-term credentials in your AWS account. Instead, use an IAM role and its temporary security credentials." Temporary credentials expire. Nobody has to rotate them. They never sit in a config file in your account.

But a role does not remove trust. It moves it. With a role, you no longer have to protect a key that lives with the vendor. You now depend on the vendor protecting every identity in its account that can assume your role. When those identities are compromised, your trust policy is satisfied, and AWS will issue credentials to whoever is holding them.

The same AWS page carries a warning that reads differently after this month: "When you grant third parties access to your AWS resources, they can access any resource that you specify in the policy."

External IDs and the confused deputy problem

Almost every SaaS vendor that asks for a role also gives you an external ID. Many teams treat it like a password. It is not one, and understanding why explains a lot about this incident.

The problem external IDs solve

AWS describes the confused deputy problem as "a security issue where an entity that doesn't have permission to perform an action can coerce a more-privileged entity to perform the action."

Here is the cross-account version. A vendor serves thousands of customers. Each customer pastes a role ARN into the vendor's web console, and the vendor's backend assumes that role whenever it does work for that customer. Role ARNs are not secret; they contain your account ID and a role name that is often predictable. So a second customer, malicious or just careless, pastes your role ARN into their vendor account. The vendor, acting for that second customer, assumes your role and reads your data. The vendor is the deputy. It had the access legitimately, and it was confused about whose behalf it was acting on.

The external ID fixes that. The vendor generates a unique value for each of its customers, tells you yours, and always sends the requesting customer's value in the AssumeRole call. You add a condition to your trust policy:

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Principal": { "AWS": "VENDOR_ACCOUNT_ID" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "sts:ExternalId": "UNIQUE_ID_FROM_VENDOR" }
    }
  }
}

Now when the second customer tricks the vendor into using your ARN, the vendor sends the second customer's external ID, it does not match the one in your policy, and AWS refuses. AWS's own explanation is that the other customer "cannot control the external ID that Example Corp includes in its request to AWS."

What external IDs do not solve

Read the AWS documentation carefully and the limit is spelled out. "AWS does not treat the external ID as a secret." It continues: "The external ID for a role can be seen by anyone with permission to view the role." The AWS example scenario says it again: the value "is not intended to be a 'secret'."

The external ID protects you against a confused vendor. It does nothing against a compromised vendor. If an attacker controls an identity inside the vendor's account, the attacker is no longer asking the vendor to act on its behalf. The attacker is the vendor, as far as AWS can tell. And external IDs are not hard to come by:

  • They are stored in the vendor's systems, mapped to your account, because the vendor has to send them.
  • They are logged in plaintext in CloudTrail, as Wiz notes, inside the request parameters of each AssumeRole event.
  • They often live in infrastructure-as-code, in your Terraform or CloudFormation templates, next to the role definition.
  • Sentra reports that the attacker bulk-read Secrets Manager values that included "the vendor's own tokens and ExternalIds", so one compromised environment can leak the external IDs for others.

Wiz found that every attributed AssumeRole request included an external ID, and every one succeeded. Sentra put it more bluntly: "the attacker presented the right code every time." Nothing in the trust policy distinguished the attacker from Qubole, because the trust policy only checked things the attacker had.

That is the core design flaw this incident exposes. A trust policy that checks the vendor account plus the external ID is checking two identifiers, neither of which is a secret held only by the legitimate party once the vendor's account is breached.

Why it worked: four conditions that lined up

No single mistake made this possible. Four ordinary conditions, each common and each defensible on its own, combined.

1. The trust outlived the relationship. Wiz reports that in a majority of affected environments the attacker's requests were the only AssumeRole activity from Qubole's account in October. Sentra says Qubole's roles "had been dormant since the spring" and that it had flagged one of them as unused for months but still able to reach sensitive data. Companies stopped using the product. Nobody removed the role. Vendor offboarding usually covers contracts, invoices and SSO accounts. It rarely covers an IAM role that a platform team created two years ago.

2. The principal was the whole vendor account. The :root principal meant any identity in Qubole's account with the right permissions could make the call. Narrowing the principal to the specific role or user the vendor uses for customer access would not have stopped this attacker, who held identities in that account, but it shrinks the set of identities whose compromise matters.

3. There was no network condition. Wiz observed that legitimate Qubole traffic came only from AWS infrastructure, while the attacker came from a Mullvad VPN and an OVH VPS. Its conclusion is the most useful line in the whole write-up: "In this activity, a source IP condition alone would have blocked every role assumption we observed." Sentra reached the same conclusion independently: "a network restriction alone would have blocked every login we saw."

4. The roles could reach far more than the product needed. A data platform needs specific buckets. These roles could list every bucket, call GetFunctionConfiguration on every Lambda function (which returns environment variables), list and read Secrets Manager secrets, and read SSM parameters. Some of that came from broad vendor samples. Some, Wiz notes, is "often granted for convenience."

Put those together and a credential theft at one vendor turned into a data theft at many of its customers, including former customers, with every request looking legitimate to AWS.

The attack chain, step by step

This reconstruction follows Wiz's published analysis, with Sentra's additions marked.

Step 1: role assumption. From Qubole's account, the attacker called sts:AssumeRole against customer integration roles, supplying an external ID each time. Role session names followed a pattern: the prefix quiam- plus a word describing the job, such as quiam-enum or quiam-scope-full. Sentra reports each role took "about a second" to assume, and that the attacker first tested on one low-value role, took thousands of customer-profile files, and then entered every Qubole-trusted role within minutes.

Step 2: discovery. With temporary credentials, the attacker enumerated broadly. Wiz lists storage calls (ListBuckets, ListObjects, GetDatabases, ListTables, DescribeClusters), compute (DescribeInstances, DescribeImages, DescribeSecurityGroups, DescribeVpcs), databases (DescribeDBInstances, DescribeDBSnapshots) and identity and keys (ListRoles, ListKeys, ListAliases, DescribeKey, GetKeyPolicy).

Step 3: credential harvesting. Three places were hit in every environment where the role allowed it:

  • Lambda: list functions, then GetFunctionConfiguration on each, which returns environment variables.
  • Secrets Manager: ListSecrets, then GetSecretValue.
  • SSM Parameter Store: DescribeParameters, then GetParameter.

Sentra adds that the attacker also read EC2 user data, used "a second route" for Lambda variables when the first was blocked, and read "about 100 passwords and keys" in seven minutes from three storage locations, plus thousands of credential records inside downloaded files.

Step 4: credential validation. The attacker ran TruffleHog to check what it had collected. For AWS keys, that meant GetCallerIdentity calls in bursts lasting seconds. For Microsoft Entra ID, it meant signing in as service principals with discovered client secrets. For Okta, it meant requesting OAuth tokens with discovered client credentials. Wiz stresses these belonged to the victim organizations, not to Qubole. This is how one vendor compromise spreads beyond AWS into a company's identity provider.

Step 5: data theft. For S3 the pattern was simple: assume the role, ListBuckets, then ListObjects and HeadObject, then GetObject. Wiz describes the actor as opportunistic, reading whatever each role could reach, sometimes pulling a full bucket in one session. Sentra reports a final pass from a different VPN address that pulled production database backups, and "0 write actions": nothing was changed or deleted.

Step 6: a possible next stage. Wiz found sso_agent.iso on the attacker's OVH server: a bootable Linux image that starts a 90-minute clock, polls TCP port 23458 for shell commands, and loads an NTFS driver, which suggests a Windows target. Wiz has not seen it deployed. Treat it as an indicator, not evidence of host compromise.

What is confirmed, what is reported, what is claimed

The numbers in this story will be repeated, so here is where each one comes from.

From Wiz's investigation (with Sentra and Mandiant): the account ID, both IAM principal IDs, the dates, IP addresses, session names, API calls, TruffleHog validation, the ISO file, and the finding that every attributed AssumeRole request included an external ID and succeeded.

From Sentra's own write-up: roles dormant since spring, about 100 secrets read in seven minutes, "millions of files" at one company, database backups taken, zero write actions, and a victim count given as both "more than a dozen" and "dozens."

From AWS: its services were not compromised and operated as designed.

Claimed, not verified: responsibility by an actor calling itself "n0n," reported by Risky Business and noted in Wiz's update. Wiz has not tied the activity to a known group.

Unknown: how Qubole's IAM users were compromised, the total victim count, the total data taken, whether access is closed on the vendor side, and Qubole's or Idera's own account.

Hunting for it in CloudTrail

If any AWS account you own has ever trusted Qubole, run these checks now. Wiz recommends reviewing CloudTrail from 1 October 2026 onward in every account that holds an integration role. Treat 1 October as a floor: dormant trusts may have been probed earlier and simply not reported.

Know what you are looking at

When another account assumes a role you own, the event is written to your trail. AWS documents that for cross-account access to a role you own, userIdentity.type is AWSAccount. The accountId is the caller's account, principalId identifies the calling IAM user, and requestParameters holds the roleArn, the roleSessionName and, if supplied, the externalId.

Calls made with the resulting credentials show userIdentity.type as AssumedRole, and the ARN ends with the session name, for example assumed-role/QuboleRole/quiam-enum. That session name is how you follow the attacker from the door into the house.

Query 1: every Qubole role assumption since 1 October

This assumes CloudTrail logs are queryable in Athena as a table named cloudtrail_logs. Adjust names to your setup.

SELECT
  eventtime,
  useridentity.accountid      AS caller_account,
  useridentity.principalid    AS caller_principal,
  sourceipaddress,
  useragent,
  json_extract_scalar(requestparameters, '$.roleArn')         AS role_arn,
  json_extract_scalar(requestparameters, '$.roleSessionName') AS session_name,
  errorcode
FROM cloudtrail_logs
WHERE eventsource = 'sts.amazonaws.com'
  AND eventname   = 'AssumeRole'
  AND useridentity.accountid = '805246085872'
  AND eventtime >= '2026-10-01T00:00:00Z'
ORDER BY eventtime;

Then apply Wiz's filters to the result:

  • Any row where sourceipaddress is not an AWS address. Wiz says all legitimate platform traffic came from AWS infrastructure. Check addresses against AWS's published IP ranges file.
  • Any row where caller_principal is AIDA3W7DIFLYOLTK63SFS. Wiz saw only the attacker use it, so any hit needs investigation.
  • Rows where caller_principal is AIDAJYRRH5Y3A7AAJM2HW and the source is outside AWS. Qubole's normal traffic also uses this user, so the network is what separates good from bad.
  • Any session_name starting with quiam-.

Query 2: follow each suspicious session

SELECT eventtime, eventsource, eventname, sourceipaddress, useragent,
       requestparameters, errorcode
FROM cloudtrail_logs
WHERE useridentity.type = 'AssumedRole'
  AND useridentity.arn LIKE '%/quiam-%'
  AND eventtime >= '2026-10-01T00:00:00Z'
ORDER BY eventtime;

Widen the LIKE to the exact role name if the session names differ. Pay attention to secretsmanager.amazonaws.com (GetSecretValue), ssm.amazonaws.com (GetParameter, GetParameters), lambda.amazonaws.com (GetFunctionConfiguration) and s3.amazonaws.com. For each secret, parameter or function the session touched, write down the name. That list drives your rotation work.

Query 3: the indicators

Search for any activity from the network indicators Wiz published, remembering that Mullvad addresses are shared VPN exits and are not proof by themselves.

IndicatorTypeNotes from Wiz
805246085872AWS accountSource of all observed role assumptions; also used by legitimate Qubole activity
AIDA3W7DIFLYOLTK63SFSIAM principal IDSeen only in attacker activity
AIDAJYRRH5Y3A7AAJM2HWIAM principal IDUsed by the attacker from 18 Oct; also in legitimate traffic from AWS addresses
quiam-*Role session nameAttacker naming pattern
57.129.54[.]120IPv4, OVHPrimary address, 15 to 26 Oct
2001:41d0:70d[:]7800::IPv6, OVHCredential enumeration and validation, 16 to 24 Oct
45.83.220[.]211IPv4, MullvadRole assumption, 14 to 15 Oct
185.209.199[.]73IPv4, MullvadRole assumption, 18 to 21 Oct
89.37.63[.]175IPv4, Mullvad-registeredRole assumption, 25 Oct
TruffleHog user agentUser agentGetCallerIdentity and Entra ID sign-ins, 16 to 19 Oct; legitimate security teams use it too
sso_agent.isoFileSHA-256 65f061281c9a712e9502793dc4c5035cae490b84da05f7d4223cefa65ad396e4

Wiz also lists several Boto3 and AWS CLI user agent strings. They are common, so use them only alongside another indicator.

Query 4: look outside AWS

Credentials stolen from your AWS account were used against your identity provider. Search Microsoft Entra ID sign-in logs for service principal sign-ins from the indicator addresses or with a TruffleHog user agent, and search Okta system logs for OAuth token requests by client credentials from the same addresses. A clean CloudTrail result does not clear an Entra or Okta client whose secret sat in a Lambda variable that a vendor role could read.

The logging gap

This is where many victims got stuck. CloudTrail management events record AssumeRole, ListBuckets and the like. They do not record which S3 objects were read. That needs S3 data events in CloudTrail or S3 server access logs, and AWS's comparison of the two notes that management events are free for the first copy while data events cost extra. Wiz reports that several victims had neither, so they could not say which objects left. Sentra adds a related trap: its team used encryption logs to prove what was taken, but objects under default encryption leave no key-usage trail, so "the real number is likely higher."

If you cannot prove which objects were read, you may have to assume everything the role could read was read. That is a much larger notification exercise.

If you find a hit: response in the right order

The order matters, because some obvious steps do not do what people expect.

  1. Cut the trust first. Remove Qubole's account from the role's trust policy, or delete the role if you do not use Qubole. This stops new sessions.
  2. Revoke existing sessions. Changing the trust policy does not end sessions already issued. In the IAM console, the role's "Revoke sessions" tab attaches an inline policy named AWSRevokeOlderSessions that denies everything to sessions issued before that moment. AWS documents this along with the condition it uses, aws:TokenIssueTime. Wiz makes the same point: rotating a credential does not end a session already granted.
  3. Preserve evidence before you clean up. Export the relevant CloudTrail events, S3 access logs and KMS logs to a separate location with restricted access. Sentra's checklist puts it simply: "Preserve the logs before they expire."
  4. Rotate everything the role could read, not only what you saw read. Every Secrets Manager secret, every SSM parameter, every Lambda environment variable within the role's reach. Logs show what was read; the role's permissions show what could have been read, and the safer list is the second one.
  5. Follow the credentials outward. For every rotated AWS key, Entra ID client secret and Okta client credential, check the downstream service's logs for use since 14 October.
  6. Scope the data. Use S3 data events or access logs where they exist. Where they do not, list every bucket the role could reach and treat it as exposed until shown otherwise.
  7. Start the legal clock. Under GDPR Article 33, a controller notifies its supervisory authority "without undue delay and, where feasible, not later than 72 hours after having become aware" of a personal data breach. Other regimes have their own clocks. Sentra notes deadlines "can be as short as 72 hours." Bring counsel in early.
  8. Tell the vendor and ask questions in writing. Sentra's list includes notifying Qubole. Ask what it has rotated, whether the two IAM users are disabled, and what network ranges its legitimate traffic uses so you can pin them.

Fix it without waiting for upstream

You do not need a statement from Qubole, Idera or anyone else to close this class of exposure in your own accounts. Every step below is under your control today.

1. Find every role that trusts an outside account. This one-liner lists roles whose trust policy names an AWS principal outside your account. Run it in every account, or use your organization's delegated administration tooling.

ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
aws iam list-roles --output json \
| jq -r --arg me "$ACCOUNT" '
  .Roles[]
  | . as $r
  | [.AssumeRolePolicyDocument.Statement[]?
     | .Principal.AWS? // empty
     | if type == "array" then .[] else . end
     | select(test($me) | not)]
  | select(length > 0)
  | "\($r.RoleName)\t\(join(","))"'

IAM Access Analyzer does this continuously. Its external access analyzer analyzes role trust policies and generates findings for roles "that can be accessed by an external entity that is outside your zone of trust." Turn it on if it is off.

2. Check when each one was last used. ListRoles does not return last-used data, so call GetRole for each candidate:

aws iam get-role --role-name ROLE_NAME \
  --query 'Role.RoleLastUsed' --output json

A vendor role with no use in 90 days, or with a last-used date after you stopped paying the vendor, is your first priority. Sentra's rule is the right one: "a dormant, privileged, external role should be removed, not watched."

3. Delete what you do not use. If you no longer run Qubole, delete the role. If you might return, remove the vendor account from the trust policy and keep the role definition in code.

4. Pin the network for what you keep. Ask each vendor for its documented egress ranges or VPC endpoint IDs, and add a condition. Wiz's evidence says this alone would have blocked every observed assumption.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::VENDOR_ACCOUNT_ID:role/VENDOR_CUSTOMER_ACCESS_ROLE" },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": { "sts:ExternalId": "UNIQUE_ID_FROM_VENDOR" },
        "IpAddress": { "aws:SourceIp": ["VENDOR_EGRESS_CIDR_1", "VENDOR_EGRESS_CIDR_2"] }
      }
    }
  ]
}

Two cautions from the AWS condition key reference. aws:SourceIp "can only be used for public IP address ranges," and it is not present when the request comes through a VPC endpoint; in that case use aws:SourceVpce with the vendor's endpoint ID. Test with the vendor before you enforce, or you will break the integration.

5. Narrow the principal. Where the vendor publishes the specific role or user it uses, replace :root with that ARN, as in the example above. Be aware that AWS stores a specific role principal by its unique ID; if the vendor deletes and recreates that role, the trust breaks and has to be re-saved. That is a feature here, not a bug.

6. Strip the credential stores from vendor roles. Most data integrations have no reason to read your secrets. Add an explicit deny to every vendor role, or attach a permissions boundary that does the same:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenySecretStoresToVendor",
      "Effect": "Deny",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:ListSecrets",
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath",
        "lambda:GetFunctionConfiguration",
        "ec2:DescribeInstanceAttribute"
      ],
      "Resource": "*"
    }
  ]
}

ec2:DescribeInstanceAttribute is in the list because it can return EC2 user data, one of the places Sentra says the attacker read. If a vendor legitimately needs one named secret, allow that ARN alone.

7. Replace "*" in S3 statements with named buckets. If you built from a vendor sample that grants s3:GetObject on every resource, rewrite it to the buckets and prefixes the product actually uses. ListAllMyBuckets lets the role see bucket names; it does not need to read their contents.

8. Move secrets out of Lambda environment variables. Environment variables are returned in full by GetFunctionConfiguration to anyone with that permission. Keep the value in Secrets Manager, fetch it at runtime, and grant the function's own role access to that one secret.

9. Rotate external IDs and make them unique per role. After an event like this, ask the vendor for a new value and use a distinct one per role where the vendor supports it. It will not stop a compromised vendor, but it stops a leaked value from one environment opening another.

10. Turn on the logs you will wish you had. An organization trail covering every account and region, plus S3 data events or server access logs on buckets that hold sensitive data. Set an alert on AssumeRole from any third-party account where the source IP is outside that vendor's known ranges. Wiz calls this a high-signal detection, and it is cheap to build.

11. Retire vendor access keys. If an integration still runs on an IAM user's long-lived keys, as some Qubole setups did, move it to a role with the conditions above, then delete the keys. The 8 to 11 October activity shows such keys are being tested in bulk.

Auditing and scoping vendor roles as a standing program

One cleanup is not enough, because trust policies drift when nobody owns them. This program draws on the Wiz and Sentra recommendations and AWS guidance.

Keep one inventory, tied to the vendor list

For every role that trusts an external principal, record: the vendor, the business owner, the product it supports, the trust policy, the permissions policy, the last-used date, the date of the last review, and the contract status. The key link is between the IAM role and the vendor record your procurement or risk team already keeps. When a contract ends, the role should show up on someone's list that week.

Review on a schedule, and on events

Quarterly is a sensible default. Also review on these triggers: a vendor contract ending, a vendor security incident, a vendor changing ownership, a role unused for 90 days, and any change to a vendor role's permissions.

Write a minimum standard for every vendor role

A short internal standard does more than a long one. A workable version:

  • Principal is the vendor's specific role or user, not :root, where the vendor supports it.
  • External ID condition is present and the value is unique to this role.
  • Source network condition is present, using published vendor ranges or endpoint IDs.
  • Permissions name specific buckets, prefixes, tables or clusters. No "*" resource on data-read actions.
  • Explicit deny on secret stores, parameter stores, Lambda configuration and instance user data, unless documented.
  • Maximum session duration set to the shortest value the vendor's product supports.
  • The role is defined in code, reviewed like any other change, and tagged with vendor and owner.

Ask vendors better questions before you sign

The security questionnaire for any SaaS that asks for a role in your cloud should include:

  • Which identities in your AWS account can call AssumeRole on customer roles, and how are they protected?
  • Do you use long-lived IAM user keys for customer access, or short-lived workload credentials?
  • Will you publish fixed egress IP ranges or VPC endpoint IDs so we can pin our trust policy?
  • Do you monitor your own CloudTrail for AssumeRole calls into customer accounts from unexpected networks?
  • What is the minimum permission set your product needs, and will you provide a scoped policy instead of a broad sample?
  • When we offboard, what do you delete on your side, and do you remind us to delete the role on ours?
  • How quickly will you notify us if an identity that can reach our account is compromised?

For SaaS providers: if you ask customers for a role

Every vendor whose onboarding page says "paste this role ARN" should adopt Wiz's provider recommendations as a baseline:

  • Use short-lived credentials from workload roles to call AssumeRole, not long-lived IAM user keys.
  • Restrict which of your principals can assume customer roles, with source conditions on your side.
  • Publish your egress ranges so customers can pin their trust policies to them.
  • Monitor your own CloudTrail for AssumeRole calls into customer accounts from networks you do not operate.

Add a few more. Ship a least-privilege policy as the default sample. Generate a unique external ID per customer and, as AWS recommends, test that the role cannot be assumed without it. When a customer cancels, tell them to delete the role and show them how. Their former trust in you still sits in their accounts, and your account is where an attacker will go to use it.

The business side: this is third-party risk, not only cloud misconfiguration

Filing this under "AWS hygiene" misses why it happened. The roles were created during vendor onboarding, were never part of offboarding, and nobody in the vendor risk process looked at what the vendor could reach.

Most third-party risk programs collect a questionnaire and a SOC 2 report. Few record the technical access each vendor holds: IAM roles, OAuth grants, API keys, service accounts. That record turns "which of our accounts trust Qubole?" into a ten-minute question instead of a week of archaeology.

The control frameworks already expect it. SOC 2's common criteria include vendor and business partner risk management. ISO/IEC 27001:2022 Annex A has a group of controls on supplier relationships, including addressing security within supplier agreements and monitoring and reviewing supplier services. GDPR makes the controller answerable for processors that touch personal data. An auditor asking "how do you know a former vendor no longer has access?" is asking exactly the question these victims could not answer.

There is also a cost argument. Data events on sensitive buckets cost little next to notifying every data subject whose records sat in a bucket the role could reach.

How Safeguard helps with this class of problem

The research credit for this campaign belongs to Sentra, which spotted the activity, and to Wiz and Mandiant, who investigated it. Safeguard's part is the work around it: helping teams know which vendors they depend on, keep those relationships reviewed, and show the evidence afterward. All of the capabilities below are generally available.

Third-party risk management

Safeguard TPRM is where vendors are onboarded, scored and continuously monitored. It can request SBOMs, projects or products from suppliers and apply policies and gates to what comes back, so supplier risk rests on evidence as well as questionnaire answers. Pair it with the inventory from "Fix it without waiting for upstream": when a vendor is offboarded or turns up in the news, its record is the natural place to attach the follow-up task, including removing the roles you created for it.

Compliance automation with evidence attached

The compliance and GRC suite maps controls, collects evidence and runs tests across frameworks including SOC 2, ISO 27001 and GDPR, with areas for vendors, risks, questionnaires and policies. Vendor access reviews, logging coverage and offboarding steps become controls with owners, schedules and stored evidence, rather than a spreadsheet updated the week before an audit. Safeguard itself is SOC 2 Type I and Type II audited.

Asset discovery and policies

Safeguard's asset discovery inventories repositories, images, packages, AI models, SBOMs, vendors and workloads, then applies guardrails and enforcement to what it finds. Policies, gates and guardrails are enforceable across the organization at build, CI and release, and policies can be written from plain English prompts.

SCA and SBOM monitoring

Supply-chain risk runs through code as well as vendors. Safeguard's SCA builds a full transitive dependency inventory and monitors SBOMs for new vulnerabilities, so when a component you depend on becomes the next incident, "where do we use it?" has a fast answer.

To see how these fit your environment, start at https://safeguard.sh.

FAQ

What happened in the Qubole incident?

An attacker used credentials for two IAM users in the AWS account Qubole uses to reach customers, and assumed the cross-account roles customers had created for Qubole. Between 14 and 26 October 2026 it read secrets from Secrets Manager, SSM Parameter Store and Lambda environment variables, validated them, and downloaded S3 data. Wiz and Sentra disclosed it on 9 November 2026.

Was AWS hacked?

No. AWS said its "services were not compromised by this issue and operated as designed." The attacker held credentials that customers' trust policies accepted, so AWS issued temporary credentials as configured.

How did the attacker get past the external ID?

It supplied an external ID with every request, and every request succeeded. External IDs are not secrets. AWS says so directly, they appear in CloudTrail, they are stored by the vendor, and they often sit in infrastructure code. They prevent a confused vendor from acting for the wrong customer; they do not stop someone who controls the vendor's identities.

What is the confused deputy problem?

It is when a party with legitimate access is tricked into using it on behalf of someone who should not have it. In AWS cross-account terms, another customer gives a vendor your role ARN, and the vendor assumes your role for them. A unique, vendor-generated external ID in your trust policy stops that.

Would anything have blocked this attack?

Yes, according to both research teams. Wiz says "a source IP condition alone would have blocked every role assumption we observed," and Sentra reached the same conclusion. Deleting the dormant roles would also have blocked it for every former customer.

How do I know if my account was affected?

Search CloudTrail from 1 October 2026 for AssumeRole events where userIdentity.accountId is 805246085872. Investigate any from non-AWS addresses, any from principal AIDA3W7DIFLYOLTK63SFS, any from AIDAJYRRH5Y3A7AAJM2HW outside AWS, and any session name starting with quiam-. Then follow each session's activity in Secrets Manager, SSM, Lambda and S3.

I removed the trust policy. Am I done?

No. Removing trust stops new sessions but does not end existing ones. Revoke active sessions on the role, rotate every secret, parameter and Lambda variable the role could read, and check Entra ID, Okta and other downstream systems for use of those credentials.

How many companies were hit?

No authoritative total has been published. Sentra's post says "more than a dozen organizations" in one place and "dozens" in another, and several outlets report "dozens."

Who was behind it?

Unknown. The researchers have not attributed it. Wiz's update notes a report that an actor calling itself "n0n" claimed responsibility; that claim is unverified.

Has Qubole made a statement?

Not at the time of the main reports. The Hacker News and BleepingComputer both found no public statement from Qubole or Idera.

What should every company do this week, even without Qubole?

List every IAM role that trusts an external account, check when each was last used, delete the dormant ones, add source network conditions and named principals to the rest, deny vendor roles access to secret stores, and turn on S3 data events for sensitive buckets.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

We email you a link to confirm, and only then add you to the list. Unsubscribe in one click. Privacy

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.