CreateAccessKey in CloudTrail: Spotting Backdoor AWS Access Keys
Published
A new long-term access key for an IAM user, or for the root user; the record shows who created it, for whom, and the new key's ID, which every later call made with the key carries.
AWS's description
Creates a new AWS secret access key and corresponding AWS access key ID for the specified user. The default status for new keys is Active.
Source: AWS API reference: CreateAccessKey · copied from AWS's documentation and refreshed weekly
Request parameters
The parameters AWS's API reference documents for CreateAccessKey. A key is shown only where AWS's example records show it, or where every IAM example names keys after the parameter with a lower-case first letter.
| API parameter | Required | In the CloudTrail record |
|---|---|---|
| UserName | No | requestParameters.userName |
Fields that matter in an investigation
| Field | What it tells you |
|---|---|
| 1requestParameters.userName | The user the key was created for. When it is absent, IAM uses the identity that signed the request, which can be the root user. |
| 2responseElements.accessKey.accessKeyId | The new key's ID. Every call later signed with this key carries it in userIdentity.accessKeyId, so it is the thread to pull. The secret access key is not in the record. |
| 3responseElements.accessKey.userName | The key's owner as IAM recorded it. Compare it with userIdentity.userName to tell a user creating their own key from someone creating one for another user. |
| 4responseElements.accessKey.status | Active for a new key, which is the default AWS documents. |
| 5responseElements.accessKey.createDate | When the key was created, written in IAM's own date format (Jul 30, 2024 9:53:13 PM in the recorded event), not ISO 8601, so parse it separately from eventTime. |
| 6userIdentity.arn | Who created the key. A key made by a role session or by another user for a human user's account deserves a question. |
| 7userIdentity.type | IAMUser, AssumedRole, Root or another identity type. Root with no userName in the request means a root access key was created. |
| 8userIdentity.accessKeyId | The key that was used to create the new one. A user's existing key creating a second key for the same user is a classic way to survive the first key being rotated. |
| 9sourceIPAddress | Where the request came from. Compare with where the creator normally works. |
| 10userAgent | The client that made the request, for example a CLI, an SDK or an attack framework that names itself. |
Every field is checked on each build against AWS's CloudTrail record contents and userIdentity documentation and the example and recorded CreateAccessKey records shown below.
What the record looks like
AWS does not publish an example CreateAccessKey record; the record below was captured during an attack simulation.
Recorded during an attack simulation: Stratus Red Team technique aws.persistence.iam-backdoor-user
Numbered lines are the fields in the table.
A real CloudTrail record from a test detonation, published by Stratus Red Team (Datadog, Apache License 2.0) and anonymized there; account IDs, addresses, access key IDs and session tokens are replaced here with AWS's documentation values, and the operator's user name with "operator". The anonymizer also changes values such as the Region.
What it records
CreateAccessKey creates a long-term access key for an IAM user. AWS documents that new keys are Active and that, without a user name, IAM uses the identity that signed the request, which means the call also creates access keys for the root user. The record from iam.amazonaws.com names the creator in userIdentity, the target in requestParameters.userName, and the new key in responseElements.accessKey: its accessKeyId, userName, status and createDate. The secret access key is never in it; AWS notes the secret is only available when the key is created.
The recorded attack-simulation event below is a management event with readOnly false, made with one user’s key for another user. That shape, one identity creating a key for a different one, is the attack the event is watched for.
When it matters
An access key is the easiest durable foothold in AWS: it works from anywhere, needs no MFA and survives a password reset. Creating one for an existing user is MITRE ATT&CK T1098.001 (additional cloud credentials). The records worth stopping on:
- A key created for someone else.
requestParameters.userNamediffers from the caller, particularly when the caller is a role session, a workload identity or an unfamiliar address. - A root access key.
userIdentity.typeRootand no user name in the request. - A second key for a user. The user’s existing key creating another one keeps access alive after the first is rotated or disabled.
- The sequence around it.
GetCallerIdentityfrom a new address, thenCreateUser,CreateAccessKeyandAttachUserPolicyin a few minutes is a stolen key building a backdoor account.
How to investigate
- Read who created the key (
userIdentity.arn,userIdentity.accessKeyId,sourceIPAddress,userAgent) and for whom (requestParameters.userName,responseElements.accessKey.userName). - Ask the owner of the target user whether they requested a key. Developers rarely create keys for other people’s accounts.
- Follow the new key: every event whose
userIdentity.accessKeyIdequalsresponseElements.accessKey.accessKeyId. - Look at the creator’s other calls in the same hour for users, login profiles, policy attachments and changed role trust policies, and deactivate the key with
UpdateAccessKeywhile you confirm.
New keys in Microsoft Sentinel, with the ones created by someone other than the key’s owner flagged:
AWSCloudTrail
| where TimeGenerated > ago(7d)
| where EventName == "CreateAccessKey" and isempty(ErrorCode)
| extend Response = parse_json(ResponseElements)
| extend KeyOwner = tostring(Response.accessKey.userName), NewKeyId = tostring(Response.accessKey.accessKeyId)
| extend MadeBySomeoneElse = UserIdentityType != "IAMUser" or KeyOwner != UserIdentityUserName
| project TimeGenerated, UserIdentityType, UserIdentityArn, KeyOwner, NewKeyId, MadeBySomeoneElse, SourceIpAddress, UserAgent
| order by TimeGenerated desc
What each new key did after it was created:
let NewKeys = AWSCloudTrail
| where TimeGenerated > ago(7d)
| where EventName == "CreateAccessKey" and isempty(ErrorCode)
| project CreatedAt = TimeGenerated, CreatedBy = UserIdentityArn,
NewKeyId = tostring(parse_json(ResponseElements).accessKey.accessKeyId);
AWSCloudTrail
| where TimeGenerated > ago(7d)
| join kind=inner NewKeys on $left.UserIdentityAccessKeyId == $right.NewKeyId
| summarize FirstUse = min(TimeGenerated), Calls = count(), Actions = make_set(EventName, 30),
Addresses = make_set(SourceIpAddress, 10) by NewKeyId, CreatedBy, CreatedAt
In Athena, over a table on the trail’s S3 bucket (named cloudtrail_logs here):
SELECT eventtime, useridentity.type, useridentity.arn AS created_by,
json_extract_scalar(responseelements, '$.accessKey.userName') AS key_owner,
json_extract_scalar(responseelements, '$.accessKey.accessKeyId') AS new_key,
sourceipaddress, useragent
FROM cloudtrail_logs
WHERE eventname = 'CreateAccessKey' AND errorcode IS NULL
ORDER BY eventtime DESC
LIMIT 200; Related events
- CreateUser IAM
- UpdateAccessKey IAM
- CreateLoginProfile IAM
- GetCallerIdentity AWS STS
- AttachUserPolicy IAM
Detect it
MITRE ATT&CK: T1098.001
Frequently Asked Questions
- Does the CreateAccessKey event contain the secret access key?
- No. The recorded event holds the new key's ID, owner, status and creation date in responseElements.accessKey, but not the secret. AWS notes the secret is available only when the key is created, in the API response, so whoever made the call has it and the log does not.
- How do I tell a backdoor key from a user rotating their own key?
- Compare requestParameters.userName, or responseElements.accessKey.userName, with the identity in userIdentity. A key that a different user, a role session or an unfamiliar address created for someone else is the pattern Stratus Red Team records for an access key backdoor, and should be confirmed with the account owner.
- Can CreateAccessKey create keys for the root user?
- Yes. AWS documents that without a user name, IAM picks the user from the key that signed the request, and that the operation can manage root user credentials. A CreateAccessKey with userIdentity.type Root and no userName is a new root access key, which most organisations never want to exist.
- What should I check after finding an unexpected CreateAccessKey?
- Search every event whose userIdentity.accessKeyId equals the new key's ID to see what it did, check whether the creator also made users, login profiles or policy changes, and deactivate the key with UpdateAccessKey until the owner confirms it.
Find every CreateAccessKey in your own CloudTrail logs
Load CloudTrail logs (the Records files CloudTrail writes to S3, or aws cloudtrail lookup-events output) into the free browser analyzer: logging changes, new access keys, administrator grants, root activity and failed console sign-ins are flagged, with event names linked to their pages here. Nothing is uploaded.