Detection rules › Elastic

AWS S3 Object Encryption Using External KMS Key

Status
production
Severity
medium
Time window
6m
Author
Elastic
Source
github.com/elastic/detection-rules

Identifies use of the S3 CopyObject API where the destination object is encrypted using an AWS KMS key from an external AWS account. This behavior may indicate ransomware-style impact activity where an adversary with access to a misconfigured S3 bucket encrypts objects using a KMS key they control, preventing the bucket owner from decrypting their own data. This technique is a critical early signal of destructive intent or cross-account misuse.

MITRE ATT&CK coverage

TacticTechniques
ImpactT1486 Data Encrypted for Impact

Event coverage

ProviderEvent
AWS-s3CopyObject

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body elastic

[metadata]
creation_date = "2024/07/02"
integration = ["aws"]
maturity = "production"
updated_date = "2026/04/10"

[rule]
author = ["Elastic"]
description = """
Identifies use of the S3 CopyObject API where the destination object is encrypted using an AWS KMS key from an external
AWS account. This behavior may indicate ransomware-style impact activity where an adversary with access to a
misconfigured S3 bucket encrypts objects using a KMS key they control, preventing the bucket owner from decrypting their
own data. This technique is a critical early signal of destructive intent or cross-account misuse.
"""
false_positives = [
    """
    Cross-account KMS key usage may be legitimate in multi-account AWS Organizations architectures where centralized
    encryption keys are used for data governance or auditing workflows. Confirm whether the external KMS key belongs to
    an expected account before taking action. Data migration or cross-account backup workflows may legitimately
    re-encrypt S3 objects using a key in another account. Ensure these workflows are documented, tied to known IAM
    roles, and occur on predictable schedules.
    """,
]
from = "now-6m"
language = "esql"
license = "Elastic License v2"
name = "AWS S3 Object Encryption Using External KMS Key"
note = """## Triage and analysis

### Investigating AWS S3 Object Encryption Using External KMS Key

This rule detects when an S3 `CopyObject` operation encrypts an object using a KMS key belonging to a different AWS account than the bucket owner. This behavior is unusual and a strong indicator of:

- Cloud ransomware techniques, where adversaries encrypt data using a key only they control.
- Cross-account privilege misuse, especially when an unauthorized principal has write access to S3.
- Misconfigured bucket permissions, enabling principals from another account to perform privileged copy operations.
- Early impact-stage activity in incidents where attackers prepare to destroy availability or deny the owner access.

The rule uses ESQL to identify cases where the `cloud.account.id` (bucket owner) differs from the dissected `kms_key_account_id` used for encrypting the new object version.


#### Possible investigation steps

**Identify the actor and access pathway**
- Review `aws.cloudtrail.user_identity.arn` and `aws.cloudtrail.user_identity.access_key_id`.
- Check whether the caller is:
  - A legitimate cross-account automation role,  
  - A compromised IAM user or workload identity, or  
  - A federated identity behaving outside of normal patterns.
- Inspect `user_agent.original` to determine whether the action came from the AWS Console, CLI, SDK, or unusual tooling.

**Analyze the encryption behavior**
- Inspect the dissected KMS key fields:
  - `Esql.aws_cloudtrail_request_parameters_kms_key_account_id`
  - `Esql.aws_cloudtrail_request_parameters_kms_key_id`
- Confirm whether the external key:
  - Belongs to an attacker-controlled account,  
  - Is unknown to your organization, or  
  - Lives in a shared or security tooling account.

**Assess the objects affected**
- Review:
  - `Esql.aws_cloudtrail_request_parameters_target_bucket_name`
  - `Esql.aws_cloudtrail_request_parameters_target_object_key`
- Identify:
  - Whether objects were overwritten or new encrypted copies were created.
  - The sensitivity or criticality of the affected data.
  - Whether object versioning is enabled (important for recovery).

**Correlate surrounding access patterns**
Pivot in CloudTrail on:
- The same access key ID  
- The same IAM principal  
- Affected bucket ARN  

Look for:
- `DeleteObject` or `DeleteObjects` calls (common in ransomware behavior)
- Mass enumeration prior to the event (`ListObjectsV2`, `GetObject`)
- Other impact-stage actions (`PutBucketPolicy`, `PutBucketAcl`, disabling logging)
- Attempts to encrypt additional objects in rapid succession

**Evaluate bucket permissions and exposure**
Review:
- S3 bucket policy changes
- IAM roles with `s3:PutObject` or `s3:PutObjectAcl` permissions
- Whether unintended cross-account `Principal` entries exist
- Whether the KMS key policy explicitly trusts your account or a foreign one

**Validate business justification**
- Confirm with storage, data engineering, or application teams whether:
  - Any migration, transformation, or backup workflows should be encrypting objects cross-account.
  - Scheduled jobs or CI/CD pipelines were operating at the time of the event.

### False positive analysis

- **Expected cross-account encryption**  
  Many organizations use centralized encryption accounts or shared security accounts. Validate:
  - Whether the KMS key account is part of your AWS Organization
  - Whether the workflow, role, or application is documented
  - Whether the principal routinely performs CopyObject operations

### Response and remediation

**Contain and prevent further impact**
- Immediately restrict S3 write access for the principal involved.
- If the KMS key is attacker-controlled, the impacted objects may be unrecoverable without versioning.
- If object versioning is disabled, enable it on the affected bucket to strengthen future resilience.

**Investigate scope and severity**
- Identify:
  - Additional objects encrypted using external keys
  - Related suspicious actions (delete, modify, exfiltration events)
  - Whether any ransom markers or unauthorized files were uploaded
- Validate whether the external KMS key grants *decrypt* permission back to the bucket owner (rare in attacker use).

**Recover and secure the bucket**
- Restore accessible previous versions if versioning is enabled.
- Revoke unauthorized access key pairs or session credentials.
- Audit bucket policies, ACLs, and IAM conditions (`aws:PrincipalArn`, `aws:SourceAccount`, `aws:SourceArn`).
- Tighten cross-account access controls:
  - Remove unintended `Principal` clauses
  - Restrict KMS usage to known accounts
  - Enforce SCPs that block cross-account KMS use unless explicitly approved

**Long-term hardening**
- Integrate object-level access logging and S3 server access logging into security monitoring.
- Add AWS Config rules (or Security Hub controls) detecting:
  - Public buckets
  - Cross-account access to S3
  - KMS policies permitting foreign principals
- Document required cross-account workflows and add explicit allowlists.

### Additional information

- **[AWS IR Playbooks](https://github.com/aws-samples/aws-incident-response-playbooks/blob/c151b0dc091755fffd4d662a8f29e2f6794da52c/playbooks/)** 
- **[AWS Customer Playbook Framework](https://github.com/aws-samples/aws-customer-playbook-framework/tree/a8c7b313636b406a375952ac00b2d68e89a991f2/docs)** 
- **Security Best Practices:** [AWS Knowledge Center – Security Best Practices](https://aws.amazon.com/premiumsupport/knowledge-center/security-best-practices/).
"""
references = [
    "https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html/",
    "https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKey.html/",
    "https://www.gem.security/post/cloud-ransomware-a-new-take-on-an-old-attack-pattern/",
    "https://rhinosecuritylabs.com/aws/s3-ransomware-part-1-attack-vector/",
]
risk_score = 47
rule_id = "ab8f074c-5565-4bc4-991c-d49770e19fc9"
setup = "AWS S3 data event types need to be enabled in the CloudTrail trail configuration for CopyObject events."
severity = "medium"
tags = [
    "Domain: Cloud",
    "Data Source: AWS",
    "Data Source: Amazon Web Services",
    "Data Source: AWS S3",
    "Data Source: AWS KMS",
    "Use Case: Threat Detection",
    "Tactic: Impact",
    "Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail-* metadata _id, _version, _index

// any successful S3 copy event
| where
  data_stream.dataset == "aws.cloudtrail"
  and event.provider == "s3.amazonaws.com"
  and event.action == "CopyObject"
  and event.outcome == "success"

// dissect request parameters to extract KMS key info and target object info
| dissect aws.cloudtrail.request_parameters
    "{%{?bucketName}=%{Esql.aws_cloudtrail_request_parameters_target_bucket_name},%{?x-amz-server-side-encryption-aws-kms-key-id}=%{?arn}:%{?aws}:%{?kms}:%{?region}:%{Esql.aws_cloudtrail_request_parameters_kms_key_account_id}:%{?key}/%{Esql.aws_cloudtrail_request_parameters_kms_key_id},%{?Host}=%{?tls.client.server.name},%{?x-amz-server-side-encryption}=%{?server_side_encryption},%{?x-amz-copy-source}=%{?bucket.object.name},%{?key}=%{Esql.aws_cloudtrail_request_parameters_target_object_key}}"

// detect cross-account key usage
| where cloud.account.id != Esql.aws_cloudtrail_request_parameters_kms_key_account_id

// keep ECS and dissected fields
| keep
  @timestamp,
  data_stream.namespace,
  user.name,
  user_agent.original,
  source.ip,
  aws.cloudtrail.user_identity.arn,
  aws.cloudtrail.user_identity.type,
  aws.cloudtrail.user_identity.access_key_id,
  aws.cloudtrail.resources.arn,
  aws.cloudtrail.resources.type,
  event.action,
  event.outcome,
  cloud.account.id,
  cloud.region,
  aws.cloudtrail.request_parameters,
  aws.cloudtrail.response_elements,
  Esql.aws_cloudtrail_request_parameters_target_bucket_name,
  Esql.aws_cloudtrail_request_parameters_target_object_key,
  Esql.aws_cloudtrail_request_parameters_kms_key_account_id,
  Esql.aws_cloudtrail_request_parameters_kms_key_id,
  _id,
  _version,
  _index
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1486"
name = "Data Encrypted for Impact"
reference = "https://attack.mitre.org/techniques/T1486/"


[rule.threat.tactic]
id = "TA0040"
name = "Impact"
reference = "https://attack.mitre.org/tactics/TA0040/"

[rule.investigation_fields]
field_names = [
    "@timestamp",
    "user.name",
    "user_agent.original",
    "source.ip",
    "aws.cloudtrail.user_identity.arn",
    "aws.cloudtrail.user_identity.type",
    "aws.cloudtrail.user_identity.access_key_id",
    "aws.cloudtrail.resources.arn",
    "aws.cloudtrail.resources.type",
    "event.action",
    "event.outcome",
    "cloud.account.id",
    "cloud.region",
    "aws.cloudtrail.request_parameters",
    "aws.cloudtrail.response_elements",
    "Esql.aws_cloudtrail_request_parameters_target_bucket_name",
    "Esql.aws_cloudtrail_request_parameters_target_object_key",
    "Esql.aws_cloudtrail_request_parameters_kms_key_account_id",
    "Esql.aws_cloudtrail_request_parameters_kms_key_id",
]

Stages and Predicates

Stage 1: from

from logs-aws.cloudtrail-* metadata _id, _version, _index

Stage 2: where

| where
  data_stream.dataset == "aws.cloudtrail"
  and event.provider == "s3.amazonaws.com"
  and event.action == "CopyObject"
  and event.outcome == "success"

Stage 3: dissect

| dissect aws.cloudtrail.request_parameters
    "{%{?bucketName}=%{Esql.aws_cloudtrail_request_parameters_target_bucket_name},%{?x-amz-server-side-encryption-aws-kms-key-id}=%{?arn}:%{?aws}:%{?kms}:%{?region}:%{Esql.aws_cloudtrail_request_parameters_kms_key_account_id}:%{?key}/%{Esql.aws_cloudtrail_request_parameters_kms_key_id},%{?Host}=%{?tls.client.server.name},%{?x-amz-server-side-encryption}=%{?server_side_encryption},%{?x-amz-copy-source}=%{?bucket.object.name},%{?key}=%{Esql.aws_cloudtrail_request_parameters_target_object_key}}"

Stage 4: where

| where cloud.account.id != Esql.aws_cloudtrail_request_parameters_kms_key_account_id

Stage 5: keep

| keep
  @timestamp,
  data_stream.namespace,
  user.name,
  user_agent.original,
  source.ip,
  aws.cloudtrail.user_identity.arn,
  aws.cloudtrail.user_identity.type,
  aws.cloudtrail.user_identity.access_key_id,
  aws.cloudtrail.resources.arn,
  aws.cloudtrail.resources.type,
  event.action,
  event.outcome,
  cloud.account.id,
  cloud.region,
  aws.cloudtrail.request_parameters,
  aws.cloudtrail.response_elements,
  Esql.aws_cloudtrail_request_parameters_target_bucket_name,
  Esql.aws_cloudtrail_request_parameters_target_object_key,
  Esql.aws_cloudtrail_request_parameters_kms_key_account_id,
  Esql.aws_cloudtrail_request_parameters_kms_key_id,
  _id,
  _version,
  _index

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
cloud.account.idne
  • Esql.aws_cloudtrail_request_parameters_kms_key_account_id
data_stream.dataseteq
  • aws.cloudtrail
event.actioneq
  • CopyObject
event.outcomeeq
  • success
event.providereq
  • s3.amazonaws.com

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
@timestampKEEP @timestamp
data_stream.namespaceKEEP data_stream.namespace
user.nameKEEP user.name
user_agent.originalKEEP user_agent.original
source.ipKEEP source.ip
aws.cloudtrail.user_identity.arnKEEP aws.cloudtrail.user_identity.arn
aws.cloudtrail.user_identity.typeKEEP aws.cloudtrail.user_identity.type
aws.cloudtrail.user_identity.access_key_idKEEP aws.cloudtrail.user_identity.access_key_id
aws.cloudtrail.resources.arnKEEP aws.cloudtrail.resources.arn
aws.cloudtrail.resources.typeKEEP aws.cloudtrail.resources.type
event.actionKEEP event.action
event.outcomeKEEP event.outcome
cloud.account.idKEEP cloud.account.id
cloud.regionKEEP cloud.region
aws.cloudtrail.request_parametersKEEP aws.cloudtrail.request_parameters
aws.cloudtrail.response_elementsKEEP aws.cloudtrail.response_elements
Esql.aws_cloudtrail_request_parameters_target_bucket_nameKEEP Esql.aws_cloudtrail_request_parameters_target_bucket_name
Esql.aws_cloudtrail_request_parameters_target_object_keyKEEP Esql.aws_cloudtrail_request_parameters_target_object_key
Esql.aws_cloudtrail_request_parameters_kms_key_account_idKEEP Esql.aws_cloudtrail_request_parameters_kms_key_account_id
Esql.aws_cloudtrail_request_parameters_kms_key_idKEEP Esql.aws_cloudtrail_request_parameters_kms_key_id
_idKEEP _id
_versionKEEP _version
_indexKEEP _index