Research · Defensive guidance

AWS key quarantine is a compromise marker, not containment

Last reviewed:

Turn AWSCompromisedKeyQuarantine policy attachments into a two-sided CloudTrail investigation that separates secret validation from hostile use and tests what still succeeded after quarantine.

In this research
  1. Contribution
  2. The pattern
  3. Why it matters to cloud defenders
  4. ATT&CK mapping
  5. Detection guidance
  6. What to do now
Research snapshot
Type
Defensive guidance
Reviewed
2026-10-02
ATT&CK
T1552.001, T1078.004, T1530

Contribution

This post adds a correlation model that treats an AWSCompromisedKeyQuarantine policy attachment as the start of a credential-compromise investigation, rather than proof that the incident is contained. It separates GitHub's validation call from hostile use, reconstructs activity on both sides of the quarantine event, and raises the priority when the same access key still completes successful API calls afterwards.

The pattern

A publicly exposed AWS access key can pass through several systems before the account's security team sees it. GitHub scans public repositories and public npm packages for provider-specific secret patterns. Its partner alerts can be sent directly to the issuer, according to GitHub's partner secret-scanning documentation. AWS can then attach an AWSCompromisedKeyQuarantine managed policy to the IAM user behind the exposed key.

Unit 42 tested that path with an access key pushed to a public repository. In its exposure-to-lockdown research, the AWSCompromisedKeyQuarantineV3 policy appeared within ten seconds of the exposure. A GitHub notification followed one second later, then AWS Health and Support notifications. The speed is useful, but the control does not revoke the key.

The distinction matters. AWS describes AWSCompromisedKeyQuarantineV3 as a policy that denies selected actions while avoiding impact to existing resources. Its deny list covers high-risk operations across IAM, EC2, S3, Lambda, Bedrock and other services. It is still a selected list. Any action already allowed to the user and absent from that explicit deny remains subject to the user's other policies. AWS states that the aim is to limit fraud-related activity that leads to unauthorised charges without affecting existing resources, not to cut off all access.

That makes quarantine an unusually strong incident marker with deliberately incomplete containment. A team that closes the alert because the managed policy appeared can miss activity before attachment, continued use of permissions outside the deny list (V3 denies s3:GetObject, s3:ListBucket and s3:ListAllMyBuckets, but not every read API), or attempts that show the intruder testing the new boundary. The policy itself tells defenders that an external process found the credential exposed. It does not say who used it first or what they reached.

The CloudTrail marker also has an attribution trap. Unit 42 observed an AttachUserPolicy management event whose userIdentity named the affected IAM user, even though that user did not attach the policy. The event did not identify the action as AWS automation. A detector that reads userIdentity as the human actor could accuse the wrong principal or suppress the event as self-service administration.

The stable fields are the event shape and target: eventSource is iam.amazonaws.com, eventName is AttachUserPolicy, and requestParameters.policyArn names an AWSCompromisedKeyQuarantine policy. The AttachUserPolicy API also records the target UserName. Those fields identify the affected identity without relying on the misleading actor presentation.

A second, related signal comes from GitHub itself. When validity checks are enabled, GitHub calls STS GetCallerIdentity with a long, explicit user agent that says the call comes from its automated AWS key validation process. Unit 42 found those calls in CloudTrail and traced their source addresses to GitHub's autonomous system. That is exposure evidence, not attacker evidence. Treating it as hostile use will inflate the incident timeline and can hide the first genuinely suspicious request in a pile of expected validation traffic.

Why it matters to cloud defenders

Long-lived IAM user keys do not expire with a login session. AWS's IAM guidance calls them long-term credentials and advises teams to monitor AWS API calls, alert on denied attempts, then review and delete keys as needed. Once a key reaches a public repository, possession is no longer a useful trust signal.

The quarantine event can surface in the default CloudTrail management-event stream. That gives a central cloud SOC a direct signal even when repository notifications go to a development team or AWS Support mail goes to an account owner. Unit 42 specifically called out that organisational gap. The practical fix is to make the CloudTrail event the shared incident trigger, then pull repository and support context into the case.

The blind spot sits on the data plane. CloudTrail records management events by default, but data events are not logged by default. S3 GetObject is a data event. V3 denies GetObject once it is attached, so the object-read window that matters most is the one before quarantine. If an exposed key reads sensitive objects in that window and the relevant bucket has no data-event selector, the account may record the identity control change without recording the data payoff.

This is why a quarantine alert should carry a telemetry check. The analyst needs to know whether S3 object-level events were collected for the affected buckets, and whether the organisation has logs for other data services the user could reach. Absence of an object-read event means little when object reads were never collected.

The V3 policy's shape creates another useful distinction. Denied calls after attachment prove that somebody is still presenting the key, but they do not prove damage. Successful calls after attachment are more serious because they show that the remaining permission surface is usable. Both belong in the case. They answer different questions.

A key can also have legitimate automation behind it. A deployment or scheduled job may continue calling AWS after quarantine and receive AccessDenied. That traffic is operational fallout, not necessarily an intruder. Compare post-quarantine source addresses, user agents, regions and API families with the identity's normal baseline. A new source performing discovery calls is different from a known runner repeating one expected deployment action.

ATT&CK mapping

The entry point maps to T1552.001 Unsecured Credentials: Credentials In Files when an AWS key is committed in source, an environment file or another readable artefact. The secret-scanning alert proves exposure of the credential material. It does not prove that an adversary found or used it.

Use of the leaked key maps to T1078.004 Valid Accounts: Cloud Accounts. CloudTrail events carrying the access key show authenticated API activity under a valid IAM identity. The quarantine attachment is context for that technique, not execution evidence by itself. A GitHub GetCallerIdentity validity check is also authenticated activity, but its declared automation signature should be classified as scanner traffic rather than hostile cloud-account use.

The payoff depends on the APIs observed. Successful S3 object reads, most plausibly before quarantine because V3 denies s3:GetObject, can support T1530 Data from Cloud Storage Object, provided S3 data events are enabled and identify the affected key. Do not report collection from ListAllMyBuckets, an AccessDenied result, or the quarantine policy's deny list. Other successful service calls need their own behaviour-level mapping.

The end-to-end chain is therefore conditional: a credential is exposed in a file, the valid cloud account is used, then a resource operation reveals what the user reached. Keeping those stages separate prevents a high-confidence exposure signal from being reported as confirmed exfiltration.

Detection guidance

Start with the policy attachment. The following Athena query assumes the table schema generated for CloudTrail logs and uses json_extract_scalar because requestparameters is stored as JSON text in that schema. Replace the table name and partition predicate for the local trail.

Code block SQL
SELECT
    eventtime,
    recipientaccountid,
    json_extract_scalar(requestparameters, '$.userName') AS affected_user,
    json_extract_scalar(requestparameters, '$.policyArn') AS policy_arn,
    useridentity.accesskeyid AS recorded_access_key,
    sourceipaddress,
    useragent
FROM cloudtrail_logs
WHERE eventsource = 'iam.amazonaws.com'
  AND eventname = 'AttachUserPolicy'
  AND json_extract_scalar(requestparameters, '$.policyArn') LIKE
      '%AWSCompromisedKeyQuarantine%';

Alert on every result. Do not require an unusual source address or actor name, because the attachment may appear under the affected IAM user's identity. Keep the full event. The request parameter gives the target user, while AWS Support or the secret-scanning record may provide the exposed access key ID.

Build a two-sided timeline once the key ID is known:

  1. Search at least the preceding hour for every CloudTrail event where userIdentity.accessKeyId equals the exposed key. Expand the window to the key's last known legitimate use if the repository commit time is unknown.
  2. Mark GitHub validity checks separately when eventSource is sts.amazonaws.com, eventName is GetCallerIdentity, and the user agent contains GHAS-AWS_KEYID-validation. Confirm the source against current GitHub network data before suppressing it.
  3. Group the remaining events by source address, region, user agent, service, action and result. A new network origin combined with enumeration, credential creation or data access is the hostile-use lane.
  4. Search forward from the attachment until the key is disabled. Retain both successful calls and errors. A burst of denied calls can show continued use. Any successful call from an unrecognised origin shows that quarantine did not close the path.

Order the timeline by parsed eventTime, then retain eventID and requestID as evidence fields. AWS notes that CloudTrail log files are not an ordered stack trace, so file order is not incident order. Normalise timestamps before calculating the before-and-after windows, especially when an organisation aggregates regions or accounts into one store. Keep the repository commit time and GitHub alert time as separate reference points. Neither replaces the AWS event timestamp, and delivery delay should not be mistaken for delayed attacker activity.

Create three case labels rather than one binary alert: exposure-only when the quarantine marker and scanner validation are the only evidence; use-before-quarantine when another origin used the key before attachment; and use-after-quarantine when the key kept producing requests afterwards. Promote the last label when a request succeeded. This state model is simple enough to implement in most SIEM correlation engines and gives responders a severity rationale instead of a generic credential-leak warning.

The highest-priority condition is a successful, non-GitHub API call from the affected key after the quarantine timestamp. The next tier is suspicious activity before quarantine, especially from a new country or autonomous system, followed by access to IAM, STS, storage, compute or secrets services. Repeated AccessDenied events after quarantine sit below successful use but still justify immediate key revocation and scoping.

False positives come from two places. GitHub's own validity check is expected after exposure and declares itself in the user agent. Existing workloads may also keep using the key until it is rotated. Baseline those workloads by stable source, user agent and API family, but do not whitelist the access key globally. A stolen copy can arrive from somewhere else while the normal job keeps running.

For S3, add object-level data events on sensitive buckets before an incident. AWS documents that GetObject, DeleteObject and PutObject are data events and that advanced selectors can restrict collection by event name. A focused selector on high-value buckets costs less than indiscriminate collection and preserves the evidence needed to decide whether exposure became data access.

What to do now

  1. Route AttachUserPolicy events for every AWSCompromisedKeyQuarantine policy to the cloud security queue. Include the raw event, affected user, policy ARN and account ID. Do not use userIdentity alone to name the actor.
  2. Disable the exposed access key, replace it with short-lived role credentials where possible, and follow the AWS Support case. AWS's policy reference says "Do NOT remove this policy" and directs the account owner to the instructions in the support case. Removing it reopens the denied actions while the compromised key may remain valid.
  3. Reconstruct the key's activity before and after the policy attachment. Split GitHub validation traffic from other API calls, then investigate successful post-quarantine activity first.
  4. Check the data-event coverage for resources the IAM user could read. If S3 object events were not enabled, record that as an evidence gap rather than concluding that no data was accessed.
  5. Review the identity's permissions after containment. The quarantine policy limits a published set of dangerous actions, but least privilege decides how much remains outside that set. Delete unused permissions and retire IAM user keys that can be replaced by roles.

The useful mental model is blunt: quarantine says the credential is exposed. It buys response time, but it is not the end of the response. The defender's job is to determine whether the key was used, what succeeded before and after the marker, and where missing data-plane logs prevent a firm answer.

01 ATT&CK references